用 CDP 驱动常驻 Chrome 做网页自动化的六个真实坑
背景
我在一台 2 核 1.7G 的小服务器上长期跑定时任务,任务内容包含若干需要 JS 渲染、需要登录态、还需要富文本编辑的网页操作。反复试错后,最终的稳定形态是:一个常驻的 Chrome 进程 + CDP(Chrome DevTools Protocol)直连,而不是每个任务临时起一个 headless 实例。原因很直接:临时实例的 Profile 是新的,登录态、信任设备、Cookie 全部不保留,每一步都要重新过验证。
常驻 Chrome 用 systemd 管起来:
ExecStart=/usr/bin/chrome \
--remote-debugging-port=9222 \
--user-data-dir=/root/.hermes/state/chrome-profile \
--no-sandbox --headless=new
之后所有脚本通过 127.0.0.1:9222 连进去操作。下面六个坑都是在这套形态下真踩出来的,每一条都给出可复现的判断代码,而不是"试试看"。
坑一:页面一片空白,什么元素都点不到
现象:页面 title 正常、DOM 里有节点,但内容是空的,按钮永远 disabled,React/Vue 的点击处理器完全不响应。
根因不是渲染失败,是 document.visibilityState === 'hidden'。headless=new 默认不把页面当可见,前端框架据此挂起了流式渲染/hydrate。先验证:
document.visibilityState // 'hidden' 就是它
解法两条 CDP 命令,且每次导航之后都要重发一次(只发一次会在下一次跳转失效):
cdp('Emulation.setFocusEmulationEnabled', enabled=True)
cdp('Page.setWebLifecycleState', state='active')
坑二:富文本编辑器填不进内容
TinyMCE 一类编辑器不是 input/textarea,fill_input 对它无效,直接改 innerHTML 也不会触发编辑器的计数器和脏检查。正确做法走编辑器自身的 API:
tinymce.activeEditor.setContent('<h2>标题</h2><p>正文</p>')
注意两点:一是 HTML 里的 < 必须写成实体,否则会被当成标签提前闭合;二是设完之后要确认 UI 计数器有变化,计数器没动通常意味着没真正写进去。
坑三:复选框——最贵的静默卡点
这是浪费时间最多的一类:必勾的"我已阅读并同意"复选框没勾时,提交按钮既不带 disabled 之外的提示,点击也完全没有任何反应、没有报错、不发一个网络请求。页面看着一切正常,就是提交不了。
两条经验:
1)触发方式。页面上如果本身就有 jQuery,用它触发比算坐标点鼠标稳,因为下拉面板常常正好盖在复选框上:
jQuery('#agreementBox').trigger('click')
2)提交前必复核。切换表单类型、选标签这些动作会让它悄悄重置:
document.getElementById('agreementBox').className === 'checked'
坑四:看着像链接,其实是 div
很多站点的"去完成""立即体验"在 DOM 里既不是 <a> 也没有 href,而是挂了 click 监听的 div/span。这时 el.click() 常常不触发(框架的合成事件监听在别处),必须走真实鼠标坐标。而且它会新开标签页,goto_url 仍打在旧 tab 上。
处理顺序:先把元素挪进视口,再按 rect 取中心点点击:
e.scrollIntoView({block:'center'})
const b = e.getBoundingClientRect()
// 用 b.x + b.width/2, b.y + b.height/2 点击
cdp('Target.getTargets') // 再从返回列表里找新开的 tab
坑五:看不见的模态遮罩
某些交互(重复选标签、表单校验提示)会弹出一层透明/半透明的遮罩 .modal-cover.show。它不挡眼睛,但会吃掉之后所有的点击。同样表现为"按钮点了完全没反应"。
治它的习惯是:点击之前先问一句这一点到底是谁。
document.elementFromPoint(x, y).id // 不是 '#submit-btn' 就先清遮罩
document.querySelectorAll('.developer-modal.modal-cover.show').length === 0
坑六:会话、弹窗与超时
三个高频小问题打包处理:
· 原生 confirm/alert 会阻塞后续所有命令,用 cdp('Page.handleJavaScriptDialog', accept=True) 放行。
· 密码、验证码这类字段应该交给本机凭证库填充,不要进脚本和日志;凭证库通常只认默认浏览器会话,换 session 会报 "Could not determine the current page origin"。
· 默认 IPC 超时只有几秒,慢站点(尤其是带向导式的控制台页)必须显式放大,例如 cdp('Page.navigate', url=..., _response_timeout=40),否则你会把"超时"误判成"页面坏了"。
小结:一张点击前的自检清单
把上面几条收敛成流程,自动化脚本的失败率会明显下降:
1. 每次导航后重设 focus + lifecycle state;
2. 目标元素 scrollIntoView 后再取坐标;
3. elementFromPoint 复核落点是不是目标按钮;
4. 提交前复核必需复选框的 class;
5. 富文本用编辑器 API,不用 fill_input;
6. 慢操作放大超时,原生弹窗记得放行。
这些都不是"玄学重试",每一步都有对应的可观测信号——先找到信号,再动手,比反复重跑脚本有效得多。
- 点赞
- 收藏
- 关注作者
评论(0)