CDP 附着已登录标签页:复用登录态做服务端抓取
凌晨两点,导报表的脚本挂了,日志最后一行是 Error: connect ECONNREFUSED 127.0.0.1:9222。这个脚本本身不登录——后台要短信验证码,只能人工每天先登一次,脚本负责复用那个已经登录的标签页把数据刮下来。上一版同事用 puppeteer.launch() 每次起新浏览器,跑出来全是登录页,问题就出在这里。
先想清楚为什么不能新建上下文
Target.createBrowserContext 建出来的是隔离上下文,cookie、localStorage、IndexedDB 全是空的。有人试过把 cookie 复制过去,仍然 401 跳登录——因为不少后台把 token 放在 sessionStorage 里,而 sessionStorage 不跨标签页共享,复制 cookie 没用。就算用 Target.createTarget 落在默认上下文里能共享 cookie,也会打断正在用浏览器的人,且页面内存态的登录状态照样拿不到。
所以思路反过来:浏览器由人启动并保持登录,脚本只做附着。启动命令固定成 chromium --remote-debugging-port=9222 --user-data-dir=/data/chrome-profile。注意新版本 Chromium 对默认 profile 目录开启远程调试有限制,必须显式给 --user-data-dir,否则端口根本不监听。
拿浏览器级 WebSocket,再 attach 目标页
先请求 GET http://127.0.0.1:9222/json/version,取返回里的 webSocketDebuggerUrl,这是浏览器级端点。不要用 /json/list 里每个页面的 webSocketDebuggerUrl,它绑死单个标签页,页面一刷新连接就断,而且没法同时挂多个页面。
连上之后两步:Target.getTargets 里筛 type === "page" 且 url 匹配的那一条,拿到 targetId;再发 Target.attachToTarget {targetId, flatten: true},从返回的 result.sessionId 拿到会话号。
flatten 是必须开的。不开 flatten 时所有子会话共用一条消息流,id 会撞,得自己维护多路复用表;开了之后每条消息自带 sessionId 字段,形如 {"id":7,"sessionId":"6F3A...","method":"Runtime.evaluate","params":{}},一个连接同时挂五六个标签页也不会串号。之后所有发给页面的命令,都要在参数里带上这个 sessionId。
Runtime.evaluate 的返回值别直接信
页面里的取数基本是异步的:点导出、等接口回、再从 DOM 刮。直接 Runtime.evaluate 一个返回 Promise 的表达式,不加 awaitPromise 只会拿到 {"result":{"type":"object","subtype":"promise"}};加上 awaitPromise 又容易把这条连接卡死到超时。可靠做法是两段式——先注入,让页面把结果存到一个全局变量,过一会儿再来读。
第一段只负责点火,不等结果:
send('Runtime.evaluate', { expression: "(async () => { const r = await fetch('/api/report?day=2026-10-24', {credentials:'include'}); window.__R = await r.json(); })();" }, sessionId)
第二段轮询读回,500ms 一次,最多 20 次:
const {result} = await send('Runtime.evaluate', { expression: 'window.__R ? JSON.stringify(window.__R) : ""', returnByValue: true }, sessionId)
两个细节必须写对。一是 returnByValue: true,不开的话拿到的是 RemoteObject 引用,还得再走一次 Runtime.getProperties,白白多一跳。二是先用 JSON.stringify 序列化,直接按值返回对象遇到循环引用会直接报错,转成字符串最稳。如果这段流程中间有整页跳转,window 会被销毁,window.__R 也就没了——改用 sessionStorage.setItem('__R', ...),同标签页同源下它能活过跳转。
标签页被关掉之后怎么兜
人工顺手把标签页关了,或者页面崩溃,浏览器级会话会收到 Target.targetDestroyed 事件;此时再往老 sessionId 发命令,回的是 Session with given id not found.。麻烦在于挂在该会话上等待回包的 Promise 会永远不 resolve,整个任务就静默卡住。
处理办法:每个 sessionId 维护自己的 pending map,收到 Target.targetDestroyed 或者 WebSocket 关闭时,把这个 session 下所有 pending 全部 reject,错误信息里带上原始 method 名,日志里一眼就能看出卡在哪个调用上。然后重新 Target.getTargets,按 url 前缀找新 targetId 再 attach 一次;找不到就直接抛给上层告警,不要静默重试——登录态掉了的时候,重试一万次也不会成功,得有人来扫码。
挂多个标签页时的回收
一次导出任务经常要挂好几个页面:列表页取 ID、详情页逐个取数。每 attach 一次,浏览器端就留一份会话状态,不 detach 会一直累积,跑一整晚攒出几百个,浏览器内存和命令延迟一起上去。
- 用完立刻
Target.detachFromTarget {sessionId},放在 finally 里,不要塞在 try 末尾。 - detach 后同步从本地 sessions map 删除,并
clearTimeout掉该会话的轮询定时器。 - 不需要发现事件时,
Target.setDiscoverTargets {discover:false},否则每开一个标签页都推一遍Target.targetCreated,日志直接刷屏。 - 任务收尾
ws.close(),Chromium 会为每条调试连接保留一个客户端对象,别留着过夜。
验证是否漏了回收:任务跑完后执行 curl -s http://127.0.0.1:9222/json/list | grep -c webSocketDebuggerUrl,数量应该回落到开工前的水平;再在浏览器级会话上发一次 Target.getTargets,看返回的 target 列表有没有异常增长。这两个数稳住,夜里就不用起来了。
- 点赞
- 收藏
- 关注作者
评论(0)