用 Selenium 跑自动化表单提交时,验证码是绕不开的一道关卡:联系表单、申请表、注册页只要挂了 reCAPTCHA、Turnstile 或图片验证码,脚本填完字段也提交不出去。思路并不复杂——先识别页面上是哪种验证码,调用 CaptchaAI 拿到 token,再写回表单完成提交,下面是一套可以直接复用的实现。
什么场景会用到这套流程
跑 QA 回归、批量提交测试申请表、验证联系表单能否正常收信,只要目标页面带验证码,脚本一到提交就卡住。国内站点常见极验(GeeTest)滑块,CaptchaAI 对接的是 GeeTest v3;测试海外 SaaS 表单则更常撞上 reCAPTCHA v2 或 Turnstile,这也是下面示例优先覆盖它们的原因。思路都一样:探测 → 识别 → 回填 → 提交。
整体架构
四步走完一次提交:加载页面、填字段、探测并识别验证码、点击提交。
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Load Form │────▶│ Fill Fields │────▶│ Detect & │────▶│ Submit Form │
│ (Selenium) │ │ │ │ Solve │ │ │
│ │ │ │ │ CAPTCHA │ │ │
└────────────┘ └──────────────┘ └────────────┘ └──────────────┘
三个类各管一段:FormCaptchaSolver 对话 CaptchaAI API,CaptchaDetector 判断验证码类型,FormAutomator 负责填字段和提交。
三个核心组件
验证码识别器
封装 in.php 提交任务、res.php 轮询结果,首次查询前先等 10 秒,太快查会白白占用线程。
import time
import requests
class FormCaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(f"Submit error: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(f"Solve error: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out")
验证码探测器
填表单前先扫一遍页面:有没有 .cf-turnstile、[data-sitekey]、验证码图片元素,判断类型和 sitekey 或图片地址。
import re
from selenium.webdriver.common.by import By
class CaptchaDetector:
def __init__(self, driver):
self.driver = driver
def detect(self):
"""Detect CAPTCHA type on current page."""
html = self.driver.page_source
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
for el in turnstile:
if "cf-turnstile" in (el.get_attribute("class") or ""):
return "turnstile", el.get_attribute("data-sitekey")
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha:
sitekey = recaptcha[0].get_attribute("data-sitekey")
if "recaptcha" in html.lower():
return "recaptcha_v2", sitekey
# Image CAPTCHA
img = self.driver.find_elements(By.CSS_SELECTOR, "img[src*='captcha'], img.captcha")
if img:
return "image", img[0].get_attribute("src")
return "none", None
表单自动化控制器
命中 reCAPTCHA 走 userrecaptcha,命中 Turnstile 走 turnstile,图片验证码下载转 base64 后走 base64 方法,拿到 token 用 execute_script 写入隐藏字段。
import base64
import requests as req
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class FormAutomator:
def __init__(self, api_key):
self.solver = FormCaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.detector = CaptchaDetector(self.driver)
def fill_field(self, selector, value):
field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
field.clear()
field.send_keys(value)
def select_option(self, selector, value):
from selenium.webdriver.support.ui import Select
select = Select(self.driver.find_element(By.CSS_SELECTOR, selector))
select.select_by_value(value)
def solve_captcha(self):
captcha_type, data = self.detector.detect()
page_url = self.driver.current_url
if captcha_type == "recaptcha_v2":
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return True
if captcha_type == "turnstile":
token = self.solver.solve({
"method": "turnstile",
"sitekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return True
if captcha_type == "image":
img_data = req.get(data).content
img_b64 = base64.b64encode(img_data).decode()
text = self.solver.solve({"method": "base64", "body": img_b64})
captcha_input = self.driver.find_element(
By.CSS_SELECTOR, "input[name*='captcha']"
)
captcha_input.clear()
captcha_input.send_keys(text)
return True
return False # No CAPTCHA detected
def submit_form(self, url, fields, submit_selector="button[type='submit']"):
"""
fields: list of (selector, value) tuples
"""
self.driver.get(url)
for selector, value in fields:
self.fill_field(selector, value)
self.solve_captcha()
submit = self.driver.find_element(By.CSS_SELECTOR, submit_selector)
submit.click()
return self.driver.current_url
def close(self):
self.driver.quit()
完整示例:联系表单
三个类拼起来之后,调用方只需给出目标页面和字段值,识别验证码这一步在 submit_form() 内部自动处理:
automator = FormAutomator("YOUR_API_KEY")
try:
result_url = automator.submit_form(
url="https://example.com/contact",
fields=[
("#name", "John Doe"),
("#email", "[email protected]"),
("#subject", "Sales inquiry"),
("#message", "I'd like to learn more about your services."),
],
submit_selector="#submit-btn",
)
print(f"Form submitted. Redirected to: {result_url}")
finally:
automator.close()
处理多种表单类型
同一套 FormAutomator,换个 url 和 fields 就能覆盖登录、注册、搜索这几类表单。
登录表单
result = automator.submit_form(
url="https://staging.example.com/qa-login",
fields=[
("#username", "testuser"),
("#password", "testpass123"),
],
submit_selector="#login-btn",
)
注册表单
字段更多,流程完全一样:
result = automator.submit_form(
url="https://example.com/register",
fields=[
("#first-name", "Jane"),
("#last-name", "Smith"),
("#email", "[email protected]"),
("#password", "SecurePass!123"),
("#confirm-password", "SecurePass!123"),
],
submit_selector="#register-btn",
)
带验证码的搜索表单
有些站点只在触发风控时才弹验证码,探测不到就直接跳过:
result = automator.submit_form(
url="https://example.com/search",
fields=[
("#query", "python developer"),
("#location", "San Francisco"),
],
submit_selector="#search-btn",
)
两个容易漏掉的情况
- 验证失败后才出现的验证码:每次提交后都重新调用
solve_captcha(),别只在最开始探测一次。 - AJAX 表单:
execute_script写隐藏字段未必生效,改成拦截请求、把 token 塞进负载里更可靠。
故障排查
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 令牌被拒绝 | token 在提交前已过期 | 最后一步再识别验证码,识别完立即提交 |
| 找不到表单字段 | 页面动态加载,元素还没渲染出来 | 用 WebDriverWait 加显式等待,不要用固定 sleep |
| 探测到错误的验证码类型 | 页面上同时存在多个验证码相关元素 | 检查 detect() 里的判断顺序,精确匹配放前面 |
| 提交后表单又刷新回来 | 服务器端校验失败,不一定是验证码问题 | 先查必填字段和格式,再确认 token 是否正确回填 |
| reCAPTCHA 写入了却没生效 | 站点用了回调而不是读取隐藏字段 | 写入 token 后手动触发 grecaptcha.execute() |
目标页面不是自己站点时,先确认已获授权,只在自有或已授权的 staging/QA 环境里跑这套脚本。
常见问题
不开浏览器,能直接提交带验证码的表单吗?
reCAPTCHA 和 Turnstile 可以:拿到 token 后直接 HTTP POST 提交即可。但表单依赖前端 JavaScript 校验或动态渲染时,还是得走 Selenium。
表单一直被拒绝,是不是 token 的问题?
先看时效性:token 通常一两分钟内失效,把“识别—提交”间隔压到最短,中间别插入额外请求或等待。
GeeTest 的表单能接进这套流程吗?
可以,在 CaptchaDetector 里加一段 GeeTest 元素判断,method 换成 geetest 即可。CaptchaAI 目前支持 GeeTest v3,v4 官方标注为即将支持,暂时不能当作已支持来用。
批量跑几十个表单会被限流吗?
限流看套餐线程数,不是表单数量。BASIC($15/月,5 线程)线程内可无限次识别,超出并发就升级套餐或排队处理。
CaptchaAI 能识别 hCaptcha 吗?
暂不支持,FunCaptcha(Arkose Labs)同样不支持。目前覆盖 reCAPTCHA v2/v3、Turnstile 与 Cloudflare Challenge、GeeTest v3、图片/OCR、九宫格及 BLS。
相关指南
把表单提交这一步彻底自动化——用 CaptchaAI 识别验证码。