Tutorials

处理单个页面上的多个验证码

同一页面出现两个甚至更多验证码时,正确做法是并行检测、并行提交,按 sitekey 回填 token——串行也能跑通,但耗时会翻倍。常见场景:登录 + 订阅复合表单,或海外电商分步注册(第一步 reCAPTCHA,第二步 Turnstile)。


检测与并行提交

先检测:页面上到底有几个验证码

用 CSS 选择器抓出所有 reCAPTCHA iframe,再从 src 里用正则提取 sitekey,不要假设页面只有一个。

Python(Selenium)

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/multi-captcha-page")

# Find all reCAPTCHA iframes
captcha_iframes = driver.find_elements(
    By.CSS_SELECTOR, 'iframe[src*="recaptcha/api2/anchor"]'
)

# Extract sitekeys from each iframe's src
import re
captchas = []
for i, iframe in enumerate(captcha_iframes):
    src = iframe.get_attribute("src")
    match = re.search(r"k=([A-Za-z0-9_-]+)", src)
    if match:
        captchas.append({
            "index": i,
            "sitekey": match.group(1),
            "iframe": iframe
        })

print(f"Found {len(captchas)} CAPTCHAs on page")
for c in captchas:
    print(f"  [{c['index']}] sitekey: {c['sitekey']}")

预期输出:

Found 2 CAPTCHAs on page
  [0] sitekey: 6LcXyzABCDEF-login
  [1] sitekey: 6LcAbcDEFGHI-signup

JavaScript(Puppeteer)

Puppeteer 直接在页面上下文查询 .g-recaptcha widget,一次拿到 sitekey、DOM id 和 callback 名称。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto('https://example.com/multi-captcha-page', {
    waitUntil: 'networkidle2'
  });

  // Extract all reCAPTCHA widgets from the page
  const captchas = await page.evaluate(() => {
    const widgets = document.querySelectorAll('.g-recaptcha');
    return Array.from(widgets).map((el, i) => ({
      index: i,
      sitekey: el.getAttribute('data-sitekey'),
      elementId: el.id || `captcha-${i}`,
      callbackName: el.getAttribute('data-callback') || null
    }));
  });

  console.log(`Found ${captchas.length} CAPTCHAs`);
  captchas.forEach(c => console.log(`  [${c.index}] ${c.sitekey}`));
})();

并行提交,同时轮询结果

每个验证码独立提交给 CaptchaAI,用线程池同时轮询——单个解决约 15-30 秒,并行处理两个和一个耗时基本相同。

Python

import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

API_KEY = "YOUR_API_KEY"
PAGE_URL = "https://example.com/multi-captcha-page"

def submit_captcha(sitekey):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": PAGE_URL,
        "json": "1"
    })
    result = resp.json()
    if result["status"] != 1:
        raise Exception(f"Submit error: {result['request']}")
    return result["request"]

def poll_result(task_id, timeout=120):
    deadline = time.time() + timeout
    while time.time() < deadline:
        time.sleep(5)
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": "1"
        })
        result = resp.json()
        if result["status"] == 1:
            return result["request"]
        if result["request"] != "CAPCHA_NOT_READY":
            raise Exception(f"Poll error: {result['request']}")
    raise TimeoutError(f"Task {task_id} timed out")

def solve_all(captchas):
    # Submit all in parallel
    task_ids = {}
    with ThreadPoolExecutor(max_workers=len(captchas)) as pool:
        futures = {
            pool.submit(submit_captcha, c["sitekey"]): c["index"]
            for c in captchas
        }
        for future in as_completed(futures):
            idx = futures[future]
            task_ids[idx] = future.result()
            print(f"[captcha-{idx}] Submitted → task {task_ids[idx]}")

    # Poll all in parallel
    tokens = {}
    with ThreadPoolExecutor(max_workers=len(task_ids)) as pool:
        futures = {
            pool.submit(poll_result, tid): idx
            for idx, tid in task_ids.items()
        }
        for future in as_completed(futures):
            idx = futures[future]
            tokens[idx] = future.result()
            print(f"[captcha-{idx}] Solved")

    return tokens

# Example usage
captchas = [
    {"index": 0, "sitekey": "6LcXyzABCDEF-login"},
    {"index": 1, "sitekey": "6LcAbcDEFGHI-signup"}
]

tokens = solve_all(captchas)

预期输出:

[captcha-0] Submitted → task 71823456
[captcha-1] Submitted → task 71823457
[captcha-1] Solved
[captcha-0] Solved

把 token 精确回填到验证码控件

每个 reCAPTCHA 小部件都有独立的 g-recaptcha-response 文本域——按 sitekey 回填,不要按出现顺序猜测,异步加载时顺序不可靠。

Python(Selenium)

def inject_tokens(driver, captchas, tokens):
    for c in captchas:
        idx = c["index"]
        token = tokens[idx]

        # Find the textarea within the widget's container
        container = driver.find_elements(By.CSS_SELECTOR, ".g-recaptcha")[idx]
        textarea = container.find_element(
            By.CSS_SELECTOR, 'textarea[name="g-recaptcha-response"]'
        )
        driver.execute_script(
            "arguments[0].value = arguments[1];", textarea, token
        )

        # Trigger the callback if defined
        callback = c.get("callback")
        if callback:
            driver.execute_script(f"{callback}('{token}');")

        print(f"[captcha-{idx}] Token injected")

inject_tokens(driver, captchas, tokens)

JavaScript(Puppeteer)

async function injectTokens(page, captchas, tokens) {
  for (const captcha of captchas) {
    const token = tokens[captcha.index];

    await page.evaluate((idx, tkn, callbackName) => {
      const widgets = document.querySelectorAll('.g-recaptcha');
      const textarea = widgets[idx].querySelector(
        'textarea[name="g-recaptcha-response"]'
      );
      textarea.value = tkn;

      if (callbackName && typeof window[callbackName] === 'function') {
        window[callbackName](tkn);
      }
    }, captcha.index, token, captcha.callbackName);

    console.log(`[captcha-${captcha.index}] Token injected`);
  }
}

await injectTokens(page, captchas, tokens);

reCAPTCHA 与 Turnstile 混合出现时怎么处理

有些页面前后两步验证码类型不同——比如海外电商结账页,第一步 reCAPTCHA,第二步换成 Turnstile。分别检测每种类型,匹配正确的 method 参数:

def detect_all_captchas(driver):
    detected = []

    # reCAPTCHA
    recaptchas = driver.find_elements(By.CSS_SELECTOR, ".g-recaptcha")
    for i, el in enumerate(recaptchas):
        detected.append({
            "type": "userrecaptcha",
            "sitekey": el.get_attribute("data-sitekey"),
            "label": f"recaptcha-{i}"
        })

    # Turnstile
    turnstiles = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
    for i, el in enumerate(turnstiles):
        detected.append({
            "type": "turnstile",
            "sitekey": el.get_attribute("data-sitekey"),
            "label": f"turnstile-{i}"
        })

    return detected
  • reCAPTCHA 提交时 methoduserrecaptcha
  • Turnstile 提交时 methodturnstile,两种类型不要混用同一个 method

常见问题与故障排查

遇到报错先查这张表,多数问题都能在这里定位:

问题 原因 处理
token 已回填,表单还提交不了 未触发 callback 检查 data-callback
只检测到第一个验证码 第二个延迟加载 等 widget 出现后再扫描
token 塞进错误控件 按位置索引而非 sitekey 用 sitekey 做映射键
ERROR_WRONG_GOOGLEKEY sitekey 提取错了 核对 srcdata-sitekey

页面出现多个验证码,是不是出问题了?

不是,这是常见设计——登录 + 订阅复合表单、分步注册流程都会让每一步触发一次验证,按独立任务处理即可。

reCAPTCHA 和 Turnstile 混合出现时,提交参数要分开传吗?

要分开。reCAPTCHA 用 userrecaptcha,Turnstile 用 turnstile,混用会导致其中一个提交失败。

同时处理多个验证码要按数量付费吗?

不需要。CaptchaAI 按并发线程数计费,不按数量计费。BASIC($15/月,5 线程)能同时提交 5 个验证码,线程内不限次数,超出部分排队,不加收费用。

第二个验证码提交后才渲染,怎么抓取?

每次表单提交后重新扫描页面,用 WebDriverWait(Selenium)或 page.waitForSelector(Puppeteer)等待新控件,不要只在加载时检测一次。

每个验证码的 sitekey 都需要单独提取吗?

需要。不同验证码通常各自使用独立的 sitekey,即使类型相同,也要按控件单独提取,不要假设共用一个。


下一步

注册 CaptchaAI 获取 API Key,几行代码即可把检测、并行提交和 token 回填接入现有脚本。

该文章已禁用评论。