文章目录
-
- 概述
- [工具背景:agent-browser 是什么](#工具背景:agent-browser 是什么)
- 踩坑一:自带浏览器是全新未登录会话,直接撞风控
- [踩坑二:Chrome 136+ 拒绝对"默认 profile"开远程调试端口](#踩坑二:Chrome 136+ 拒绝对"默认 profile"开远程调试端口)
- [踩坑三:复制 profile 想"偷渡"登录态,被 App-Bound 加密拦住](#踩坑三:复制 profile 想"偷渡"登录态,被 App-Bound 加密拦住)
- [踩坑四:两个长得一样的 Chrome 窗口,扫错了窗口](#踩坑四:两个长得一样的 Chrome 窗口,扫错了窗口)
- [踩坑五:登录了,CDP 自动化导航文章页还是 40362](#踩坑五:登录了,CDP 自动化导航文章页还是 40362)
- [关键转折:导出 Cookie,改用 requests 直连](#关键转折:导出 Cookie,改用 requests 直连)
-
- [踩坑六:state save 抓不到 httpOnly Cookie,改用 CDP Storage.getCookies](#踩坑六:state save 抓不到 httpOnly Cookie,改用 CDP Storage.getCookies)
- [踩坑七:系统代理导致"软封"------200 却没有正文](#踩坑七:系统代理导致"软封"——200 却没有正文)
- [踩坑八:成功判据写错------文章正文藏在 js-initialData](#踩坑八:成功判据写错——文章正文藏在 js-initialData)
- [防 ban 节流策略](#防 ban 节流策略)
- 两个工程性小坑
- 总结
概述
本文复盘一次真实任务:批量下载某乎专栏「InsideUE」作者大钊的 39 篇文章并整理成学习资料。任务看似简单------"把列表里的文章都下载到本地"------但某乎对自动化访问有相当强的风控,整个过程踩了近十个坑:从 agent-browser 自带浏览器未登录、Chrome 新版禁止对默认 profile 开调试端口、App-Bound 加密导致复制 profile 带不出登录态,到 CDP 自动化指纹被识别、系统代理触发"软封"、httpOnly Cookie 抓不到、压缩解码偶发失败......
最终的可用方案出人意料地"返璞归真":用 agent-browser 通过 CDP 接管一个真实登录的 Chrome,从中导出完整 Cookie(含 httpOnly 的登录票据),再用最朴素的 requests 直连抓取。本文把每个坑的现象、根因和解法都记录下来,给做浏览器自动化 / 爬虫的人省点时间。
工具背景:agent-browser 是什么
agent-browser 是一个面向 AI Agent 的浏览器自动化 CLI,基于 Chrome DevTools Protocol(CDP),不依赖 Playwright/Puppeteer。它的核心交互模型是"快照 + 元素引用":
bash
agent-browser open <url> # 打开页面
agent-browser snapshot -i # 抓取可交互元素(@e1、@e2 ... 引用)
agent-browser click @e3 # 对引用执行动作
agent-browser get text @e1 # 读取内容
agent-browser eval "<js>" # 执行任意 JS
agent-browser connect <port> # 通过 CDP 接管已有浏览器
它既能启动自带的 Chromium,也能通过 connect <port> 接管一个开了远程调试端口的浏览器。后面会看到,这个 connect 能力是整个方案的关键。
踩坑一:自带浏览器是全新未登录会话,直接撞风控
第一反应是直接用 agent-browser 打开文章页:
bash
agent-browser open https://zhuanlan.xxxhu.com/p/22813908
agent-browser get count ".RichText" # 返回 0
截图一看,页面是某乎的风控拦截 JSON:
json
{"error":{"message":"您当前请求存在异常,暂时限制本次访问。如有疑问,您可以通过手机摇一摇或登录后私信某乎小管家反馈。","code":40362}}
根因 :agent-browser 启动的是它自带的、独立 profile 的 Chromium(路径在 ~/.agent-browser/browsers/chrome-xxx/),这是一个全新、未登录、且容易被识别为自动化的会话。某乎对未登录 + 自动化特征的请求直接返回 40362。
教训:自动化浏览器 ≠ 你日常登录的浏览器,二者 profile、Cookie、指纹完全独立。
踩坑二:Chrome 136+ 拒绝对"默认 profile"开远程调试端口
既然自带浏览器没登录,那就接管已登录的真实 Chrome 吧。agent-browser connect 9222 需要目标 Chrome 以 --remote-debugging-port 启动。于是尝试用默认 profile 启动:
powershell
Start-Process "C:\Program Files\Google\Chrome\Application\chrome.exe" `
-ArgumentList '--remote-debugging-port=9222','--restore-last-session'
# 结果:http://localhost:9222/json/version 连不上
CDP 端点根本起不来。
根因 :出于安全考虑,Chrome 136+ 会在 --user-data-dir 指向"默认用户数据目录"时,忽略 --remote-debugging-port。也就是说,你没法对日常登录的默认 profile 直接开调试端口------这是 Google 官方堵死的,目的是防止恶意程序静默接管你登录态浏览器。
绕过办法只有一个方向:使用非默认目录的 profile。
踩坑三:复制 profile 想"偷渡"登录态,被 App-Bound 加密拦住
顺着"非默认目录"的思路,自然想到:把已登录的 profile 复制一份到新目录,再用调试端口启动这个副本,不就既有登录态、又能开调试端口了吗?
powershell
# 复制 Local State(含 Cookie 加密密钥)+ Default 目录(排除缓存)
robocopy "$src" "$dst" "Local State" /NJH /NJS
robocopy "$src\Default\Network" "$dst\Default\Network" "Cookies" /E
这里还遇到一个小插曲:Chrome 运行时 Cookies 这个 SQLite 文件被独占锁定 ,普通复制和 robocopy /B(备份模式)都拿不到,必须先完全关闭 Chrome(注意"关窗口 ≠ 退进程",后台还有一堆 chrome.exe,要确认进程全退)。
关掉 Chrome、复制成功、用副本目录 + 调试端口启动:
powershell
Start-Process $chrome -ArgumentList `
'--remote-debugging-port=9222', `
'--user-data-dir="C:\chrome-debug-profile"', `
'--no-first-run','https://www.xxxhu.com/signin'
# CDP OK: Chrome/149.x ------ 调试端口起来了!
可一打开某乎首页,仍然是未登录状态 (显示登录二维码,.SignFlow 元素存在)。Cookie 文件明明复制过来了,为什么没登录?
根因 :Chrome 127+ 引入了 App-Bound Encryption(ABE) 。Cookie 的加密密钥经过应用绑定服务包装,与原始安装/环境强绑定。当 profile 被复制到另一个 user-data-dir 路径 后,ABE 拒绝解密这些 Cookie------于是关键的登录票据(z_c0 等)解不出来,等于没登录。
教训:复制 profile 目录已经无法"搬运"现代 Chrome 的登录态了,ABE 就是专门防这个的。
踩坑四:两个长得一样的 Chrome 窗口,扫错了窗口
ABE 既然让复制失败,那就在这个副本 profile 的 Chrome 里重新扫码登录一次------它是真实 Chrome(不是被标记的 Chromium),重新登录的 Cookie 是原生正确加密的。
但这里又踩了个"人因坑":屏幕上同时存在两个外观完全一样的 Chrome 窗口(一个是之前正常 Chrome,一个是ai控制的调试副本)。扫码扫到了正常 Chrome,而不是受控的那个,导致验证登录态时仍然显示未登录。
解法:用命令行精确区分并关掉"非调试"窗口,只留下受控窗口:
powershell
Get-CimInstance Win32_Process -Filter "name='chrome.exe'" | Where-Object {
$_.ExecutablePath -like 'C:\Program Files\Google\Chrome*' -and
$_.CommandLine -notlike '*chrome-debug-profile*'
} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
CommandLine 里是否含 chrome-debug-profile 是区分两类窗口的可靠依据。清场后让对着唯一的窗口扫码,登录成功。
验证登录态时还有个小细节:agent-browser 接管的可能不是你看的那个标签。直接查 CDP 的标签列表更准:
powershell
(Invoke-RestMethod "http://localhost:9222/json/list") |
Where-Object { $_.type -eq 'page' } | Select-Object title, url
# title: ** - 某乎 url: https://www.xxxhu.com/people/xxxx ← 已登录用户主页
踩坑五:登录了,CDP 自动化导航文章页还是 40362
满怀期待地用登录后的受控浏览器导航文章页:
bash
agent-browser open "https://zhuanlan.xxxhu.com/p/22813908"
agent-browser get text "body"
# 还是 {"code":40362} !
诡异的是:某乎首页、个人主页能正常打开,唯独文章页(SSR)被拦 。换没访问过的文章、加 Referer 头都没用。
根因 :文章页的服务端渲染有更严格的反自动化检测。即便登录,CDP 接管的浏览器存在可被识别的自动化指纹(Runtime 域被 enable、调试器附加等),文章页这条链路会拦,而首页相对宽松。叠加我们前面反复测试已经让这个浏览器/IP 被"临时限制",于是全站性地撞墙。
这说明:靠 CDP 直接驱动浏览器渲染文章页,对某乎这种强风控站点不可靠。
关键转折:导出 Cookie,改用 requests 直连
既然浏览器渲染这条路被自动化指纹卡死,那就换思路:只用浏览器拿登录态(Cookie),抓取交给最朴素、最无指纹的 requests。
踩坑六:state save 抓不到 httpOnly Cookie,改用 CDP Storage.getCookies
先试了 agent-browser state save,导出的 14 个 Cookie 里没有 z_c0(某乎登录票据,是 httpOnly)。
改用 CDP 的 Storage.getCookies 直接从浏览器拿全量 Cookie(含 httpOnly)。这里又一个坑:Chrome 的 CDP WebSocket 有 Origin 校验 ,Python 的 websocket-client 默认带 Origin 头会被拒:
bash
Handshake status 403 Forbidden ... Use --remote-allow-origins=* to allow
解法是建连时 suppress_origin=True 跳过 Origin 头:
python
import json, urllib.request, websocket
v = json.load(urllib.request.urlopen('http://localhost:9222/json/version'))
ws = websocket.create_connection(v['webSocketDebuggerUrl'],
suppress_origin=True, max_size=None)
ws.send(json.dumps({'id': 1, 'method': 'Storage.getCookies'}))
# 读取 id==1 的响应
cookies = resp['result']['cookies'] # 44 个,含 z_c0 / __zse_ck / d_c0
拿到全量 44 个某乎 Cookie,包含 z_c0(登录票据)+ __zse_ck / d_c0(反爬票据),存盘备用。
踩坑七:系统代理导致"软封"------200 却没有正文
带着完整 Cookie 用 requests 抓,第一次手测竟然成功(200,17 万字符,有正文)。但批量跑起来,每篇都返回 200/无正文 ------状态码 200,却抓不到文章内容;紧接着诊断请求直接代理超时。
排查发现环境里设了系统代理:
text
HTTP_PROXY=http://192.168.250.2500:3100
HTTPS_PROXY=http://192.168.250.2500:3100
requests 默认 trust_env=True 会读取系统代理。走代理的请求被某乎判定为异常 IP,返回 200 但抽掉正文(软封),代理本身还不稳定会超时。第一次手测之所以成功,很可能是当时直连或代理状态不同。
解法:强制直连,忽略系统代理:
python
S = requests.Session()
S.trust_env = False # 关键:不读系统代理,直连
for k, v in cookies.items():
S.cookies.set(k, v, domain=".xxxhu.com")
直连后立刻恢复:200、无 40362、正文容器 Post-RichText 在。
踩坑八:成功判据写错------文章正文藏在 js-initialData
最初用 'articleBody' in r.text 判断是否成功,结果误判"无正文"。实际上这些页面的正文容器是 <div class="RichText ztext Post-RichText ...">,而且更干净的数据其实在页面内嵌的 js-initialData JSON 里------它包含结构化的标题、点赞数、评论数和正文 HTML:
python
import re, json
def parse(raw):
m = re.search(r'<script id="js-initialData" type="text/json">(.*?)</script>',
raw, re.S)
j = json.loads(m.group(1))
arts = j['initialState']['entities']['articles']
a = list(arts.values())[0]
return {'title': a['title'], 'voteup': a['voteupCount'],
'comment': a['commentCount'], 'content': a['content']}
直接解析 js-initialData 比从 DOM 抠正文稳得多,拿到的 content 是干净的文章 HTML,再转 Markdown 即可。
顺带一提,某乎的 Web API(
/api/v4/articles/{id})裸调会返回10003 请求参数异常,因为它需要x-zse-96签名头(由前端 JS 计算)。所以走"SSR 页面 + js-initialData"反而比走 API 省事。
防 ban 节流策略
某乎对频率敏感,下载器必须克制。采用的节流方案:
python
import time, random
for idx, art in enumerate(articles, 1):
download_one(art)
nap = random.uniform(6, 14) # 每篇随机 6~14 秒
if idx % 5 == 0:
nap = random.uniform(30, 55) # 每 5 篇额外长歇 30~55 秒
time.sleep(nap)
并对风控做退避:检测到响应含 40362 / 请求存在异常 时,sleep(120) 长退避。配合断点续传(已下载的文件跳过),全程未再触发风控,39 篇全部抓下。
两个工程性小坑
踩坑九:import 脚本意外触发执行。 下载脚本没写 if __name__ == '__main__': 守卫,结果想 import download_articles 复用其中的解析函数时,整个下载主流程被直接执行了 。教训:可复用的脚本一定要加 __main__ 守卫,把"库"和"入口"分开。
踩坑十:brotli 解压偶发失败。 个别文章报:
text
Received response with content-encoding: br, but failed to decode it.
brotli: decoder process called with data when 'can_accept_more_data()' is False
这是 brotli 解码器的偶发问题。解法是对失败项改用 Accept-Encoding: gzip, deflate 重试,避开 br:
python
H = {..., 'Accept-Encoding': 'gzip, deflate'}
两篇失败文章用 gzip 重试后立即成功。
总结
这次任务最大的认知更新是:对强风控站点,"用自动化浏览器渲染页面"往往不如"用浏览器拿登录态 + 朴素 requests 抓取"可靠。浏览器自动化(CDP)的价值在于解决"登录态获取"这个真人环节,而真正的批量抓取交给无指纹的 HTTP 客户端反而更稳、更快、更省。
把踩过的坑收敛成几条经验:
- 自动化浏览器和你的登录浏览器是两套东西,别指望前者天然有登录态。
- Chrome 136+ 不允许对默认 profile 开调试端口 ;Chrome 127+ 的 App-Bound 加密让复制 profile 带不出 Cookie------这两条堵死了"偷渡登录态"的老办法,只能在受控的非默认 profile 里重新登录。
- 多窗口要靠
CommandLine精确区分 ,别让人扫错二维码;查登录态用/json/list看真实标签。 - httpOnly Cookie(如
z_c0)要用Storage.getCookies取 ,state save拿不全;CDP WebSocket 记得suppress_origin=True。 - 系统代理是隐形杀手 :
requests默认吃HTTP_PROXY,强风控站点下务必trust_env=False直连。 - 优先找页面内嵌的结构化数据 (如
js-initialData),比抠 DOM 或调签名 API 都省事。 - 节流 + 退避 + 断点续传 是批量抓取的标配;脚本加
__main__守卫;压缩解码出错就换Accept-Encoding。
工具只是手段,理解目标站点的防护链路、在"浏览器"和"HTTP 客户端"之间各取所长,才是把活干成的关键。