chrome-headless-shell 实战:从无头浏览器解压到 CDP 自动化测试
简介适用于Windows 64位平台的Chrome无头浏览器压缩包版本133.0.6943.53无需图形界面即可运行面向Web开发者、测试工程师与爬虫开发者适用于自动化测试、页面渲染和数据抓取等场景。压缩包共125个文件以核心可执行程序、pak资源文件、hyb数据、dll运行库、json配置和js脚本为主整体约101.94MB解压后可直接使用共同构成完整的无头浏览器运行环境。已有241人学习下载适合需要快速搭建无头浏览器环境的开发者。借助该工具可在不打开完整窗口的前提下完成网页交互测试、批量采集与性能监控节省系统资源并提升自动化稳定性同时支持最新CSS、HTML与JavaScript标准基于Chrome稳定版构建在性能、安全性和兼容性上均有保障便于开发者在自动化流程中高效复用浏览器能力。1. 从 chrome-headless-shell-win64 这个 zip 看 Chrome 无头化的两条路线chrome-headless-shell-win64-133.0.6943.53.zip 这个文件名里最难读的部分是 chrome-headless-shell。它不是完整 Chrome 的压缩包而是 Chrome 团队为自动化任务单独编译的无头浏览器可执行文件。Chrome 112 之后无头模式分成两条线普通 chrome.exe 内置的新无头模式以及独立编译的 chrome-headless-shell。我在 Windows Server 上做定时截图、页面巡检和 PDF 报表都用后者启动快、不吃界面资源、不加载扩展行为也更贴近旧 headless 语义。这里就从解压、验证版本开始讲到截图、DOM 导出、PDF 和 CDP 自动化最后落在版本钉死与排错上。适合写采集、做 UI 自动化、维护脚本化 Chrome 环境的工程师。2. win64 下解压 chrome-headless-shell 并验证版本的最小命令2.1 解压后看什么目录结构与命名差异官方 Chrome for Testing 下载的压缩包本来叫 chrome-headless-shell-win64.zip版本号在外层路径里你手上这个带 133.0.6943.53 的命名通常是镜像站归档时自己加的或者按路径规则手工改的名。解压后是一个同名目录里面没有完整 Chrome 那套 locales、资源包和默认扩展主体就是 chrome-headless-shell.exe配上少量运行库。这个结构决定了它的体积和启动速度都优于完整浏览器也意味着你没法在里面用 --load-extension 加载扩展。Expand-Archive -Path .\chrome-headless-shell-win64-133.0.6943.53.zip -DestinationPath .\chrome-headless-shell ls .\chrome-headless-shell\chrome-headless-shell-win64\第一行把 zip 解压到指定目录第二行确认目录内容看到 chrome-headless-shell.exe 再继续。在 Windows 上解压 exe 类工具最常见的坑是文件被 Mark of the Web 拦截右键 zip 属性勾选“解除锁定”再重新解压否则后面可能出现不明不白的权限报错。2.2 版本校验不启动浏览器的前置断言$shell .\chrome-headless-shell\chrome-headless-shell-win64\chrome-headless-shell.exe $shell --version输出里应该同时包含 “Chrome Headless Shell” 字样和 133.0.6943.53。--version 不会启动完整浏览器进程只是把可执行文件的版本信息打出来用来验证解压完整性、运行库依赖是否能加载。如果报缺少 DLL 或者版本对不上先别往下走重新解压或换下载源后续所有行为都依赖这个版本与归档名一致。我在脚本里习惯把它做成前置断言版本不匹配直接退出而不是让任务带着错误的浏览器跑完再报错。2.3 第一张截图六个参数各管什么事 $shell --headless --disable-gpu --hide-scrollbars --window-size1280,800 --screenshot$PWD\home.png --virtual-time-budget5000 https://example.com这是 chrome-headless-shell 的最小可复现任务逐项说清楚--headless 在 chrome-headless-shell 上其实是默认语义但保留它方便以后把这组参数直接换到完整 Chrome 的新无头模式上--disable-gpu 关闭 GPU 合成。远程桌面断开、无显示设备的 Windows 容器里GPU 进程经常是报错源头服务器环境默认加上--window-size1280,800 决定视口尺寸。截图尺寸、页面媒体查询、懒加载策略都受它影响宁可设大再裁切--screenshot 接收输出 PNG 路径路径含空格时用引号包起来--virtual-time-budget5000 让页面虚拟时钟快进 5 秒。它与 sleep 的本质区别是setTimeout、requestAnimationFrame 会被加速执行而不是真等 5 秒所以跑完通常比真实等待快得多。执行成功时终端不会打印“截图成功”之类的提示检查方式是看 home.png 是否存在且非空。失败时最常见的两个原因是参数拼写错误以及 https://example.com 在内网出口受限的环境里解析不了。2.4 chrome-headless-shell 高频参数速查表参数作用使用建议--headless无头开关保留便于和完整 Chrome 共用参数--disable-gpu禁用 GPU 合成服务器、容器默认加--no-sandbox关闭渲染沙箱仅服务账户受限时加别随手加--window-sizeW,H视口宽高截图和 PDF 前必设--hide-scrollbars隐藏滚动条截图更干净--virtual-time-budgetMS虚拟时间预算依赖定时器的页面配置--dump-domDOM 输出到 stdout配 budget 抓首屏--print-to-pdfFILE导出 PDF配 --no-pdf-header-footer--user-data-dirDIR用户数据目录并发实例必须独立--remote-debugging-portPORTCDP 监听端口自动化、调试时开启这张表是后面的公共底座。接下来看三类高频任务各自怎么组合这些参数。3. chrome-headless-shell 的三个高频任务DOM 导出、PDF 与整页截图3.1 --dump-dom 抓首屏 DOM预算、编码与重定向 $shell --dump-dom --virtual-time-budget3000 https://example.com | Out-File -Encoding utf8 page.html--dump-dom 把页面渲染完成后的 DOM 序列化到 stdout和 curl 抓 HTML 的区别在于它执行了脚本SPA 里由 fetch 和模板渲染出来的节点只要在预算内完成就会出现在输出里。注意别用 PowerShell 默认的重定向它会把文本编码成 UTF-16抓下来的 HTML 在编辑器里是一堆空字节这里用 Out-File -Encoding utf8 显式指定编码。预算给多少我一般 3000 起步页面有多轮接口请求就往上加。但有一点要记得预算管不到真实网络耗时接口响应慢的时候预算耗尽 DOM 还是空壳这个在第五章给轮询兜底方案。3.2 --print-to-pdf 生成报表页眉页脚与打印背景 $shell --print-to-pdfD:\reports\daily.pdf --no-pdf-header-footer --virtual-time-budget5000 https://example.com--print-to-pdf 走的是打印渲染管线和截图不是一条路径CSS 里的 page 规则、A4 分页、page-break 都会生效。默认生成的 PDF 带浏览器页眉页脚内容包含标题、日期和 URL出正式报告很难看--no-pdf-header-footer 把它们关掉这个参数在 Chrome 120 之后可用。命令行版没有“打印背景”开关深色站点或需要背景色的报告生成的 PDF 背景是白的。两条出路在 CSS 里给根元素加 -webkit-print-color-adjust: exact或者走 CDP 的 Page.printToPDF把 printBackground 设成 true。后者参数更全能同时控制纸型、边距和 scale第四章会看到同一套调用方式。3.3 整页截图窗口高度与 captureBeyondViewport 的取舍默认 --screenshot 只截视口也就是 window-size 那么大。要整页暴力做法是把窗口高度拉大 $shell --hide-scrollbars --window-size1280,20000 --screenshotlong.png https://example.com页面在 20000 像素以内都能截到超出就截断。这个办法的优点是命令简单缺点是高度写死页面内容一变就截断。稳定做法是用 CDP 的 Page.captureScreenshot 加 captureBeyondViewport: true返回的图片以完整内容高度为准。命令行入门用前者生产环境建议用后者。3.4 任务与参数组合对照任务参数组合常见失误首屏截图--screenshot --window-size --hide-scrollbars不设 window-size 拿到 800x600 默认图整页截图拉高 window-size 或 CDP captureBeyondViewport高度写死导致截断DOM 导出--dump-dom --virtual-time-budget不加预算抓到空壳PDF 生成--print-to-pdf --no-pdf-header-footer忘关页眉页脚需要登录态的页面复用固定 --user-data-dir每次新临时目录导致会话丢失4. 用 CDP 把 chrome-headless-shell 接进 Python 自动化4.1 启动常驻实例并确认协议端点 $shell --remote-debugging-port9222 --user-data-dirD:\tmp\ch-profile --disable-gpu about:blank起一个常驻进程之后不再用命令行一次一个任务而是通过 Chrome DevTools Protocol 动态创建页面、执行脚本、截图。参数里 --remote-debugging-port9222 打开 CDP 监听--user-data-dir 指定专用目录避免污染真实用户配置。起好后用浏览器访问 http://127.0.0.1:9222/json/version看到 Browser 字段是 133.0.6943.53 就说明协议层就绪。Windows 10 以上自带 curl也可以直接curl.exe http://127.0.0.1:9222/json/version在命令行确认。4.2 Python 最小 CDP 截图脚本import base64 import json import time import urllib.request import websocket # pip install websocket-client # 通过 HTTP 端点新建一个标签页返回 target 信息 req urllib.request.Request( http://127.0.0.1:9222/json/new?https://example.com, methodPUT, ) with urllib.request.urlopen(req) as resp: target json.loads(resp.read()) ws websocket.create_connection(target[webSocketDebuggerUrl], timeout30) def cdp(method, paramsNone, msg_id1): ws.send(json.dumps({id: msg_id, method: method, params: params or {}})) while True: frame json.loads(ws.recv()) if frame.get(id) msg_id: return frame cdp(Page.enable) time.sleep(3) # 演示用等待生产环境监听 Page.loadEventFired shot cdp(Page.captureScreenshot, {format: png, captureBeyondViewport: True}) with open(cdp_full.png, wb) as f: f.write(base64.b64decode(shot[result][data])) ws.close()逻辑说明/json/new 创建新标签页并返回 JSON里面的 webSocketDebuggerUrl 就是该页面的 CDP 通道WebSocket 上跑的是 JSON-RPC 风格的请求每条消息带自增 id 用来配对响应Page.captureScreenshot 返回 base64 编码的 PNG写文件前要 decode。captureBeyondViewport 设为 true 表示截整页而不是视口。两个容易踩的协议细节/json/new 在新版本要求 PUT 方法用 GET 会拿到 405CDP 响应不保证顺序返回所以 cdp() 里必须按 id 匹配不能简单接收第一条。这个函数骨架可以扩展成通用客户端Runtime.evaluate、Page.printToPDF、Network.setBlockedURLs 都是同一套调用方式。4.3 与 Selenium、Playwright、Puppeteer 的边界在哪chrome-headless-shell 不实现 WebDriver 协议只暴露 CDP。Selenium 的 ChromeDriver 期望后端是完整 Chrome把 binary 指到这个 exe 会在 session 创建阶段失败。想沿用现成框架常见做法是 Playwright 的 connectOverCDP或者 puppeteer-core 指定 executablePathconst puppeteer require(puppeteer-core); const browser await puppeteer.launch({ executablePath: C:\\tools\\chrome-headless-shell-win64\\chrome-headless-shell.exe, headless: shell });说明puppeteer-core 不会自动下载浏览器executablePath 指向你解压出来的 exeheadless: shell 是 Puppeteer 21 里专门配对旧无头壳的取值不要写成 true。用 Playwright 的话更直接实例起好后 chromium.connectOverCDP(http://127.0.0.1:9222) 接管连 executablePath 都不用传。4.4 生产环境启动模板随机端口与 DevToolsActivePort$base D:\tools\chrome-headless-shell-win64 $profile D:\tmp\profile-$PID $base\chrome-headless-shell.exe --headless --disable-gpu --no-first-run --no-default-browser-check --user-data-dir$profile --remote-debugging-port0 about:blank--remote-debugging-port0 让系统分配随机空闲端口端口号写入 $profile\DevToolsActivePort 文件的第一行。CI 并发跑多个实例时不用自己维护端口分配逻辑读这个文件就能拿到各自实例的地址。--no-first-run 和 --no-default-browser-check 去掉首次启动的提示干扰。$PID 确保每批任务的 profile 目录互不相同避免进程间抢同一个目录锁。任务结束把这个目录删掉即可。5. 版本钉死、多实例与三个高频问题的现场处置5.1 为什么 133.0.6943.53 值得写进 CI 清单Chrome 大版本决定了 DOM、CSS 和 CDP 协议的默认行为版本漂移是自动化脚本“昨天还好好的”的最大来源。把 133.0.6943.53 写死在依赖清单里和锁 npm 版本一个道理。启动前的断言写成这样$v $shell --version if ($v -notmatch 133\.0\.6943\.53) { throw chrome-headless-shell 版本漂移: $v }升级策略是显式的先升级 puppeteer-core 或 websocket 客户端再换 shell 版本顺序反了容易踩协议不兼容。5.2 多实例并发与 profile 锁一个常驻实例就是一个进程树加一个 profile 目录。两个进程用同一个 user-data-dir 时后启动的会因 SingletonLock 直接退出。批量任务里每个实例用独立 profile 目录端口交给 --remote-debugging-port0 分配。内存规划我按这个量级起步具体以任务页面复杂度为准机器内存建议并发实例数观察指标8GB4进程树总内存接近 3.5GB 就排队16GB6-8标签页多时单实例可达 800MB-1.2GB32GB10-12先看 CPU 再看内存5.3 三个高频坑及排查顺序第一budget 不等于等待网络。慢接口页面的正确姿势是轮询 DOM 而不是无限开大预算for _ in range(20): r cdp(Runtime.evaluate, {expression: document.querySelectorAll(tr.item).length}) if r[result][result].get(value, 0) 100: break time.sleep(0.5)第二中文字体豆腐块。Windows Server 默认不带微软雅黑时截图全是方框。装完字体要换一个新的 user-data-dir 重启实例字体缓存不会自动刷新。判断方法很直接看 C:\Windows\Fonts 下有没有 msyh.ttc。第三远程桌面断开后的 GPU 报错。断了 RDP 的服务器上 GPU 进程有时起不来表现是启动卡住或 stderr 出现 GPU 相关错误。保持 --disable-gpu不要额外加 --use-gl 之类的参数。改完任何参数都先跑一遍 --version 再跑任务把启动问题和页面问题分开定位。排查顺序固定为先看 stderr再看产物文件是否非空最后怀疑参数组合。日志里出现 panic 或 DLL 加载失败通常不是参数问题是文件没下全。另外记得实例退出前调 Browser.close 而不是直接 kill 进程树profile 里的锁文件才能正常释放这是并发场景里最容易被忽略的收尾动作。本文还有配套的精品资源点击获取