不用 Playwright:120 行原生 WebSocket 直连 CDP 的浏览器自动化实践

举报
yd_231843322 发表于 2026/10/20 10:32:34 2026/10/20
【摘要】 自动化框架守护进程挂掉之后,退回 Chrome DevTools Protocol 协议层:用 Node 22 内置 WebSocket 直连 9333 端口,覆盖导航、取文本、截图、导出 Cookie,并记录无头模式秒退、沙箱拦回环、UA 覆盖触发 WAF、httpOnly Cookie 四个真实坑。

背景:当自动化框架不好用时

做浏览器自动化,第一反应通常是 Playwright、Puppeteer 或各种 agent-browser 封装。它们省事,但有一个共同的前置条件:框架自己的运行时必须是健康的。一旦守护进程卡死、版本与内核不匹配,你会发现所有 API 调用都变成"调用后无响应,五分钟后超时",而底层 Chrome 明明还开着。

最近我就踩了这个坑:一个自动化脚本里 open 操作全部挂起,最后只能杀掉全部进程重来。于是退回到最底层——直接对着 Chrome DevTools Protocol(CDP)说 WebSocket 协议。结果整个驱动脚本不到 120 行,零第三方依赖,反而比框架更稳。

一、CDP 到底是什么

CDP 是 Chrome 暴露的调试协议,它分两层:

  • HTTP 发现层:/json/version 拿浏览器信息与 WebSocket 入口,/json/list 列出所有可调试目标(page、iframe、worker……)。
  • WebSocket 命令层:连上去之后,用 JSON 发命令,按 id 匹配响应,用 method 接收事件。

请求长这样,id 自己递增、用于配对响应:

{"id":1,"method":"Page.navigate","params":{"url":"https://example.com"}}
{"id":2,"method":"Runtime.evaluate","params":{"expression":"document.title","returnByValue":true}}

只要 Chrome 用 --remote-debugging-port=9333 启动,http://127.0.0.1:9333/json/list 就能列出目标。

二、Node.js 原生 WebSocket 就够了

Node 22 内置了全局 WebSocket,所以连依赖都不用装:

const targets = await (await fetch('http://127.0.0.1:9333/json/list')).json();
const page = targets.find(t => t.type === 'page');
const ws = new WebSocket(page.webSocketDebuggerUrl);

let seq = 0;
const waiters = new Map();

ws.addEventListener('message', ev => {
  const msg = JSON.parse(ev.data);
  if (msg.id && waiters.has(msg.id)) {
    waiters.get(msg.id)(msg);
    waiters.delete(msg.id);
  }
});

function send(method, params = {}) {
  const id = ++seq;
  return new Promise((resolve, reject) => {
    waiters.set(id, m => (m.error ? reject(new Error(m.error.message)) : resolve(m.result)));
    ws.send(JSON.stringify({ id, method, params }));
  });
}

基于这一层,几个高频能力一行就出来了:

await send('Page.navigate', { url });
const { result } = await send('Runtime.evaluate',
  { expression: 'document.body.innerText', returnByValue: true });
const shot = await send('Page.captureScreenshot', { format: 'png' });

三、四个真实踩坑

1. 无头模式经常"打开即退出"

用 --headless 启动时,某些站点或内核组合下进程会瞬间退出,连调试端口都来不及监听。判断方法很简单:

netstat -ano | findstr :9333

没有 LISTENING 就是没起来。改成有头模式(去掉 --headless)后立刻稳定,代价是必须能开图形界面。

2. curl 能通,Node 连不上:沙箱拦回环

这个坑很迷惑:curl http://127.0.0.1:9333/json/list 返回正常 JSON,但同一个脚本里 WebSocket 直接 ECONNREFUSED。原因是执行环境把本地回环地址隔离了——curl 走的是宿主网络,而脚本进程被限制。

解决办法不是改代码,而是让脚本在不受限的权限下运行,或者干脆把 CDP 操作放在宿主机侧的脚本里执行。别在协议层浪费时间排查,先确认网络命名空间。

3. UA 与 WAF:改了 UA 反而 403

很多站点把登录态和 UA 绑定。用 CDP 接管一个已登录的浏览器后,如果你顺手 Network.setUserAgentOverride 把 HeadlessChrome 字样去掉,有些带 WAF 的接口会直接返回 403 或"未登录"。

结论:不要自作聪明改 UA。要么原样透传,要么把原浏览器的 UA 完整取出来再设回去:

const { result } = await send('Runtime.evaluate',
  { expression: 'navigator.userAgent', returnByValue: true });
await send('Network.setUserAgentOverride', { userAgent: result.value });

4. 拿不到 httpOnly Cookie

document.cookie 读不到 httpOnly 的会话票据,但 CDP 能:

const { cookies } = await send('Storage.getCookies', {});

返回的是全量 Cookie,含 httpOnly、domain、expiry。做"导出登录态给脚本复用"时这是最干净的路径,比抓包复制干净得多。

四、什么时候该用它

  • 框架坏了但仍需自动化:CDP 是最后的稳定层。
  • 需要复用已登录的浏览器:直接接管用户态,无需再走登录流程。
  • 需要精确控制:cookie 导出、UA 覆盖、请求拦截,都是原生能力。

代价也很明确:没有自动等待、没有选择器引擎、没有重试逻辑,document.querySelector 之后的一切都要自己写。所以我的实践是——框架能跑就用框架,跑不起来再用裸 CDP 兜底,把两者当成两层,而不是二选一。

小结

CDP 的门槛其实只有一个:知道 /json/list 和 webSocketDebuggerUrl 这两个东西。跨过去之后,120 行代码就能覆盖导航、取文本、截图、导出 Cookie 四件事,而且在框架挂掉的时候依然可用。自动化链路里留一层"协议层兜底",比反复重启守护进程划算得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。