如何在 SeleniumBase 测试中识别验证码(Python)

seleniumbase captcha - How to Solve CAPTCHA in SeleniumBase Tests (Python)

SeleniumBase 负责驱动浏览器,它从不替你去读验证码。所以在 SeleniumBase 里,一个验证码步骤就是三个动作:从页面上取出 sitekey,向识别工具要一个 token,再把这个 token 写回表单并提交。识别发生在浏览器之外,这正是同一份代码在有头运行、无头运行和 CI 任务里都能用的原因。下面用你最可能在用的两种写法各给一遍。

你需要什么

  • Python 3.10 或更高版本,并已安装 SeleniumBase 和 CapSkip SDK。
  • 一个真的会渲染出小组件的页面。Google 官方的 reCAPTCHA 测试密钥永远都能通过,所以它们证明不了你的代码有没有问题。
  • CapSkip 处于运行状态,可以是在回环地址上以 Local 模式运行,也可以是在你的测试运行器能访问到的机器上以 Server 模式运行。两者都记录在 连接设置,具体该选哪一种,取决于你的测试在哪里执行。
# Both packages, one command.
pip install seleniumbase capskip

UC 模式做什么,不做什么

SeleniumBase 自带 UC 模式,而它的职责值得说清楚,因为不少人是把它当识别工具来用的。UC 模式减少浏览器泄露出去的自动化信号,它的 GUI 辅助功能还能在 Cloudflare 的过渡页上点掉一个复选框。这和生成一个 reCAPTCHA token 是两回事。UC 模式里没有任何东西会去读一张扭曲的图片,或者从九宫格里挑出有公交车的那几格。

# pip install seleniumbase
from seleniumbase import SB

with SB(uc=True) as sb:
    # Disconnects the driver briefly so the page loads unobserved.
    sb.uc_open_with_reconnect("https://example.com/protected", reconnect_time=3)

    # Drives the real mouse pointer, so it needs a desktop session
    # or a virtual display. It clicks a checkbox; it solves nothing.
    sb.uc_gui_click_captcha()

由此有两个后果。GUI 辅助功能需要真实的显示器,这就把它挡在了大多数无头 CI 之外。而对于 reCAPTCHA 和极验(GeeTest),你仍然需要一个 token,这也是本文剩下部分要讲的内容。两者要一起用:用 UC 模式降低被挑战的频率,用识别工具解决那些照样找上门来的挑战。

第 1 步:从页面上读出 sitekey

sitekey 是公开的。它就作为一个属性挂在小组件元素上,SeleniumBase 一次调用就能读到,所以你完全不必按环境把它硬编码进代码。

# pip install seleniumbase
from seleniumbase import SB

PAGE = "https://example.com/page-with-recaptcha"

with SB() as sb:
    sb.open(PAGE)

    # Present on the widget div for v2, and on the script tag for v3.
    sitekey = sb.get_attribute("[data-sitekey]", "data-sitekey")

    print(sitekey)      # this plus the page URL is all the solver needs

第 2 步:向识别工具要一个 token

这一步和 SeleniumBase 毫无关系。它就是一次普通的 Python 调用,打到你自己机器上的一个服务,返回的就是当一个真人通过小组件时,页面本该生成的那串 token。

# pip install capskip
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# v2 checkbox is the default shape. Pass invisible=1 or
# enterprise=1 for those variants, or version="v3" for v3.
result = solver.recaptcha(
    sitekey=sitekey,
    url=PAGE,
)

token = result["code"]      # the value the form is waiting for

同一个客户端也能处理你在测试套件里可能遇到的其他类型。Turnstile 用 solver.turnstile,传 sitekey 和页面 URL;极验用 solver.geetest,传 gt 和 challenge 两个值;图片验证码用 solver.normal,传一个文件路径、一个 URL 或者一个 base64 data URI。 Python 验证码识别页面 完整覆盖了这套接口。

第 3 步:注入 token 并提交

Google 会在表单里放一个隐藏的 textarea,并期望 token 出现在它的值里。用一次脚本调用把 token 写进去,然后按页面自身 UI 的方式提交表单。

# The response field is hidden, so a normal type() will not reach it.
sb.execute_script(
    "document.getElementById('g-recaptcha-response').value = arguments[0];",
    token,
)

sb.click("button[type='submit']")
sb.assert_element(".signup-success")

如果页面是给小组件挂了自己的回调函数,而不是在提交时去读那个字段,那就把 token 传给这个函数,而不是去点击提交。你需要哪一种,看小组件的标记就知道:data-callback 属性会写出函数名,没有这个属性就说明表单读的是那个字段。

完整的测试

下面是把整件事写成一个 BaseCase 类的样子,这种写法能直接塞进已有的 pytest 测试套件,不需要额外接线。

# pip install seleniumbase capskip
from seleniumbase import BaseCase
from capskip import CapSkip

BaseCase.main(__name__, __file__)

PAGE = "https://example.com/page-with-recaptcha"


class RecaptchaTest(BaseCase):
    def test_signup_form(self):
        self.open(PAGE)
        sitekey = self.get_attribute("[data-sitekey]", "data-sitekey")

        solver = CapSkip(host="127.0.0.1", port=8080)
        token = solver.recaptcha(sitekey=sitekey, url=PAGE)["code"]

        self.execute_script(
            "document.getElementById('g-recaptcha-response').value = arguments[0];",
            token,
        )
        self.click("button[type='submit']")
        self.assert_element(".signup-success")

识别要尽量放晚。一个 reCAPTCHA token 大约只有两分钟有效期,所以在测试开头就取一个、走完另外四个步骤再用,这是一场你会时不时输掉的竞速。把识别紧挨在提交之前,重试也应该放在同一个位置。

把识别工具跑在服务器上

测试运行器很少就是你面前这台机器。CapSkip 用第二种连接模式覆盖了这种情况,而你代码里唯一要改的就是 host。

模式监听地址适用场景
本地127.0.0.1,仅限该设备测试和识别工具跑在同一台机器上
服务器你的内网地址或公网 IPCI 运行器、VPS 或者测试网格从外部调用
# pip install capskip
import os
from capskip import CapSkip

# The SDK reads these names from the environment too, so one test
# file runs unchanged on a laptop and on a shared runner.
solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", "8080")),
)

如果调用方位于你自己网络之外,建议使用静态公网 IP。Server 模式依然是你自己的硬件,依然不按量计费:它改变的只是识别工具运行的位置,而不是它归谁所有,也不改变每次识别的成本。完整的选项都在 连接设置.

常见错误

你所看到的原因修复
NetworkException那个 host 和端口上没有任何东西在监听启动 CapSkip,或者把 host 指向正在运行它的那台服务器
TimeoutException轮询超过了 recaptchaTimeout调高超时时间,或者确认 sitekey 是当前生效的那个
ERROR_GOOGLEKEYsitekey 根本没取到,或者格式有误在发送之前,先把 get_attribute 返回的内容打印出来
ERROR_PAGEURL页面 URL 缺失,或者不是一个完整的 URL发送浏览器当前所在的那个绝对 URL
表单拒绝了一个有效的 token页面用的是回调,而不是那个字段把 token 传给 data-callback 指定的函数

API 可能返回的每一个错误码都列在 CapSkip API 文档,并写明了各自的触发条件。

常见问题

UC 模式自己能识别 reCAPTCHA 吗?

不能。UC 模式让浏览器看起来不那么像自动化的,还能通过 GUI 辅助功能在 Cloudflare 的过渡页上点掉一个复选框。这两件事都不会产生 reCAPTCHA token。你仍然要从识别工具那里取到 token 并写进表单,就像上面演示的那样。两者一起跑:UC 模式降低你被挑战的频率,识别工具清掉你确实碰上的挑战。

我能在 CI 里以无头模式跑这套流程吗?

可以,因为识别过程根本不碰浏览器。给 SB 传 headless,或者在 pytest 命令行上加上对应的参数,那三个步骤一步都不用改。在无头运行器上活不下来的是 UC 模式的 GUI 点击,它要驱动真实的鼠标指针,需要一个桌面会话或者虚拟显示。

我的测试跑在 Linux 运行器上,识别工具该放在哪?

放在一台你自己掌控的 Windows 机器上,用 Server 模式访问它。CapSkip 是 Windows 应用,所以标准做法是在一台 Windows 机器上跑一个识别实例,任意数量的运行器通过 API 指向它。在运行器上设置 CAPSKIP_HOST,测试里其他任何东西都不用改。

为了让识别和我的浏览器保持一致,需要用代理吗?

有时候需要,而且 reCAPTCHA、Turnstile 和极验都支持。传入一个带类型和 URI 的代理字典,识别就会走这个代理;当站点在意 token 和会话是否来自同一个地址时,这一点很重要。图片验证码不接受代理,因为它们和网络路径没有任何绑定关系。

简短版结论

用 get_attribute 读出 sitekey,取到 token,把它写进隐藏的响应字段,然后提交。让识别和提交待在同一个步骤里,token 才是新鲜的;把 host 放进环境变量,测试套件在哪儿跑都是一样的。想看更宏观的 Selenium 全景,请看 Selenium 验证码识别页面;至于 v2 的具体细节,请看 reCAPTCHA v2 识别页面。剩下的就是成本,而这正是一个每次提交都要跑的测试套件最吃亏的地方: 本地验证码识别工具 意味着每一次识别都发生在你已经拥有的硬件上,不按量计费,无论测试触发得多频繁。