如何在 DrissionPage 中识别验证码并注入 token

DrissionPage 里的验证码环节就三步:从页面上读出 sitekey,发给识别工具,再用 JavaScript 把 token 写回表单。DrissionPage 通过 DevTools Protocol 直接驱动一个真实的 Chrome,不走 webdriver,所以浏览器这一侧比 Selenium 简单。元素模型可没这么简单,而且它有一个默认设置,会把「小组件没找到」变成识别工具报的错,而不是你的定位符报的错。大多数脚本失败得莫名其妙都是因为这个默认值,所以下面单开一节讲它。
你需要什么
- Python 3.10 或更高版本,已安装 DrissionPage 4.1 和 CapSkip SDK。
- 一个 DrissionPage 能启动或接管的 Chromium 浏览器。
- 受保护表单的页面 URL。运行时读到的 sitekey。
- 如果脚本和识别工具在同一台机器上,就让 CapSkip 以 Local 模式运行;如果脚本跑在另一台机器上,就用 Server 模式。两种模式都写在 连接设置.
# Both packages, one line. pip install DrissionPage capskip
DrissionPage 为什么会改变这件事的做法
DrissionPage 直接通过 CDP 跟 Chrome 通信。中间没有 chromedriver 进程,所以没有驱动二进制要打补丁,也没有单独的服务需要跟着你的浏览器版本走。它还能接管一个已经在运行的浏览器,这一点在这里是真有用:你可以手动登录一次,让配置文件保持打开,然后让脚本接着用这个会话。
不变的是 token。识别成功的 reCAPTCHA 就是一个字符串,它最终必须落进那个隐藏的 g-recaptcha-response textarea 里,你才能提交。那个 textarea 是 display:none,所以在任何自动化工具里都没法直接往里输入。你要用 JavaScript 写进去,而 DrissionPage 的 run_js 方法就是干这个的。
有一点得说清楚:不用 webdriver 不等于不会被检测。通过 CDP 驱动 Chrome 只去掉了一个信号,你指纹里的其余部分原封不动。识别挑战和让自己看起来像一个真实浏览器是两件事,本文只讲前者。
第 1 步:从页面上读出 sitekey
sitekey 在宿主文档上,不在小组件的 iframe 里。Google 自己的标记把它放在一个容器元素上,属性名是 data-sitekey,而 DrissionPage 的属性定位符不用 CSS 选择器就能找到它。
# pip install DrissionPage
from DrissionPage import ChromiumPage
page = ChromiumPage()
page.get("https://example.com/page-with-recaptcha")
# @attr finds by attribute. The default search timeout is 10s,
# which is plenty for a widget that renders on load.
holder = page.ele("@data-sitekey")
sitekey = holder.attr("data-sitekey")
print(sitekey) # 6LxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxDrissionPage 的定位符前缀值得在这里学一下,因为它们能替掉你原本要写的大部分 XPath:@attr=value 是精确匹配,@attr:value 匹配子串,@attr^value 匹配开头,@attr$value 匹配结尾。在定位符前面加上标签名,比如 tag:iframe,还能把元素类型一起锁定。
有些站点根本不把 sitekey 放在宿主页面上,只在 iframe 的 URL 里传。那就改从那里读,用子串匹配定位到你要的那个 frame。
# Fallback: the k= query parameter on the widget iframe.
from urllib.parse import urlparse, parse_qs
frame = page.ele("tag:iframe@src:recaptcha/api2/anchor")
src = frame.attr("src")
sitekey = parse_qs(urlparse(src).query)["k"][0]第 2 步:调用本地 API 识别
识别工具跑在你自己机器的 8080 端口上,说的是 2captcha 那套 API,所以 SDK 调用只有一行,背后也没有按次计费。页面 URL 要从 page.url 取,而不是自己重新敲一遍,因为 get() 和识别动作之间若发生跳转,你提交的就会是错的 URL。
# pip install capskip from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) # Same call shape for v3 (version="v3") and Enterprise (enterprise=1). result = solver.recaptcha(sitekey=sitekey, url=page.url) token = result["code"] # the g-recaptcha-response value
Turnstile 和极验(GeeTest)各有自己的方法,solver.turnstile() 和 solver.geetest(),两者的调用形态和上面那次一样。各自的完整参数列表见 CapSkip API 文档.
第 3 步:注入 token 并提交
DrissionPage 向 run_js 传递额外参数时是按位置传的,脚本里读取的形式是 arguments[0] 等等。把 token 作为参数传进去,而不是用 f-string 拼出 JavaScript:token 很长、不透明,偶尔还塞满了你不想操心怎么转义的字符。
# Extra args arrive as arguments[0], arguments[1], ...
page.run_js(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
token,
)
# Then submit the form the way the page expects.
page.ele("tag:button@type=submit").click()如果站点不是在提交时读 textarea,而是定义了一个回调,那就在设置完值之后调用它。回调是站点自定义的函数名,所以要从页面自身的标记里读出来,不要靠猜;另外注意这仍然是普通的 reCAPTCHA v2:回调改变的是你怎么把 token 交出去,不是它怎么被识别的。 reCAPTCHA v2 识别工具页面 把两种提交方式都讲了。
最耗时间的那个静默失败
DrissionPage 出厂时 Settings.raise_when_ele_not_found 是 False。定位符什么都没匹配到,也不会抛异常。它返回一个 NoneElement,这个对象是假值,和 Python 的 None 比较相等,于是你的脚本继续往下跑。
这是有意为之的设计,在你探测可选元素时很好用。在这里就不好用了,因为你下一步就要读属性,而报出来的错点的是定位符,不是小组件。更糟的情况是元素确实存在,却压根没有 data-sitekey 属性。这时 attr() 交还一个 None,这个 None 一路以空 key 的形式传到识别工具,失败最后以 ERROR_GOOGLEKEY 的形式,从一个你根本没起疑心的 API 调用里冒出来。
两行代码就能解决。把抛异常打开,并且在花掉一次识别之前先检查 sitekey。
# Make a missing element fail where it happened.
from DrissionPage.common import Settings
Settings.set_raise_when_ele_not_found(True)
# And still check the value, because a found element can hold nothing.
if not sitekey:
raise RuntimeError("No sitekey on the page. Check the widget rendered.")完整可运行示例
上面所有内容,合成一个脚本。这里的 finally 块比平时更重要,因为 DrissionPage 可以接管一个不是你启动的浏览器,而把接手过来的浏览器关掉,通常并不是你想要的。
# pip install DrissionPage capskip
from DrissionPage import ChromiumPage
from DrissionPage.common import Settings
from capskip import CapSkip
Settings.set_raise_when_ele_not_found(True)
PAGE_URL = "https://example.com/page-with-recaptcha"
page = ChromiumPage()
solver = CapSkip(host="127.0.0.1", port=8080)
try:
page.get(PAGE_URL)
sitekey = page.ele("@data-sitekey").attr("data-sitekey")
if not sitekey:
raise RuntimeError("Widget found but data-sitekey was empty.")
result = solver.recaptcha(sitekey=sitekey, url=page.url)
page.run_js(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
result["code"],
)
page.ele("tag:button@type=submit").click()
page.wait.doc_loaded()
print(page.title) # the page you land on after submitting
finally:
page.quit()把识别工具跑在另一台机器上
爬虫迟早会被搬到服务器上,DrissionPage 跟着搬没问题:它需要一个 Chromium 二进制和一个显示缓冲区,其他也没什么了。识别工具倒不必跟着搬到同一台机器上。
CapSkip 有两种连接模式。 本地 模式绑定到 127.0.0.1,只有该设备本身能访问,你在写脚本的阶段用它正合适。 服务器 模式绑定到你的内网或公网 IP,这样一台采集用的虚拟机、一台容器宿主机或者第二台工作站,都能通过 API 调用同一个识别工具。固定公网 IP 能让这个地址保持稳定。代码里除了你传进去的主机地址之外什么都不用改,价格也不变,因为硬件还是你自己的。
# Same SDK, same call. Only the host moves. solver = CapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")
识别工具一旦监听在网络地址上,就把密钥校验打开,并且给每台机器发各自的密钥,这样吊销其中一个不会动到其他机器。 设置指南 把两种模式都走了一遍。
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| attr() 抛出 ElementNotFoundError | 定位符什么都没匹配到,返回了 NoneElement | 放宽定位符,或者等小组件渲染出来 |
| 元素不存在,脚本却继续往下跑 | raise_when_ele_not_found 默认为 False | Settings.set_raise_when_ele_not_found(True) |
| ERROR_GOOGLEKEY | attr() 返回了 None,提交上去的是一个空 key | 检查这个值,或者从 iframe 上读 k 参数 |
| NetworkException | CapSkip 没在运行,或者 host 填错了 | 启动应用,或者把 host 指向服务器地址 |
| TimeoutException | 识别耗时超过了 recaptchaTimeout | 把它调到默认的 300 秒以上 |
| 表单拒绝了一个识别本身没问题的 token | token 在提交之前就过期了 | 在提交前一刻才识别,不要在页面加载时就识别 |
常见问题
我该用 ChromiumPage 还是更新的 Chromium 类?
都行。4.1 版从顶层包导出 Chromium、ChromiumPage、SessionPage 和 WebPage,而 ChromiumPage 仍然是拿到单个标签页最短的路径。验证码这部分的活儿两者完全一样,因为读 sitekey、识别和注入 token 都是通过标签页对象完成的。
我需要切进 reCAPTCHA 的 iframe 吗?
不需要,这恰恰是大家过度设计的地方。复选框确实在 iframe 里,但 sitekey 属性和那个隐藏的响应 textarea 都属于宿主文档。只有当站点不把 sitekey 放到页面上、你必须从 frame 自己的 URL 里读时,才需要去碰 frame。
为什么我的脚本传了一个 None 的 sitekey 却一声不吭?
因为 DrissionPage 的默认行为是返回一个假值占位对象而不是抛异常,也因为一个存在的元素照样可能没有你要的那个属性。两条路径最后都会让一个你以为是字符串的变量装着 None。在文件顶部把抛异常打开,再对这个值加一次显式检查,两者合起来就把两种情况都盖住了。
我的爬虫跑在 VPS 上,识别工具该放哪?
放哪都行,只要两边能互相访问。Server 模式让识别工具监听在一个网络地址上,而不是回环地址,于是 VPS 就像调用任何内部服务那样通过 API 调它。把 host 参数指向那个地址,打开密钥校验,再给这台 VPS 发一个自己的密钥。
简短版结论
用属性定位符读出 sitekey,在 127.0.0.1:8080上完成识别,用 run_js 和位置参数把 token 注入进去,然后提交。做这些之前,先把 raise_when_ele_not_found 打开,因为默认值恰恰藏起了你最先会撞上的那个失败。想了解 Python 这边更完整的图景,包括 Selenium 和 Playwright,见 Python 验证码识别页面。而且,因为这个识别工具是 本地验证码识别工具 而不是按量计费的服务,所以重试一千个页面的抓取,和只重试十个页面的抓取,花的钱一样。
