reCAPTCHA v2 的 token 在 API 里单独测试没问题,一放进 Selenium/Playwright 浏览器就报错——多半不是 token 坏,而是回调没触发、提交慢导致过期,或 pageurl 对不上。下面按出现概率排序,五类修复均附可复用代码。
国内团队做海外站点自动化,常年打交道的是 GeeTest(极验),一换成 reCAPTCHA 或 Turnstile 就容易栽在注入细节上。本文以 reCAPTCHA v2 为主,Turnstile 见修复 5。
token 在 API 里能用,说明 CaptchaAI 那端没问题,故障一定出在浏览器注入或页面环境里。
快速自查表:症状对应哪类原因
| 症状 | 根本原因 | 处理方式 |
|---|---|---|
| 表单拒绝提交 | 选择器错或回调未触发 | 见修复 1 |
| 提交按钮禁用 | 回调负责启用它 | 见修复 1 |
| "验证码无效" | token 已过期 | 见修复 2 |
| 提交后返回 403 | 机器人特征识别 | 见修复 4 |
| API 能用浏览器失败 | pageurl 不一致 | 见修复 3 |
修复 1:正确写入 token 并触发回调
只把 token 写进 g-recaptcha-response 的 textarea 还不够,回调函数也必须调用一次表单才会认可:
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
def inject_recaptcha_token(driver, token):
"""Properly inject reCAPTCHA v2 token in browser."""
# Step 1: Make the response textarea visible and set the value
driver.execute_script("""
// Find all response textareas (may be multiple on page)
var textareas = document.querySelectorAll('[name="g-recaptcha-response"]');
textareas.forEach(function(ta) {
ta.style.display = 'block';
ta.value = arguments[0];
});
// Also set via ID if present
var byId = document.getElementById('g-recaptcha-response');
if (byId) {
byId.style.display = 'block';
byId.value = arguments[0];
}
""", token)
# Step 2: Trigger the callback
driver.execute_script("""
// Try standard callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
var clients = ___grecaptcha_cfg.clients;
for (var key in clients) {
var client = clients[key];
// Navigate nested structure to find callback
for (var prop in client) {
var val = client[prop];
if (val && typeof val === 'object') {
for (var subprop in val) {
var subval = val[subprop];
if (subval && typeof subval === 'object' && subval.callback) {
subval.callback(arguments[0]);
return;
}
}
}
}
}
}
// Fallback: try common callback names
if (typeof onCaptchaSuccess === 'function') onCaptchaSuccess(arguments[0]);
else if (typeof captchaCallback === 'function') captchaCallback(arguments[0]);
""", token)
# Usage
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# ... solve CAPTCHA via CaptchaAI ...
inject_recaptcha_token(driver, token)
time.sleep(1)
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
回调不会自动触发,不少表单靠它启用按钮或填充隐藏字段,下面依次尝试两种方式找到并调用:
def find_and_trigger_callback(driver, token):
"""Find the reCAPTCHA callback and trigger it."""
# Method 1: Extract callback from data attribute
callback_name = driver.execute_script("""
var widget = document.querySelector('.g-recaptcha');
if (widget) {
return widget.getAttribute('data-callback');
}
return null;
""")
if callback_name:
driver.execute_script(f"window['{callback_name}'](arguments[0]);", token)
return True
# Method 2: Extract from grecaptcha config
triggered = driver.execute_script("""
try {
var widgetId = 0;
var callback = ___grecaptcha_cfg.clients[widgetId].aa.l.callback;
if (typeof callback === 'function') {
callback(arguments[0]);
return true;
}
} catch(e) {}
return false;
""", token)
return triggered
修复 2:token 有效期很短,解决后要立刻用
reCAPTCHA v2 的 token 通常几分钟内过期,轮询拿到结果后:
- 立即注入,不要插入额外等待
- 表单在 0.5 秒内提交
import requests
import time
def solve_and_inject_fast(driver, api_key, sitekey, page_url):
"""Solve CAPTCHA and inject token with minimal delay."""
# Submit solve request
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": 1,
}, timeout=30)
task_id = resp.json()["request"]
# Poll for result
time.sleep(15)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
token = data["request"]
# IMMEDIATELY inject — don't wait
inject_recaptcha_token(driver, token)
# Submit form within 5 seconds
time.sleep(0.5)
return True
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("Solve timeout")
修复 3:SPA 和 iframe 场景下,pageurl 该取哪个
reCAPTCHA 常渲染在 iframe 里,pageurl 要点:
- 别用最初打开的地址——页面一旦跳转或重定向就对不上了
- 统一用
driver.current_url,token 才不会因地址不符而失效
def get_correct_pageurl(driver):
"""Get the URL that reCAPTCHA actually sees."""
# Check if CAPTCHA is in an iframe
iframes = driver.find_elements(By.TAG_NAME, "iframe")
for iframe in iframes:
src = iframe.get_attribute("src") or ""
if "recaptcha" in src or "anchor" in src:
# CAPTCHA is in iframe — use parent page URL
return driver.current_url
# For SPAs, use current URL (may differ from initial load)
return driver.current_url
# WRONG — using the URL you navigated to
pageurl = "https://example.com/form" # May have redirected
# CORRECT — using the URL the browser is actually on
pageurl = get_correct_pageurl(driver)
修复 4:token 通过了,网站却还是拦下浏览器
拿到有效 token 不代表万事大吉,部分站点会在校验通过后单独拦截自动化特征,以下是标准 QA 配置:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
def create_stealth_driver():
"""Create a browser that avoids basic bot detection."""
options = Options()
# Remove automation indicators
options.add_argument("--no-sandbox")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
# Set realistic window size
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
# Override navigator.userAgent
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
return driver
driver = create_stealth_driver()
实测要点:
- Chrome/Chromium 兼容性最好
- Firefox 能跑通,但 CDP 方法要单独适配
修复 5:Turnstile 的 token 提交方式不一样
Cloudflare Turnstile 用的字段名和触发方式跟 reCAPTCHA 不同,注入代码要相应调整:
def inject_turnstile_token(driver, token):
"""Inject Turnstile token into the page."""
driver.execute_script("""
// Find Turnstile response input
var inputs = document.querySelectorAll(
'input[name="cf-turnstile-response"], ' +
'input[name="g-recaptcha-response"]'
);
inputs.forEach(function(input) {
input.value = arguments[0];
});
// Trigger change event
inputs.forEach(function(input) {
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('input', { bubbles: true }));
});
// Try Turnstile callback
if (window.turnstile) {
var widgets = document.querySelectorAll('[data-callback]');
widgets.forEach(function(w) {
var cb = w.getAttribute('data-callback');
if (typeof window[cb] === 'function') {
window[cb](arguments[0]);
}
});
}
""", token)
调试清单:三个字段定位问题出在哪一步
先跑一遍下面这段脚本,重点看三个字段,比逐行猜测快:
textarea_value_set—— token 是否写进去了callback_name—— 回调找不找得到current_url—— 是否与传给 API 的 pageurl 一致
def debug_captcha_injection(driver, token):
"""Print debug info to diagnose injection failures."""
info = driver.execute_script("""
var result = {};
// Check textarea exists
var ta = document.querySelector('[name="g-recaptcha-response"]');
result.textarea_found = !!ta;
result.textarea_value_set = ta ? ta.value.length > 0 : false;
// Check reCAPTCHA loaded
result.grecaptcha_loaded = typeof grecaptcha !== 'undefined';
// Check for callback
var widget = document.querySelector('.g-recaptcha');
result.has_data_callback = widget ? !!widget.getAttribute('data-callback') : false;
result.callback_name = widget ? widget.getAttribute('data-callback') : null;
// Current URL
result.current_url = window.location.href;
// Check for errors in console (basic)
result.has_submit_button = !!document.querySelector('button[type=submit], input[type=submit]');
return result;
""")
for key, value in info.items():
print(f" {key}: {value}")
return info
常见问题
API 请求里 token 正常,浏览器里为什么不通过?
多半回调没触发——表单靠回调启用按钮或填充隐藏字段,只写 textarea 不够,见修复 1。
回调函数一直找不到,怎么排查?
先看 .g-recaptcha 的 data-callback,找不到再遍历 ___grecaptcha_cfg.clients,调试脚本会打印。
国内环境测试 reCAPTCHA 经常超时,是 token 出问题了吗?
大概率不是。reCAPTCHA 依赖 Google 托管脚本,国内网络本身不稳定,表现成"卡住",不代表 token 有问题。
要不要用 headless 模式?
部分站点识别 headless 特征并拒绝已通过校验的 token,建议先 headful 跑通再切换。