Chrome CDP+DOM解析实现合规前端资产提取 简介这是一份面向前端开发者、爬虫初学者及网页分析爱好者的在线扒站源码工具包聚焦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/xhtmlxml或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 ProtocolCDPCDP 是 Chrome 浏览器暴露的底层调试协议它允许外部程序控制浏览器行为其优势在于完全模拟真实用户环境自动携带完整 UA、Cookie、TLS 指纹、Sec-Fetch-*头支持等待 DOM 加载完成通过Page.loadEventFired事件确保 HTML 解析完毕可禁用图片/字体加载大幅降低带宽消耗加速分析流程能捕获所有网络请求精准提取script src...和link relstylesheet 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-port9222 --headlessnew --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, jsonnavigate_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, jsonhtml_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, jsonserialize_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 树根节点 IDdepth: -1表示获取全部层级第 5 步用DOM.getOuterHTML提取完整 HTML 字符串此结果等同于在 DevTools 中右键“Copy outerHTML”的效果。关键参数说明--headlessnew启用新版无头模式兼容现代 CSS Grid/Flexbox 渲染--disable-gpu避免 Linux 服务器无显卡环境报错--no-sandbox仅限开发环境使用生产环境应配置 proper sandbox--remote-debugging-port9222必须显式指定端口否则默认随机分配脚本无法固定连接。3. 如何从 HTML 中精准提取 JS/CSS 资源链接——避开正则的 DOM 解析法3.1 为什么不用re.findall(rscript.*?src(.*?), html)正则解析 HTML 是前端开发界的“玄学禁区”script srca.js async/script与scriptconsole.log(x)/script混在同一行时正则易错匹配link relpreload asscript hrefb.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 relstylesheet 和 style - preload_scripts: link relpreload asscript - module_scripts: script typemodule 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[relstylesheet]): 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[relpreload][asscript]): 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[typemodule]): 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, indent2, ensure_asciiFalse))参数说明与工程取舍urljoin(base_url, src)处理相对路径如script src/js/main.js→https://example.com/js/main.jstext_content()[:200]内联脚本只取前 200 字符避免因广告 JS 过长导致内存溢出xpath(//link[relpreload][asscript])精准匹配预加载脚本这是 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字段是否存在空值标记未启用 SRISubresource Integrity的脚本。4. 常见问题排查五个让新手翻车的真实场景与解法4.1 现象Chrome 启动后http://127.0.0.1:9222/json返回空数组原因Chrome 未正确启用远程调试或端口被占用。常见误操作包括在已运行的 Chrome 实例上加参数需完全关闭所有 Chrome 进程Linux 环境下未加--no-sandbox但生产环境严禁此参数Windows 防火墙阻止 9222 端口。解决彻底关闭 ChromeMac 上killall -9 Google\ ChromeWindows 上任务管理器结束所有chrome.exe启动命令改为chrome --remote-debugging-port9222 --headlessnew --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, jsonwait_payload)或更简单在DOM.getDocument前加time.sleep(2)仅限调试正式脚本需用Runtime.evaluate。4.3 现象extract_resources()提取的srcURL 404原因目标站点对资源请求做了 Referer 校验或 Token 签名如https://api.example.com/static/js/main.js?t123456789中的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(fRedirected 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.jsonconst 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 htmlhtml.../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_FILEoutput.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-renamingfalse # 生成 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 55 线程并发下载比curl快 3 倍以上sed s/[?#].*$//去除 URL 中的查询参数如?v1.2.3避免重复下载mapping.csv为后续 Webpack/Vite 配置resolve.alias提供数据源实现“线上资源→本地 mock”的无缝切换。我一般会在某高校实验室的 UI 兼容性项目中用这套流程批量采集 50 主流建站平台的表单组件 HTML 结构再用diff工具比对input typedate的渲染差异。过程中最深的教训是永远不要相信document.write()的输出——它可能在DOMContentLoaded后异步注入必须用MutationObserver监听 DOM 变化。希望帮到你。本文还有配套的精品资源点击获取