Troubleshooting

浏览器自动化验证码失败但 API 有效:调试指南

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 要点:

  1. 别用最初打开的地址——页面一旦跳转或重定向就对不上了
  2. 统一用 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)

调试清单:三个字段定位问题出在哪一步

先跑一遍下面这段脚本,重点看三个字段,比逐行猜测快:

  1. textarea_value_set —— token 是否写进去了
  2. callback_name —— 回调找不找得到
  3. 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-recaptchadata-callback,找不到再遍历 ___grecaptcha_cfg.clients,调试脚本会打印。

国内环境测试 reCAPTCHA 经常超时,是 token 出问题了吗?

大概率不是。reCAPTCHA 依赖 Google 托管脚本,国内网络本身不稳定,表现成"卡住",不代表 token 有问题。

要不要用 headless 模式?

部分站点识别 headless 特征并拒绝已通过校验的 token,建议先 headful 跑通再切换。


相关指南



后续阅读

该文章已禁用评论。