如果你的 undetected-chromedriver 脚本去年还能用,如今却卡在 'Just a moment...'、抛出 403,或者眼睁睁看着 Turnstile 组件无尽地转圈,那并不是你做错了什么。这个库本身并没有从根本上崩坏。只是 Cloudflare 已经把检测推进到了 undetected-chromedriver 当初设计来隐藏的那些破绽之外。
本文会通俗地讲清楚:undetected-chromedriver(UC)究竟做了什么、为什么现代 Cloudflare 依然能识别它,以及如何解读你看到的各种症状。我们会先从诚实的免费修复方法讲起,并指明它们究竟在哪里碰壁,然后介绍当你需要不会每次 Cloudflare 更新就崩溃的自动化时的务实路径:要么继续驱动浏览器但把挑战外包出去,要么彻底放弃浏览器,通过一次简单的 HTTP 调用取回 cf_clearance。
undetected-chromedriver 究竟做了什么
undetected-chromedriver 是 Selenium 的 ChromeDriver 的一个打过补丁的构建版本。原版 ChromeDriver 会留下明显的指纹:它把 navigator.webdriver 设为 true,在 document 对象里注入一个 bot 检测器会去搜索的 cdc_ 变量,并触发若干 Chrome DevTools 协议(CDP)破绽。UC 给 ChromeDriver 二进制文件和启动参数打补丁,剥离这些信号,因此一个刚启动的浏览器看起来远比默认的 Selenium 会话更像人类的 Chrome。
在很长一段时间里,这就足够了。Cloudflare 较旧的检查严重依赖的正是这些自动化破绽,所以移除它们往往就是 403 和 200 之间的区别。UC 之所以成为 '用真实浏览器抓取 Cloudflare 站点' 的默认推荐,正是因为它掩盖了那些容易暴露的痕迹。而 2026 年的问题在于,那些容易暴露的痕迹已不再是 Cloudflare 所关注的东西。
为什么现代 Cloudflare 仍能识别它
Cloudflare 当前的 bot 管理并不依赖单一破绽。它叠加了多个相互独立的检测系统,而 undetected-chromedriver 只处理其中最古老的那一层:
- UC 无法通过的 Turnstile 与托管挑战:Turnstile 会运行一个非交互式的工作量证明(proof-of-work)外加浏览器 API 探测,然后才签发一个 cf-turnstile-response token。托管挑战会按每次请求动态挑选检查项。UC 能渲染这些组件,但它没有任何逻辑去真正生成有效 token 或攻克一个困难的托管挑战,因此页面始终无法前进。
- CDP 与运行时泄露:UC 通过 DevTools 协议驱动 Chrome,而这个连接本身就是可观测的。检测器会探测 CDP 痕迹、时序异常,以及只有在浏览器被自动化时才存在的运行时对象。给 navigator.webdriver 打补丁并不会移除其底下的 CDP 表面。
- TLS / JA3 指纹:在任何 JavaScript 运行之前,Cloudflare 就会检查 TLS ClientHello。自动化技术栈的握手可能与真正的消费级 Chrome 产生偏差,而 JA3/JA4 不匹配在第一个数据包就会被标记,无论 JS 环境看起来多么干净。
- navigator.webdriver 与堆栈追踪破绽:即便打补丁修掉了明显的标志位,错误堆栈追踪、被注入的函数签名,以及微妙的属性描述符差异仍会出卖自动化。现代检测脚本读取的正是这些更高阶的破绽,而不只是 UC 修复的那个布尔值。
- 维护放缓:这种猫鼠游戏只有在补丁跟得上 Chrome 与 Cloudflare 发版时才有效。UC 的维护已经放缓,而同一位维护者现在引导大家转向 nodriver——一个以远少得多的 CDP 痕迹与 Chrome 通信的继任者。如果你还在用 UC,你的部分问题可能仅仅是补丁落后了。
解读你看到的症状
你得到的错误会告诉你是哪一层抓住了你。把症状对上原因,可以省去瞎猜:
- 卡在 'Just a moment...':过渡页加载了,但挑战始终没有通过。你的浏览器被要求自证身份却没通过检查,要么是因为 CDP/运行时泄露,要么是因为这是一个 UC 无法求解的困难托管挑战。
- 普通 403(或 error 1020):你在握手之前或握手期间就被标记了,通常是 TLS/JA3 不匹配或低信誉的数据中心 IP。Cloudflare 直接拒绝了你,甚至没有下发挑战。
- Turnstile 组件始终不通过:表单渲染出了一个 Turnstile 框,它一直转圈或不断重置。UC 没有任何 token 求解路径,因此该组件永远无法生成可用的 cf-turnstile-response,你的提交也就失败了。
- 即使 headless=False 也被检测到:一个常见的意外。以非无头模式运行确实移除了无头破绽,但它对 CDP 痕迹、TLS 指纹或托管挑战毫无作用。一个可见的窗口并不等于一个不可检测的窗口。
值得先试的免费修复
在为任何东西付费之前,先把免费选项用尽。对于较轻量的 Cloudflare 配置,这些方法确实有效,没必要给只做被动检查的站点过度设计。
按工作量从小到大依次尝试:
- 升级 UC 和 Chrome:相当一部分 'undetected-chromedriver 失效' 的报告,不过是针对较新 Chrome 的一个过时补丁而已。先固定匹配的版本并更新,再做其他任何事。
- 切换到 nodriver:同样出自那位维护者,nodriver 彻底抛弃了 Selenium/ChromeDriver 这一层,以远少得多的 CDP 痕迹直接驱动 Chrome。它是预期中的现代替代品,往往能搞定 UC 已经拿不下的站点。
- 使用持久化的 user-data-dir:复用一个真实的 Chrome 配置文件,让 cookie、本地存储以及先前的 cf_clearance 在多次运行之间得以保留。一个 '热' 配置文件远比每次启动都被挑战的新配置更像人类。
- 在真实显示器上以非无头模式运行:无头模式会增加破绽。一个真正的窗口化浏览器(或服务器上的 xvfb)能移除这个特定信号,不过如上所述,它本身并非根治之法。
- 通过干净的住宅代理路由:数据中心 IP 会被快速标记。轮换的住宅或移动 IP 能提高你的信誉分,减少一刀切式的 403。
免费修复在哪里到头
对这里的上限要对自己诚实。即便是一套完全调优的 UC 或 nodriver 配置,也带有任何补丁都消除不了的硬性限制:
驱动真实浏览器很重:每个实例都要吃掉数百兆内存和实打实的 CPU,因此扩展到多个并发 worker 会迅速变得昂贵且脆弱。这套技术栈仍然可被指纹识别,因为即便明显的标志位已被移除,CDP 痕迹、TLS 偏差以及更高阶的运行时破绽依然存在。而关键在于,这些工具都无法求解 Turnstile token:它们能把组件显示给你,却无法铸造出有效的 cf-turnstile-response,因此任何由 Turnstile 把守的表单都会一直关着。当目标下发一个困难的托管挑战或一个 Turnstile 组件时,仅靠给浏览器打补丁就走到尽头了。
务实路径:把困难部分外包出去
一旦撞上打过补丁的浏览器也攻不下的那些层,务实之举就是别再试图在打补丁上压过 Cloudflare,而是把那个特定的困难步骤交给一个为此而生的服务。这有两种形态。
第一,你可以让浏览器继续负责所有能正常工作的部分,只在需要 Turnstile token 或托管挑战清除时调用一个托管 API。第二,通常也更简单,你可以彻底放弃浏览器:许多抓取任务一旦持有有效的 cf_clearance cookie 就不再需要真实浏览器,于是你通过一次 HTTP 调用取回那个 cookie,其余流程都跑在一个普通的 requests session 上,以原生速度运行。
NSLSolver 正是为这种外包而生的开发者 API。它在单一 HTTP 契约背后处理 Cloudflare Turnstile、Cloudflare Challenge(托管/JS 挑战)以及 Kasada。它不求解 reCAPTCHA、hCaptcha、DataDome 或图像验证码,因此这是专门在 Cloudflare 或 Kasada 成为你的拦路墙时才合适的工具。Turnstile 求解平均约 250ms,成功率 99.9%,按成功求解计费,因此失败的求解免费;定价为按量付费:Turnstile 每 1,000 次求解 $0.40,Cloudflare Challenge 每 1,000 次 $0.50,Kasada 每 1,000 次 $1.50。新账户注册即送 100 次免费请求,无需信用卡、无需加密货币即可开始。
在 Python 中用 NSLSolver 取回 cf_clearance
下面这个版本针对托管/JS 挑战彻底替换掉了浏览器。你把目标 URL POST 到 /solve,取回你必须在自己的 session 上重放的 cookies 和 User-Agent。cf_clearance 值存放在一个以 cf_clearance 为键的 cookies 字典内,因此用 session.cookies.update(...) 应用它,并把 User-Agent 请求头设为返回的 user_agent,使你的指纹与签发该 cookie 时所用的指纹相匹配。
请准确注意字段名:响应字段是 user_agent(snake_case 蛇形命名),而 cookies 是一个字典,不是列表。Cloudflare 会把 clearance cookie 与那个特定的 User-Agent 绑定在一起,所以请始终回传 API 返回的值,而不是你自己的。如果你要保留浏览器,则改为请求一个 Turnstile token:POST type 'turnstile' 并附上组件的 site_key 和页面 url,然后把返回的 token 注入为 cf-turnstile-response 的值。
import requests
API = "https://api.nslsolver.com"
HEADERS = {"X-API-Key": "nsl_YOUR_API_KEY"}
TARGET = "https://target-site.com"
# 1) Hand the Cloudflare managed/JS challenge to NSLSolver
resp = requests.post(
f"{API}/solve",
headers=HEADERS,
json={
"type": "challenge",
"url": TARGET,
"proxy": "http://user:pass@host:port",
},
timeout=120,
)
data = resp.json()
# 2) Replay the cf_clearance cookie + matching User-Agent
# on a plain requests session -- no browser needed
session = requests.Session()
session.cookies.update(data["cookies"]) # dict keyed by cf_clearance
session.headers["User-Agent"] = data["user_agent"] # snake_case field
# 3) Scrape at native speed -- the request looks like a cleared browser
page = session.get(TARGET)
print(page.status_code) # 200
print(page.text[:500])