用 CDP 直接驱动 Chrome:让浏览器自动化跑得更稳
浏览器自动化做了几年,最深的体会是:真正难的不是「点一下按钮」,而是让这件事在半年后还能跑起来。页面改版、登录态过期、弹窗遮挡,任何一个都能让脚本停摆。最近把手上的几套自动化从框架层挪到了 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 的价值在于「必须由真实浏览器完成、且要长期稳定运行」的场景,比如流程审批、后台配置、需要登录态的定期巡检。
选型时问自己一个问题:这个流程,未来三个月会不会因为页面改版而断?会,就用协议层加回读断言;不会,别过度设计。
- 点赞
- 收藏
- 关注作者
评论(0)