用 CDP 直接驱动 Chrome:让浏览器自动化跑得更稳

举报
yd_230653297 发表于 2026/11/08 02:11:17 2026/11/08
【摘要】 页面每天都在变,选择器天天失效。把驱动层从框架换成原生 CDP 之后,自动化脚本的维护成本明显下降。本文梳理连接方式、登录态处理、跨域 iframe 穿透和稳定性设计这几个最容易踩坑的环节。

浏览器自动化做了几年,最深的体会是:真正难的不是「点一下按钮」,而是让这件事在半年后还能跑起来。页面改版、登录态过期、弹窗遮挡,任何一个都能让脚本停摆。最近把手上的几套自动化从框架层挪到了 Chrome DevTools Protocol(CDP)直连,维护成本降了不少,这里把关键点整理一下。

一、为什么从框架层下沉到协议层

先说结论:框架适合写业务流程,协议适合写「不可靠环境里的可靠动作」。

  • 框架版本和浏览器版本强绑定,升级一次浏览器,依赖链就断一次。
  • 很多框架把「等待就绪」抽象成固定 sleep,脚本一跑就是几十分钟空转。
  • 出问题时框架只给你一句 timeout,看不到网络请求、看不到 DOM 快照,排障全靠猜。

CDP 的好处是它把浏览器拆成了可观测的原始指令:导航、求值、输入事件、网络拦截、Cookie 读写、帧树遍历,全部是显式调用。脚本里每一步做了什么,日志里一目了然。

二、连接方式的取舍

CDP 有三种常见连接姿势,选错了会很痛苦:

1. 远程调试端口

启动时带上 `--remote-debugging-port=9222`,之后用 HTTP 接口列标签页,再连 WebSocket。

import json, urllib.request, websockets

def targets():
    with urllib.request.urlopen("http://127.0.0.1:9222/json", timeout=10) as r:
        return [t for t in json.loads(r.read()) if t["type"] == "page"]

async def connect(url):
    return await websockets.connect(url[0]["webSocketDebuggerUrl"],
                                    max_size=80 * 1024 * 1024)

优点是跨进程、可持久化、人机可以共用同一个浏览器;缺点是端口暴露在生产环境需要额外防护。

2. 调试管道

`--remote-debugging-pipe` 走标准输入输出,不占端口,更安全。但它要求浏览器由你的进程启动,进程一退浏览器就没了。这种模式下如果要换成端口方式,重启前一定先导出 Cookie,否则登录态跟着临时 profile 一起被删。

3. 云侧调试端点

容器里的无头浏览器通常提供一个签名过的 WebSocket 地址。注意这类地址有有效期,长任务要么保持长连接,要么在过期前刷新。

三、登录态:别指望抠 Cookie 直调接口

这是踩过最多次的坑。很多人第一反应是把浏览器里的 Cookie 全导出来,塞进 requests 直接打接口。结果一堆 403。

原因通常不是缺 Cookie,而是服务端把会话绑在了浏览器上下文上:指纹、TLS 会话、甚至前端注入的临时令牌。正确的做法是让请求仍然发生在页面里:

await page.evaluate(async (csrf) => {
  const r = await fetch("/rest/api/list", {
    credentials: "include",
    headers: { "csrf": csrf }
  });
  return r.status;
}, token);

至于那个 csrf,别从 `document.cookie` 里读。真实项目里前端经常在页面加载时轮换它,cookie 里的是旧值。更可靠的办法是注入钩子,从页面自己发出的请求头里取最后一个值:

const of = window.fetch;
window.fetch = function (input, init) {
  const h = new Headers((init && init.headers) || {});
  if (h.get("csrf")) window.__hits.push(h.get("csrf"));
  return of.apply(this, arguments);
};

四、跨域 iframe 怎么穿透

父页面 `contentDocument` 是 null 的时候,不要绕着用坐标硬点,那是最后手段。CDP 有正经解法:先取帧树,再在目标帧里开一个隔离世界。

tree = await send("Page.getFrameTree")
frame = next(f for f in walk(tree["frameTree"])
             if "workspace.example.com" in f["frame"]["url"])
ctx = await send("Page.createIsolatedWorld",
                 {"frameId": frame["frame"]["id"],
                  "worldName": "probe", "grantUniveralAccess": True})
await send("Runtime.evaluate",
           {"expression": "document.title", "contextId": ctx["executionContextId"]})

隔离世界里 DOM 是共享的,变量不共享。读文本、改输入值、触发 click 都可以;只有键盘鼠标这类真实事件必须在父页面按视口坐标发。这个分工记住就不会乱。

五、稳定性设计的四个习惯

  • **每个动作都可回读**。点完按钮不要睡三秒了事,去读它该变成什么状态。
  • **先判据,再动作**。登录过没过期,唯一可信的判据是业务接口返回 200,页面右上角显示用户名不等于登录有效——那可能只是 HTML 缓存。
  • **临时标签用完就关**。长期跑下来几百个僵尸标签会吃满内存,CDP 一句 close 就清掉。
  • **失败要留证据**。截图、DOM 片段、最近几条网络请求,一起落盘。没有证据的失败等于白跑。

六、什么时候不该用 CDP

不是所有场景都值得下沉。如果目标站点有稳定的开放 API,直接调 API 又快又省;如果只需要一次性的数据抓取,一个简单的请求库加解析就够了。CDP 的价值在于「必须由真实浏览器完成、且要长期稳定运行」的场景,比如流程审批、后台配置、需要登录态的定期巡检。

选型时问自己一个问题:这个流程,未来三个月会不会因为页面改版而断?会,就用协议层加回读断言;不会,别过度设计。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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