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

undetected-chromedriver captcha - How to Handle CAPTCHA in 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
NetworkExceptionCapSkip 没有运行,或者端口不一致启动应用,并在它的设置里核对端口
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 是一个 本地验证码识别工具,所以上面每一次识别都发生在你自己的机器上。