如何在 SeleniumBase 测试中识别验证码(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,仅限该设备 | 测试和识别工具跑在同一台机器上 |
| 服务器 | 你的内网地址或公网 IP | CI 运行器、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_GOOGLEKEY | sitekey 根本没取到,或者格式有误 | 在发送之前,先把 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 识别页面。剩下的就是成本,而这正是一个每次提交都要跑的测试套件最吃亏的地方: 本地验证码识别工具 意味着每一次识别都发生在你已经拥有的硬件上,不按量计费,无论测试触发得多频繁。
