【skill】agent-browser实战与踩坑-CDP接管浏览器突破知乎反爬

文章目录

    • 概述
    • [工具背景: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 客户端反而更稳、更快、更省。

把踩过的坑收敛成几条经验:

  1. 自动化浏览器和你的登录浏览器是两套东西,别指望前者天然有登录态。
  2. Chrome 136+ 不允许对默认 profile 开调试端口Chrome 127+ 的 App-Bound 加密让复制 profile 带不出 Cookie------这两条堵死了"偷渡登录态"的老办法,只能在受控的非默认 profile 里重新登录。
  3. 多窗口要靠 CommandLine 精确区分 ,别让人扫错二维码;查登录态用 /json/list 看真实标签。
  4. httpOnly Cookie(如 z_c0)要用 Storage.getCookiesstate save 拿不全;CDP WebSocket 记得 suppress_origin=True
  5. 系统代理是隐形杀手requests 默认吃 HTTP_PROXY,强风控站点下务必 trust_env=False 直连。
  6. 优先找页面内嵌的结构化数据 (如 js-initialData),比抠 DOM 或调签名 API 都省事。
  7. 节流 + 退避 + 断点续传 是批量抓取的标配;脚本加 __main__ 守卫;压缩解码出错就换 Accept-Encoding

工具只是手段,理解目标站点的防护链路、在"浏览器"和"HTTP 客户端"之间各取所长,才是把活干成的关键。

相关推荐
曦尧2 小时前
GitHub - jwasham/coding-interview-university: 一份完整的计算机科学学习计划,助你成为软件工程师。· GitHub
ai·自动化
Sam09273 小时前
Loop Engineering 是什么:让 AI Agent 从一次性回答变成可迭代执行
人工智能·ai
财经资讯数据_灵砚智能3 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月15日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
Sam09273 小时前
AI Agent 幻觉怎么控制:从 RAG 证据、工具调用到上线门禁
人工智能·ai
程序员无隅3 小时前
面向 Skill 编程:用领域知识工程解决项目文档腐化
ai
菜鸟分享录3 小时前
AI 学习路线 04:机器学习到底在学什么?从分类、回归到模型评估
人工智能·机器学习·ai
DS随心转插件3 小时前
智谱清言化学式粘贴后变形如何修复?AI 导出鸭从根源解决化学公式跨文档乱码难题
人工智能·ai·豆包·deepseek·ai导出鸭
marsh02063 小时前
62 openclaw金融级应用开发:安全与性能的双重挑战
安全·青少年编程·ai·金融