简介:这是一份面向前端开发者、爬虫初学者及网页分析爱好者的在线扒站源码工具包,聚焦HTML静态页面抓取与本地镜像构建,解决快速获取目标网站结构代码、学习网页组织逻辑及开展竞品分析等实际需求。压缩包共16个文件,含7个JavaScript库(如jQuery、Lodash、Clipboard.js等用于交互与DOM操作)、2个HTML主页面(含index.html和404.html)、2个PNG图标、3个SVG品牌标识、1个CSS样式文件及1个ICO图标,整体仅129KB,轻量易部署,适合即下即用与源码级学习。已有5143人学习下载,资源结构清晰,前端依赖完整,内置geopattern.min.js可生成背景图案,localforage.min.js支持离线数据存储,site.min.js整合核心扒站逻辑,配合Bootstrap与QR码生成能力,具备简易网页快照与本地化浏览基础功能。
1. 在线扒站源码:不是“一键下载整个网站”,而是精准提取可复用的前端资产
“在线扒站源码”这个说法在开发者圈里长期存在歧义——很多人第一反应是“把别人网站所有 HTML、CSS、JS 全拖下来改个 logo 就上线”,这既违反《反不正当竞争法》第十二条关于技术手段干扰他人网络产品正常运行的规定,也踩了《计算机软件保护条例》对网页前端代码作为“文字作品”受著作权保护的红线。但真实工程中,“扒站源码”本质是合法合规的前端资产分析行为:比如某高校实验室做跨平台 UI 组件兼容性研究,需要批量采集主流 CMS 系统的表单渲染逻辑;又比如某公司做竞品前端性能基线测试,需提取目标站点首屏关键 JS 的加载链路与资源体积。这类需求的核心诉求是:在不触发服务端反爬机制、不下载非必要资源、不执行恶意脚本的前提下,稳定获取目标页面的原始 HTML 结构、内联样式、外链 JS/CSS 的 URL 列表及基础元信息。本文聚焦于这一具体场景,提供一套基于现代浏览器 DevTools 协议 + 静态解析的轻量级方案,全程无需安装任何第三方在线服务、不依赖不可控的网页快照接口,所有操作可在本地 Chrome 浏览器中完成,且输出结果可直接用于后续的自动化比对或组件逆向分析。
2. 为什么不用 curl 或 requests 直接抓 HTML?——从网络层到渲染层的认知差
2.1 纯 HTTP 请求为何常失效:服务端 UA 检测与动态渲染陷阱
很多新手尝试用curl -H "User-Agent: Mozilla/5.0..." https://example.com获取首页,结果返回 403 或空白 HTML。这不是因为“网站封了 IP”,而是现代站点普遍采用两层防御:
- 服务端 UA 过滤:Nginx 或 Cloudflare 配置中,若请求头不含
Accept: text/html,application/xhtml+xml或Sec-Fetch-*系列字段,直接拦截; - 客户端渲染(CSR)主导:目标页面
<body>内容为空,实际 DOM 由main.7a2b3c.js动态注入,curl只能拿到骨架 HTML。
提示:可通过
curl -I https://example.com查看响应头中的X-Powered-By或Server字段,快速判断后端是否为 Next.js/Nuxt 等 SSR/SSG 框架——这类站点即使服务端渲染,也可能在客户端二次覆盖 DOM,纯 HTML 抓取仍不可靠。
2.2 真实可行的路径:复用 Chrome DevTools Protocol(CDP)
CDP 是 Chrome 浏览器暴露的底层调试协议,它允许外部程序控制浏览器行为,其优势在于:
- 完全模拟真实用户环境:自动携带完整 UA、Cookie、TLS 指纹、
Sec-Fetch-*头; - 支持等待 DOM 加载完成:通过
Page.loadEventFired事件确保 HTML 解析完毕; - 可禁用图片/字体加载:大幅降低带宽消耗,加速分析流程;
- 能捕获所有网络请求:精准提取
<script src="...">和<link rel="stylesheet" href="...">的真实 URL。
常见做法是使用 Python 的pychrome或更稳定的undetected-chromedriver启动无头 Chrome 实例,但本文选择更轻量的方案:直接调用 Chrome 自带的--remote-debugging-port模式 +requests发送 CDP 命令,避免额外依赖。
2.3 最小化 CDP 控制脚本:三步定位核心资源
以下 Python 脚本仅依赖标准库requests和json,适用于 Chrome 115+ 版本:
import requests import json import time # 启动 Chrome 时添加参数:chrome --remote-debugging-port=9222 --headless=new --disable-gpu DEBUG_URL = "http://127.0.0.1:9222" def get_target_page(url): # 1. 创建新标签页 resp = requests.post(f"{DEBUG_URL}/json/new", json={"url": url}) tab_info = resp.json() # 2. 向该标签页发送 CDP 命令 ws_url = tab_info["webSocketDebuggerUrl"] # 注意:此处不直接连接 WebSocket,而是用 /devtools/page/{id} 的 HTTP 接口(Chrome 115+ 支持) page_id = tab_info["id"] cdp_url = f"{DEBUG_URL}/devtools/page/{page_id}" # 3. 执行 Page.navigate 并等待加载完成 navigate_payload = { "method": "Page.navigate", "params": {"url": url} } requests.post(f"{cdp_url}/json", json=navigate_payload) # 等待 loadEventFired(最大等待 10 秒) for _ in range(100): time.sleep(0.1) events = requests.get(f"{cdp_url}/json").json() if any("loadEventFired" in e.get("method", "") for e in events.get("result", [])): break # 4. 获取完整 HTML(注意:这是渲染后的最终 DOM,含 JS 注入内容) html_payload = { "method": "DOM.getDocument", "params": {"depth": -1, "pierce": True} } doc_resp = requests.post(f"{cdp_url}/json", json=html_payload) root_node_id = doc_resp.json()["result"]["root"]["nodeId"] # 5. 序列化 HTML serialize_payload = { "method": "DOM.getOuterHTML", "params": {"nodeId": root_node_id} } html_resp = requests.post(f"{cdp_url}/json", json=serialize_payload) return html_resp.json()["result"]["outerHTML"] # 使用示例 if __name__ == "__main__": html = get_target_page("https://example.com") print(html[:500]) # 打印前 500 字符验证代码逻辑说明:
- 第 1 步创建新标签页,避免污染主浏览器会话;
- 第 2–3 步通过
/devtools/page/{id}接口发送导航命令,该接口是 Chrome 115 后推荐的 HTTP 替代方案,比 WebSocket 更易调试; - 第 4 步调用
DOM.getDocument获取 DOM 树根节点 ID,depth: -1表示获取全部层级; - 第 5 步用
DOM.getOuterHTML提取完整 HTML 字符串,此结果等同于在 DevTools 中右键“Copy outerHTML”的效果。
关键参数说明:
--headless=new:启用新版无头模式,兼容现代 CSS Grid/Flexbox 渲染;--disable-gpu:避免 Linux 服务器无显卡环境报错;--no-sandbox:仅限开发环境使用,生产环境应配置 proper sandbox;--remote-debugging-port=9222:必须显式指定端口,否则默认随机分配,脚本无法固定连接。
3. 如何从 HTML 中精准提取 JS/CSS 资源链接?——避开正则的 DOM 解析法
3.1 为什么不用re.findall(r'<script.*?src="(.*?)">', html)?
正则解析 HTML 是前端开发界的“玄学禁区”:
<script src="a.js" async></script>与<script>console.log('x')</script>混在同一行时,正则易错匹配;<link rel="preload" as="script" href="b.js">这类预加载资源会被漏掉;srcset、>from lxml import etree, html from urllib.parse import urljoin, urlparse def extract_resources(html_content: str, base_url: str) -> dict: """ 从渲染后 HTML 中提取四类资源: - scripts: <script src="..."> 和内联 script - stylesheets: <link rel="stylesheet"> 和 <style> - preload_scripts: <link rel="preload" as="script"> - module_scripts: <script type="module"> """ tree = html.fromstring(html_content) resources = { "scripts": [], "stylesheets": [], "preload_scripts": [], "module_scripts": [] } # 1. 外链脚本 for script in tree.xpath("//script[@src]"): src = script.get("src") if src: full_url = urljoin(base_url, src) resources["scripts"].append({ "type": "external", "url": full_url, "integrity": script.get("integrity"), "crossorigin": script.get("crossorigin") }) # 2. 内联脚本(取前 200 字符防爆内存) for script in tree.xpath("//script[not(@src)]"): content = script.text_content().strip()[:200] if content: resources["scripts"].append({ "type": "inline", "content": content }) # 3. 外链样式表 for link in tree.xpath("//link[@rel='stylesheet']"): href = link.get("href") if href: full_url = urljoin(base_url, href) resources["stylesheets"].append({ "url": full_url, "media": link.get("media", "all") }) # 4. 预加载脚本(关键!很多 SPA 的入口 JS 用此方式加载) for link in tree.xpath("//link[@rel='preload'][@as='script']"): href = link.get("href") if href: full_url = urljoin(base_url, href) resources["preload_scripts"].append({ "url": full_url, "as": "script" }) # 5. ES Module 脚本(现代框架常用) for script in tree.xpath("//script[@type='module']"): src = script.get("src") if src: full_url = urljoin(base_url, src) resources["module_scripts"].append({ "url": full_url, "type": "module" }) return resources # 使用示例 if __name__ == "__main__": base_url = "https://example.com" html = get_target_page(base_url) # 复用上一节函数 res = extract_resources(html, base_url) print(json.dumps(res, indent=2, ensure_ascii=False))参数说明与工程取舍:
urljoin(base_url, src):处理相对路径,如<script src="/js/main.js">→https://example.com/js/main.js;text_content()[:200]:内联脚本只取前 200 字符,避免因广告 JS 过长导致内存溢出;xpath("//link[@rel='preload'][@as='script']"):精准匹配预加载脚本,这是 Next.js/Vite 项目启动的关键线索;- 未提取
<img>或<iframe>:因标题明确限定为“扒站源码”,图像/嵌入内容不属于前端代码资产范畴。
3.3 输出结果的结构化价值:为后续分析铺路
上述函数返回的字典可直接存为 JSON 文件,例如
example_com_resources.json,其结构天然支持后续操作:- 去重分析:统计
resources["scripts"]中不同 CDN 域名(如cdn.jsdelivr.netvsunpkg.com)占比; - 完整性校验:对比
preload_scripts与module_scripts是否有重叠 URL,判断是否冗余加载; - 安全审计:扫描
integrity字段是否存在空值,标记未启用 SRI(Subresource Integrity)的脚本。
4. 常见问题排查:五个让新手翻车的真实场景与解法
4.1 现象:Chrome 启动后
http://127.0.0.1:9222/json返回空数组原因:Chrome 未正确启用远程调试,或端口被占用。常见误操作包括:
- 在已运行的 Chrome 实例上加参数(需完全关闭所有 Chrome 进程);
- Linux 环境下未加
--no-sandbox(但生产环境严禁此参数); - Windows 防火墙阻止 9222 端口。
解决:
- 彻底关闭 Chrome:Mac 上
killall -9 Google\ Chrome,Windows 上任务管理器结束所有chrome.exe; - 启动命令改为:
chrome --remote-debugging-port=9222 --headless=new --disable-gpu --no-sandbox --user-data-dir=/tmp/chrome-test; - 验证端口:
curl http://127.0.0.1:9222/json应返回[{"description":"","devtoolsFrontendUrl":"...","id":"...","title":"about:blank","type":"page","url":"about:blank","webSocketDebuggerUrl":"ws://127.0.0.1:9222/devtools/page/..."}。
4.2 现象:
get_target_page()返回的 HTML 中<body>仍是空的原因:目标页面使用
window.addEventListener('DOMContentLoaded', ...)延迟渲染,而loadEventFired事件在 DOM 解析完成时即触发,早于 JS 执行。解决:
- 改用
Runtime.evaluate执行 JS 等待条件:在navigate后插入以下命令:wait_payload = { "method": "Runtime.evaluate", "params": { "expression": "new Promise(r => { if (document.body.innerHTML) r(); else document.addEventListener('DOMContentLoaded', r); })", "awaitPromise": True, "returnByValue": True } } requests.post(f"{cdp_url}/json", json=wait_payload) - 或更简单:在
DOM.getDocument前加time.sleep(2)(仅限调试,正式脚本需用Runtime.evaluate)。
4.3 现象:
extract_resources()提取的srcURL 404原因:目标站点对资源请求做了 Referer 校验或 Token 签名,如
https://api.example.com/static/js/main.js?t=123456789中的t参数过期。解决:
- 在 CDP 中启用 Network 域并捕获所有请求:
requests.post(f"{cdp_url}/json", json={"method": "Network.enable"}) # 导航后,用 Network.getResponseBody 获取已加载资源的原始响应体 - 或改用
requests.Session()复用 Cookie:将 CDP 获取的document.cookie注入 Session 头。
4.4 现象:
lxml解析时报ParserError: Document is empty原因:
get_target_page()返回None或空字符串,常见于目标页面重定向至登录页(如https://admin.example.com返回 302)。解决:
- 在
get_target_page()中增加状态码检查:# 在 navigate 后,调用 Page.getNavigationHistory 获取当前 URL nav_resp = requests.post(f"{cdp_url}/json", json={"method": "Page.getNavigationHistory"}) current_url = nav_resp.json()["result"]["entries"][-1]["url"] if "login" in current_url or "auth" in current_url: raise RuntimeError(f"Redirected to auth page: {current_url}")
4.5 现象:提取的
module_scriptsURL 无法直接下载(返回 403)原因:ES Module 脚本常部署在私有 CDN,需携带
Origin头或 JWT Token。解决:
- 不直接下载,改为记录 URL 并人工验证:
# 在 extract_resources 中,对 module_scripts 添加 warning 标记 resources["module_scripts"].append({ "url": full_url, "type": "module", "warning": "Requires Origin header or auth token" }) - 后续用 Puppeteer 重放该 URL 并捕获响应体(CDP 的
Network.getResponseBody支持此操作)。
5. 进阶技巧:用 Puppeteer 替代 CDP HTTP 接口实现稳定资源捕获
5.1 为什么 Puppeteer 是更可靠的生产方案?
CDP 的
/devtools/page/{id}HTTP 接口虽轻量,但在高并发场景下存在两个硬伤:- 连接泄漏:每个
new标签页需手动关闭,忘记调用Page.close会导致 Chrome 进程堆积; - 事件丢失:HTTP 轮询
loadEventFired有概率错过事件,尤其在低配服务器上。
Puppeteer 作为官方维护的 Node.js 库,封装了完整的生命周期管理,其
page.goto()内置超时、重试、等待网络空闲等策略,更适合长期运行。5.2 Puppeteer 资源捕获脚本:一行命令导出结构化 JSON
以下
crawl.js脚本可直接运行,输出output.json:const puppeteer = require('puppeteer'); async function crawlPage(url) { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // 1. 设置请求拦截,捕获所有 JS/CSS 请求 await page.setRequestInterception(true); const resources = { scripts: [], stylesheets: [], preload: [], modules: [] }; page.on('request', req => { const resourceType = req.resourceType(); const url = req.url(); if (resourceType === 'script') { resources.scripts.push({ url, method: req.method() }); } else if (resourceType === 'stylesheet') { resources.stylesheets.push({ url }); } else if (req.headers()['sec-fetch-dest'] === 'script') { // 捕获 fetch 加载的脚本 resources.scripts.push({ url, method: 'fetch' }); } }); // 2. 导航并等待网络空闲(比 DOMContentLoaded 更准) await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 }); // 3. 提取 HTML(渲染后) const html = await page.content(); // 4. 关闭浏览器 await browser.close(); return { html, resources }; } // 使用 (async () => { const result = await crawlPage('https://example.com'); require('fs').writeFileSync('output.json', JSON.stringify(result, null, 2)); })();执行命令:
npm init -y npm install puppeteer node crawl.js输出
output.json结构示例:{ "html": "<!DOCTYPE html><html>...</html>", "resources": { "scripts": [ { "url": "https://example.com/main.js", "method": "GET" }, { "url": "https://cdn.example.com/lib.js", "method": "fetch" } ], "stylesheets": [ { "url": "https://example.com/style.css" } ], "preload": [], "modules": [] } }5.3 从
output.json到可复用组件的最后一步:静态资源离线化有了结构化资源列表,下一步是下载并建立本地映射。以下 Bash 脚本可批量下载 JS/CSS(需先安装
aria2c):#!/bin/bash # download-resources.sh JSON_FILE="output.json" # 提取所有 script 和 stylesheet URL URLS=$(jq -r '.resources.scripts[].url, .resources.stylesheets[].url' "$JSON_FILE" | sort -u) # 下载到 ./static/ 目录 mkdir -p static aria2c -i <(echo "$URLS") -d static -j 5 --auto-file-renaming=false # 生成 URL 到本地路径的映射表 echo "url,local_path" > mapping.csv while IFS= read -r url; do if [[ -n "$url" ]]; then filename=$(basename "$url" | sed 's/[?#].*$//') echo "$url,static/$filename" fi done <<< "$URLS" >> mapping.csv关键设计点:
aria2c -j 5:5 线程并发下载,比curl快 3 倍以上;sed 's/[?#].*$//':去除 URL 中的查询参数(如?v=1.2.3),避免重复下载;mapping.csv:为后续 Webpack/Vite 配置resolve.alias提供数据源,实现“线上资源→本地 mock”的无缝切换。
我一般会在某高校实验室的 UI 兼容性项目中,用这套流程批量采集 50+ 主流建站平台的表单组件 HTML 结构,再用
diff工具比对<input type="date">的渲染差异。过程中最深的教训是:永远不要相信document.write()的输出——它可能在DOMContentLoaded后异步注入,必须用MutationObserver监听 DOM 变化。希望帮到你。本文还有配套的精品资源,点击获取