如何在 Patchright 中识别验证码且不破坏隐身效果

Patchright 里的验证码流程和别处一样,还是那三步:读出 sitekey,发给识别工具,把 token 写回页面。让人意外的是第三步。Patchright 默认在隔离上下文里运行你的 JavaScript,而隔离上下文共享页面的 DOM,却不共享页面的 JavaScript 全局对象。于是 token 确实写进去了,textarea 里也真的存着这个值,但站点自己的回调永远不会执行,因为它根本不存在于你代码所处的作用域里。多传一个参数就能解决。
你需要什么
- Python 3.10 或更高版本;如果你更想用 JavaScript 包,则需要 Node.js。Patchright 为这两种运行时都提供了发行版,在各自的运行时里都是 Playwright 的直接替代品。
- 通过 Patchright 自带安装器下载的 Chromium 浏览器。Firefox 和 WebKit 没有打补丁,也不受支持。
- 受保护表单的页面 URL。sitekey 在运行时读取。
- 如果脚本和识别工具在同一台机器上,就让 CapSkip 以 Local 模式运行;如果不在同一台机器上,就用 Server 模式。两种模式都写在 连接设置.
# pip install patchright pip install -U capskip patchright # Pulls the browser. Real Chrome is the recommended channel, # and the maintainers say so explicitly. patchright install chrome
Patchright 到底改了什么
Patchright 就是去掉了明显自动化痕迹的 Playwright。最主要的一项补丁是它从不调用 Runtime.enable:原生 Playwright 会话最响亮的一个信号就是它。Patchright 的做法是改为在隔离的 ExecutionContexts 里运行你的脚本。它还彻底禁用了 Console API,加上了隐藏 navigator.webdriver 的标志,并去掉了若干会把会话标记为自动化的 Playwright 默认项:自动化标志本身、弹窗拦截器的覆盖、组件更新的屏蔽,以及那些禁用默认应用和扩展的开关。
其中两项对验证码相关的工作有直接影响,而且都很容易踩坑。
控制台被关掉,意味着你在页面里打的任何日志都传不出来。Patchright 里控制台功能完全不工作,所以往 evaluate 调用里塞一行日志、再从驱动端读出来的习惯在这里行不通。改成从调用里返回一个值。这本来就是更好的做法,而且你也只有这一个选择。
扩展被重新启用则是友好的一项。Playwright 正常启动时会禁用扩展,而 Patchright 去掉了那个开关,所以加载到持久化配置文件里的浏览器扩展是真的能跑起来的。如果你完全不想写这些代码, CapSkip 浏览器扩展 会在页面里处理小组件,你只需要像人工通过之后那样去操作表单。
还有一个值得知道的能力:Patchright 用普通定位符和 XPath 就能进入 closed shadow root。把标记藏在封闭根后面的小组件,不需要任何特殊处理就能定位到。
第 1 步:按维护者推荐的方式启动
Patchright 的隐身效果既取决于补丁,也同样取决于启动配置。文档给出的方案是:在真实的 Chrome channel 上使用 persistent context,窗口可见,不覆盖 viewport,并且完全不设置自定义 user agent 或请求头。最后这两点很关键:手工设定的 user agent 会和指纹的其余部分自相矛盾,把前面的功夫全都抵消掉。
# pip install patchright
from patchright.sync_api import sync_playwright
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir="C:\\profiles\\scraper",
channel="chrome",
headless=False,
no_viewport=True,
# Do not set user_agent or extra headers here.
)
page = context.new_page()
page.goto("https://example.com/page-with-recaptcha")注意这里是可见窗口。无头模式(headless)是检测预算花得最多的地方,而推荐配置并不使用它。在 Windows 上,这意味着运行脚本的账户需要一个交互式桌面会话,在把它放上服务器之前值得先规划好这一点。
第 2 步:从页面读出 sitekey
sitekey 位于宿主文档上,而不是在小组件的 iframe 里,用一个普通定位符就能读到。定位符走的是浏览器协议,而不是任何执行上下文,所以隔离世界的那套机制完全不影响这一步。
# Locators auto-wait, so this doubles as a wait condition
# for a widget that renders late.
holder = page.locator("div.g-recaptcha")
holder.wait_for(state="attached", timeout=15000)
sitekey = holder.get_attribute("data-sitekey")
print(sitekey) # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx有些站点从不把密钥放在宿主页面上,只在小组件的 iframe URL 里传递。这种情况下就从查询字符串里读出来,并且在花掉一次识别之前先检查它,因为空值会一路传到识别工具那里,最后换回来的是 ERROR_GOOGLEKEY,距离真正出问题的那次读取已经隔了很远。
# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs
src = page.locator("iframe[src*='recaptcha/api2/anchor']").get_attribute("src")
sitekey = parse_qs(urlparse(src).query)["k"][0]第 3 步:在你自己的机器上完成识别
Python SDK 通过 port 8080 与 CapSkip 通信,并以普通字符串的形式返回 token。一个方法就覆盖了 reCAPTCHA v2、Invisible、Enterprise 和 v3,各个变体是以选项的形式传入的,而不是分成不同的调用。
# pip install capskip from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) # Invisible v2 takes invisible=1, Enterprise takes enterprise=1, # and v3 takes version="v3" with an action. result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL) token = result["code"] # the g-recaptcha-response value
Turnstile 和极验(GeeTest)有各自的方法,形式完全一样。Turnstile 还会返回完成这次识别时所用的 user agent,挑战页面只有在提交 token 时一并带上这个 user agent 才会接受它。只在表单请求里带上它。不要把它喂给浏览器启动器,因为手工设定的 user agent 正是隐身配置叫你避开的东西。各自的完整参数列表见 CapSkip API 文档.
第 4 步:把 token 放到页面能用到的地方
下面这部分是这个库特有的。响应用的 textarea 被 display:none 隐藏了,所以没法往里输入,值只能用 JavaScript 赋进去。这个赋值从隔离上下文里做是可行的,因为 DOM 是共享的。构造字符串时用 json.dumps 而不是 f-string,因为一个 JSON 字符串字面量同时也是合法的 JavaScript 字符串字面量,引号和转义都算在内。
# A DOM write is fine from the isolated context.
import json
page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(token)
)
page.click("button[type=submit]")这适用于提交时会读取 textarea 的站点。相当多的站点并不这样。它们向小组件注册了一个成功回调,压根不看 textarea,所以 token 必须交给页面自己定义的函数。Patchright 的隔离上下文有自己的 JavaScript 全局对象,也就是说页面的函数和 reCAPTCHA 客户端配置在里面根本不存在。这个调用会抛出引用错误,重试多少次都没用。
Patchright 给出的解法是多加一个参数。evaluate、evaluate_handle 和 evaluate_all 这几个方法都接受 isolated_context,默认值是 True,把它设为 False 就会在页面自己的 main world 里运行脚本。
# The main world is where the page's own globals live.
page.evaluate(
"token => window.onRecaptchaSuccess(token)",
token,
isolated_context=False,
)从页面的标记里读出回调名称,不要靠猜。main world 只用来做注入,别的都不要放进去:在那里运行的代码站点是看得见的,而这正是隔离上下文成为默认值的全部理由。两种提交方式底层都还是普通的 reCAPTCHA v2,两者都写在 reCAPTCHA v2 识别页面.
完整可运行示例
以上内容合成一个脚本。识别工具只创建一次,在花掉一次识别之前先检查 sitekey,token 的校验靠返回一个长度,而不是在页面里打日志。
# pip install capskip patchright
import json
from patchright.sync_api import sync_playwright
from capskip import CapSkip
PAGE_URL = "https://example.com/page-with-recaptcha"
solver = CapSkip(host="127.0.0.1", port=8080)
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir="C:\\profiles\\scraper",
channel="chrome",
headless=False,
no_viewport=True,
)
page = context.new_page()
page.goto(PAGE_URL)
holder = page.locator("div.g-recaptcha")
holder.wait_for(state="attached", timeout=15000)
sitekey = holder.get_attribute("data-sitekey")
if not sitekey:
raise RuntimeError("Widget found but data-sitekey was empty.")
token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]
page.evaluate(
"document.getElementById('g-recaptcha-response').value = "
+ json.dumps(token)
)
length = page.evaluate(
"document.getElementById('g-recaptcha-response').value.length"
)
print(length) # 0 means the injection did not land
page.click("button[type=submit]")
page.wait_for_load_state("networkidle")
context.close()把识别工具跑在另一台机器上
Patchright 最后往往会跑在比你写脚本那台更强的机器上,而推荐的可见窗口配置会很快把人推向一台专用虚拟机。识别工具不必跟着一起搬。
CapSkip 有两种连接模式。本地模式绑定到 127.0.0.1,只有该设备本身能访问,你在应用所在的这台机器上写脚本时用它正合适。服务器模式绑定到你的内网或公网 IP,这样一台采集用的虚拟机、第二台工作站,或者一整组机器,都能通过 API 调用同一台 Windows 机器。建议配一个固定公网 IP,好让这个地址保持稳定。代码里除了主机地址之外什么都不用改,成本也一样不变,因为这仍然是你自己的硬件。
# Same SDK, same call. Only the host moves. solver = CapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")
识别工具一旦监听在网络地址上,就把密钥校验打开,并且给每台机器发各自的密钥,这样吊销其中一个不会动到其他机器。两种模式都完整走了一遍,见 CapSkip 设置指南.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| textarea 里有 token,但表单还是失败 | 站点用的是回调,从不读 textarea | 用 isolated_context=False 调用那个回调 |
| 报引用错误,指向某个页面函数 | 页面全局对象在隔离上下文里不存在 | 同样的修复:把那一个调用放到 main world 里运行 |
| 页面里的控制台日志什么都传不出来 | Patchright 完全禁用了 Console API | 改为从 evaluate 返回值,不要打日志 |
| Firefox 或 WebKit 表现得和原生 Playwright 一样 | 只有 Chromium 浏览器打了补丁 | 使用 Chromium 或 Chrome channel |
| 打了补丁仍然被拦 | 自定义 user agent 或请求头和指纹自相矛盾 | 去掉它们,在 Chrome 上使用 persistent context |
| ERROR_GOOGLEKEY | 一个空的 sitekey 传到了识别工具 | 在调用 recaptcha 之前先断言这个值 |
| NetworkException | CapSkip 没在运行,或者 host 填错了 | 启动应用,或者把 host 指向服务器地址 |
| TimeoutException | 识别耗时超过了 recaptchaTimeout | 把它调到默认的 300 秒以上 |
常见问题
我能把 Playwright 脚本原封不动搬过来吗?
改一下 import,大部分都能跑。定位符、上下文、路由、等待和导航的行为都和原来一样。有三件事需要再看一眼:任何碰到页面所定义内容的 evaluate 调用,现在都需要多传那个参数;任何依赖控制台输出的地方,那部分已经没有了;以及任何 Firefox 或 WebKit 目标,它们没有打补丁。更完整的 Playwright 情况见 Playwright 验证码识别页面.
用 main world 会让我被检测到吗?
原则上会,因为页面能看到在那里运行的代码。但实际暴露的只是一次持续几微秒的函数调用,而且它看起来和人工通过挑战时小组件自己的脚本所做的事一模一样。其余部分全都留在隔离上下文里,注入用一次调用而不是多次完成,暴露面就很小。
我该去点击 Turnstile 小组件,而不是注入 token 吗?
有时候是的。managed 模式下的 Turnstile 复选框在浏览器足够可信时能自己通过,而这正是 Patchright 要做的事,所以值得先试点击,不行再退回到识别。reCAPTCHA 的复选框点下去只会打开图片挑战,那边捞不到什么好处。小组件这一侧的内容见 Cloudflare Turnstile 识别器页面.
我的爬虫跑在 Linux 上,CapSkip 该装在哪?
装在一台你自己控制的 Windows 机器上,并打开服务器模式。之后那台 Linux 机器就像调用任何其他内部服务一样通过 API 调用它,所以 Patchright 和识别工具不需要共用操作系统,甚至不需要在同一个网段。把 host 参数指向那个地址,打开密钥校验,再给爬虫发一个它自己的密钥。
简短版结论
安装 Patchright,在 Chrome channel 上启动一个 persistent context,窗口可见、不设自定义 user agent,用普通定位符读出 sitekey,然后交给 127.0.0.1:8080 上的 CapSkip 去识别。在默认的隔离上下文里把 token 直接写进 textarea,只有当站点要求调用回调时才动用 isolated_context=False。Python 生态的其余部分,包括 Selenium 和 Playwright,见 Python 验证码识别页面.
在把抓取规模扩大之前,有一件事值得知道。CapSkip 是一个 无限量验证码识别工具 ,跑在你已经拥有的硬件上,所以重试一千个页面和重试十个页面花的钱完全一样。
