如何在 undetected-chromedriver 中处理验证码(Python)

undetected-chromedriver 会给 Chrome 打补丁,让自动化标志不再泄露。这能让你进到页面上,但过不了 reCAPTCHA 复选框,因为小组件不是指纹检测,而是对 token 的要求。undetected-chromedriver 遇到的验证码,仍然要识别出来,再把结果注入 DOM。
四个步骤:启动打过补丁的 driver,从页面读出 sitekey,在本地识别,把 token 放到页面期待的位置。下面是完整的 Python 实现。
undetected-chromedriver 能做什么,不能做什么
| 信号 | driver 是否处理 |
|---|---|
navigator.webdriver 以及 CDP 留下的破绽 | 是 |
| 打过补丁的二进制文件,因此不会被认出是自动化 driver | 是 |
| 登录表单上的 reCAPTCHA v2 复选框 | 否 |
| 由页面触发的隐形 v2 或 v3 挑战 | 否 |
| Turnstile 小组件 | 否 |
这个区别之所以重要,是因为爬虫被拦时,常见的建议都是 “加个 stealth”。可只要小组件出现在屏幕上,再多的 stealth 也去不掉它。你需要的是一个 token。
你需要什么
- 已装好 Chrome,并且 CapSkip 正在运行,监听
127.0.0.1:8080。 设置指南 介绍了这个应用。 - Python 3.10 或更高版本。
- 三个包。
# the patched driver, selenium itself, and the local solver pip install undetected-chromedriver selenium capskip
第 1 步:启动打过补丁的 driver
把它导入为 uc ,然后像用普通 Selenium 一样构建浏览器。Options 来自 uc.ChromeOptions(),而不是 Selenium 的,因为这个库会在传递过程中给它们打补丁:
# pip install undetected-chromedriver
import undetected_chromedriver as uc
options = uc.ChromeOptions()
options.add_argument("--window-size=1280,800")
# use_subprocess keeps the patched binary alive long enough
# to shut down cleanly on Windows.
driver = uc.Chrome(options=options, use_subprocess=True)
driver.get("https://example.com/login")不要在它上面再叠一个 stealth 插件。两个打补丁的库争抢同一批属性,本身就是一个检测信号,也是原本能用的配置突然失效的常见原因。
第 2 步:从页面读出 sitekey
sitekey 是小组件元素上的公开属性。要从实时 DOM 里读,而不是写死,因为不少站点会按环境轮换密钥:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# The widget is injected by script, so wait for it.
widget = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)
sitekey = widget.get_attribute("data-sitekey")
page_url = driver.current_url
print(sitekey)使用 driver.current_url ,而不是你原本请求的那个 URL。如果站点把你重定向到了挑战页路径,就必须把你实际落地的页面告诉识别工具。
第 3 步:在本地识别
CapSkip 跑在你自己的机器上,所以识别就是一次对 localhost 的调用。不用账号,不按次计费,除了 sitekey 和页面 URL,没有任何东西离开这台机器:
# pip install capskip
from capskip import CapSkip
solver = CapSkip(
host="127.0.0.1",
port=8080,
recaptchaTimeout=300, # seconds, also covers Turnstile
)
result = solver.recaptcha(sitekey=sitekey, url=page_url)
token = result["code"] # the g-recaptcha-response value各种变体只是选项,而不是不同的方法。加上 invisible=1 用于隐形 v2 小组件, version="v3" 带有一个 action 用于 v3, enterprise=1 用于 Enterprise 产品。
第 4 步:注入 token 并触发回调
token 必须落进隐藏的 g-recaptcha-response textarea 里。对普通的表单提交来说,光这一步就够了,因为这个字段会和表单其余部分一起提交上去:
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".innerHTML = arguments[0];",
token,
)
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()单页应用不一样。它们从不读这个 textarea,而是等小组件自己的回调把 token 交给它们。函数名声明在元素上,所以你可以直接调用:
callback = widget.get_attribute("data-callback")
if callback:
# Call the page's own handler with the token, exactly as
# the widget would have done.
driver.execute_script(f"{callback}(arguments[0]);", token)如果注入之后什么都没发生,而且元素上也没有 data-callback,那就是页面在 JavaScript 里挂的回调,而不是写在标记里。 reCAPTCHA v2 回调识别工具 页面讲了这种情况下怎么把它找出来。
在同一个 driver 里处理 Turnstile
Cloudflare Turnstile 的做法完全一样,只是两个名字变了。响应字段是 cf-turnstile-response,而且它是 input,不是 textarea:
result = solver.turnstile(sitekey=sitekey, url=page_url)
driver.execute_script(
"document.querySelector('[name=cf-turnstile-response]')"
".value = arguments[0];",
result["code"],
)以上说的是嵌在表单里的小组件。整页拦截式挑战还需要两个额外的值,以及执行识别时所用的 user agent,这些在 Turnstile 识别工具 页面里有说明。
完整可运行示例
# pip install undetected-chromedriver selenium capskip
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from capskip import (
CapSkip, NetworkException, ApiException, TimeoutException,
)
URL = "https://example.com/login"
solver = CapSkip(host="127.0.0.1", port=8080)
driver = uc.Chrome(options=uc.ChromeOptions(), use_subprocess=True)
try:
driver.get(URL)
widget = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)
sitekey = widget.get_attribute("data-sitekey")
result = solver.recaptcha(sitekey=sitekey, url=driver.current_url)
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".innerHTML = arguments[0];",
result["code"],
)
callback = widget.get_attribute("data-callback")
if callback:
driver.execute_script(f"{callback}(arguments[0]);", result["code"])
else:
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
except NetworkException:
print("CapSkip is not running on 127.0.0.1:8080")
except (ApiException, TimeoutException) as err:
print(f"solve failed: {err}")
finally:
driver.quit() # always, or the patched binary lingers识别和提交要在同一次运行里完成。reCAPTCHA token 大约两分钟内有效,所以一个先识别、再干三十秒别的活、然后提交的脚本没问题;把 token 排队留到以后再用的脚本就不行。
常见错误
| 你所看到的 | 原因 | 修复 |
|---|---|---|
This version of ChromeDriver only supports Chrome version N | 打过补丁的 driver 猜错了 Chrome 的主版本号 | 显式传入: uc.Chrome(version_main=N) ,其中 N 是你已安装的 Chrome 主版本号 |
OSError: [WinError 6] The handle is invalid 在退出时出现 | driver 在关闭之前就被垃圾回收了 | 调用 driver.quit() ,放在 finally 块里,并保持 use_subprocess=True |
NoSuchElementException ,在定位以下元素时出现: [data-sitekey] | 小组件在 iframe 里,或者还没渲染出来 | 按上面的方式等它出现。如果在 iframe 里,就改从 iframe 的 src 查询字符串里读 sitekey |
ERROR_GOOGLEKEY | 空的或被截断的 sitekey 传到了识别工具 | 识别前先打印出来。参见 修复 ERROR_GOOGLEKEY |
NetworkException | CapSkip 没有运行,或者端口不一致 | 启动应用,并在它的设置里核对端口 |
| token 已注入,表单仍然被拒 | 页面用的是回调,从不读那个 textarea | 调用 data-callback ,并把 token 传进去 |
每个错误码和参数都列在 API 文档.
常见问题
undetected-chromedriver 自己能识别验证码吗?
不能。它隐藏的是那些会让浏览器被标记的自动化信号。已经渲染出来的小组件仍然要一个有效的 token,所以你得把它识别出来,再注入结果。这两件事是互补的,不是二选一。
可以无头运行吗?
可以,用 uc.Chrome(headless=True),它应用的是这个库自己的无头补丁,而不是 Selenium 的那个开关。要预料到挑战会比有头模式多。token 注入本身在两种模式下完全一样。
识别时需要代理吗?
只有在浏览器本身已经走代理时才需要。把同一个代理作为 proxy={"type": "HTTPS", "uri": "user:pass@host:port"} 传进去,让识别和提交走同一条网络路径。代理适用于 reCAPTCHA、Turnstile 和极验(GeeTest),对图片验证码从来不适用。
这套做法在普通 Selenium 上也能用吗?
能。第 2 到第 4 步都是普通的 WebDriver 调用,所以在标准 Selenium、Selenium Grid 和远程 driver 上都能原样运行。只有第 1 步是这个打补丁的库特有的。
小结
指纹交给 undetected-chromedriver,小组件自己动手。先等 [data-sitekey],对 localhost 发起识别,把 token 写进 g-recaptcha-response,并调用 data-callback (如果页面声明了回调的话)。在 finally 块里退出 driver,免得打过补丁的进程一直赖着不走。
更完整的 Selenium 相关内容在 Selenium 验证码识别工具 页面,完整的方法列表在 Python 验证码识别 页面,而 网络爬虫验证码识别 讲了怎么把这套流程跑到规模上。CapSkip 是一个 本地验证码识别工具,所以上面每一次识别都发生在你自己的机器上。
