用 Chrome DevTools Protocol 驱动浏览器:自动化任务踩坑记录
最近在一台 2 核 1.7G 内存的小服务器上跑定时自动化任务:每天登录站点、填表单、点按钮、核对结果。装 Selenium 或 Playwright 都会被内存劝退(光浏览器常驻就 700MB+),最后直接用 Chrome DevTools Protocol(CDP)驱动一个常驻 Chrome。下面记录的都是真实踩过的坑,代码取自实际跑通的脚本。
一、常驻浏览器 + 远程调试端口
用 systemd 把 Chrome 起成常驻服务,profile 落在磁盘上,登录态、Cookie 都能跨进程保留:
ExecStart=/usr/bin/google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=/root/.hermes/state/chrome-profile \
--no-first-run --no-default-browser-check
好处很直接:不必每次任务重新登录,也不必重新过人机校验。代价是会话级 Cookie 在进程重启后会失效,所以"进程重启导致页面回到未登录"是正常现象,不是信任设备失效——用密码重登即可,信任设备还在的话不会再要短信码。
二、页面不渲染:visibilityState 是 hidden
这是最隐蔽的一个坑。常驻 Chrome 里页面常以 document.visibilityState === 'hidden' 运行,React / Next 这类流式页面会一直不 hydrate,表现是页面空白、按钮全部 disabled,看起来像网站挂了。
一次导航后重设这两个状态就能救回来:
cdp('Emulation.setFocusEmulationEnabled', enabled=True)
cdp('Page.setWebLifecycleState', state='active')
每次导航之后都要重设一次,只在开头设一次不够。
三、跨域 iframe 里的编辑器
很多富文本 / Markdown 编辑器是跨域 iframe(res-hd.hc-cdn.cn/... 之类),父页面拿不到内部 DOM,document.querySelector 一律返回 null,TinyMCE 也不存在。正确做法是用 Target.getTargets 找到 iframe 的 targetId 再 attach:
tid = [t['targetId'] for t in cdp('Target.getTargets')['targetInfos']
if 'HDMDEditorPortal' in t.get('url', '')][0]
sid = cdp('Target.attachToTarget', targetId=tid, flatten=True)['sessionId']
cdp('Runtime.evaluate', expression='setEditorContent(%s)' % json.dumps(md),
session_id=sid)
两个注意点:必须带 flatten=True,否则拿到的是嵌套 session;sessionId 不跨进程调用存活,每次用之前重新 attach。
四、JS click() 不触发 React 事件
页面上大量按钮其实是带 onClick 的 div / span,用 element.click() 经常毫无反应、也不报错。必须走真实坐标点击:
el.scrollIntoView({block: 'center'})
rect = el.getBoundingClientRect()
click_at_xy(rect.x + rect.width/2, rect.y + rect.height/2)
反过来看,这也是个有用的信号:如果点击后完全没有网络请求,大概率是没点中或被遮罩挡了。提交类操作前建议先复核落点:
document.elementFromPoint(x, y).id // 是不是目标按钮
模态遮罩(提示标签重复、不支持手动添加等)会盖在提交按钮上,静默吞掉所有点击,这是最常见的"按钮坏了"假象。
另外要小心隐藏模板元素:querySelectorAll('.status-btn')[0] 可能是一个 0×0 的模板节点,应取 filter(b => b.getBoundingClientRect().width > 0)[0]。
五、填输入框:两种可靠方式
- 普通 input:
js设 value 后必须派发input/change事件,否则框架收不到。 contenteditable或 CodeMirror:聚焦后用 CDP 的Input.insertText整段写入,比逐字符模拟按键稳得多,也不会被输入法的 composition 事件打断。- 密码框显示
********且值不在 DOM 里时,点旁边的复制图标,再建临时 textarea 聚焦后Ctrl+V把值粘出来读。
六、IPC 超时和长等待
默认 IPC 超时通常只有 5 秒,慢站上 Runtime.evaluate 会随机报错。给每个 CDP 调用显式加超时:cdp('Page.navigate', url=..., _response_timeout=40)。
还有两个反直觉的点:
- 在
js()里await一条 fetch 链会等满整条链,超时就整段失败。改成"结尾赋值 + 立即返回",再用单独的调用轮询读结果。 - 在自动化会话里
time.sleep超过约 60 秒,标签页连接会冻结,后续所有 CDP 调用全部 5 秒超时。长等待放到 shell 里,或者拆成多次短调用。
七、内存:标签页要随手关
小内存机器上,站内卡片点击几乎都在新标签页打开,几个标签就能吃掉几百 MB,最后表现是连 1+1 的 evaluate 都超时。处理完就用 Target.closeTarget 关掉,常驻只留 1–2 个。判断当前开着的页面数:
len([t for t in cdp('Target.getTargets')['targetInfos'] if t['type'] == 'page'])
真卡死时 systemctl --user restart hw-chrome 立刻恢复,profile 在磁盘上,登录态不丢。
小结
CDP 的好处是看得见:网络请求、DOM、Cookie、JS 返回值都能直接核验。做自动化时不要相信页面上的文案反馈(“已收藏”、计数 +1 都可能是全局数据),真正的判据是网络请求是否发出、接口返回是否变化、以及结果页/列表里能否查到自己刚提交的记录。
- 点赞
- 收藏
- 关注作者
评论(0)