Tutoriales

Manejo de múltiples CAPTCHA en una sola página

¿Tu script se atasca en una página que muestra dos o tres CAPTCHA a la vez? La receta es tratar cada widget como un desafío independiente: detectas todos los sitekeys, los resuelves en paralelo e inyectas cada token en su callback.

Resolverlos de uno en uno solo multiplica el tiempo de espera sin ningún beneficio.

Es un caso más frecuente de lo que parece: un portal de trámites con reCAPTCHA en el login y Turnstile en el formulario de contacto, una tienda con un CAPTCHA en el alta de cuenta y otro en la suscripción al boletín, o un alta en varios pasos donde cada paso lanza su propio desafío. La estrategia siempre es la misma; lo que cambia es el número de widgets.

En la práctica, el proceso se reduce a tres pasos que repites por cada widget de la página:

  1. Detecta cada CAPTCHA y extrae su sitekey.
  2. Resuelve todos en paralelo con la API de CaptchaAI.
  3. Inyecta cada token en el widget correcto y dispara su callback.

Cómo detectar todos los CAPTCHA de la página

Enumera cada widget y extrae su sitekey antes de enviar nada. No asumas que la página tiene uno solo: recórrela entera primero. De cada CAPTCHA necesitas tres datos:

  • El sitekey, que identifica el widget ante el servicio de resolución.
  • El tipo (reCAPTCHA, Turnstile…), que decide qué parámetro method usarás al enviarlo.
  • El elemento de destino, donde inyectarás el token una vez resuelto.

Detección con Python y 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']}")

Resultado esperado:

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

Detección con JavaScript y Puppeteer

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}`));
})();

Resolver todos los CAPTCHA en paralelo

Envía todos los CAPTCHA a CaptchaAI a la vez y luego sondea cada tarea. Con hilos de trabajo, el envío y el sondeo ocurren en paralelo: dos o tres desafíos tardan casi lo mismo que uno. El flujo tiene dos fases claras:

  • Envío: mandas cada sitekey y guardas el ID de tarea que devuelve el servicio.
  • Sondeo: consultas cada ID hasta que su tarea entrega el token resuelto.

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)

Resultado esperado:

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

Fíjate en el orden de salida: la segunda tarea termina antes que la primera, porque cada hilo entrega su resultado en cuanto está listo, sin bloquear al resto del lote.


Inyectar cada token en su widget

Cada widget reCAPTCHA tiene su propia área de texto g-recaptcha-response anidada en su contenedor. El error más habitual al cierre del flujo es meter el token en el widget equivocado, así que ten presentes dos detalles:

  • Localiza el textarea dentro del contenedor de cada widget, no en toda la página.
  • Asigna los tokens por sitekey, nunca por posición a ciegas.

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);

Rellenar el textarea no siempre basta: muchos formularios validan mediante el callback que reCAPTCHA dispara al resolverse. Si el widget define un data-callback, invócalo con el token para desbloquear el envío.


Páginas con tipos de CAPTCHA mezclados

Cuando la página combina tecnologías distintas —reCAPTCHA en el login y Cloudflare Turnstile en otro formulario, por ejemplo— detecta cada tipo por separado antes de enviarlo:

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

Envía cada tipo con su parámetro method correspondiente:

  • userrecaptcha para los widgets reCAPTCHA.
  • turnstile para los de Cloudflare Turnstile.

El resto del pipeline —envío, sondeo e inyección— es idéntico; lo único que cambia es el widget de destino del token.


Rendimiento y consumo de threads

Aquí es donde el modelo de CaptchaAI ayuda: la facturación es por thread concurrente, no por resolución, y cada plan incluye resoluciones ilimitadas por thread.

Cada CAPTCHA que tienes en vuelo ocupa un thread hasta que se resuelve, así que tu paralelismo real lo marca el número de threads de tu plan.

  • BASIC ($15/mes, 5 threads): resuelve hasta cinco CAPTCHA a la vez, de sobra para una página con dos o tres widgets.
  • STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads): más holgura cuando automatizas muchas páginas en paralelo.

No hay recargos por tipo de CAPTCHA ni límites diarios: el coste mensual en USD es fijo y predecible, algo cómodo para agencias y freelancers que facturan en monedas locales más volátiles.


Buenas prácticas para páginas con varios CAPTCHA

  • Extrae el sitekey por widget; nunca asumas que la página comparte uno solo.
  • Espera a que todos los iframes y widgets terminen de cargar antes de escanear.
  • Asigna cada token a su sitekey, no a su índice posicional.
  • Invoca el data-callback de cada widget después de inyectar el token.
  • Vuelve a escanear la página tras cada envío por si aparecen desafíos nuevos.

Cuándo vuelven a aparecer los CAPTCHA en formularios de varios pasos

En un alta de varios pasos, el segundo o el tercer CAPTCHA a veces solo se renderiza después de enviar el paso anterior.

Trata cada paso como una página nueva: vuelve a escanear con WebDriverWait (Selenium) o page.waitForSelector (Puppeteer), detecta el widget recién aparecido y repite el ciclo de resolver e inyectar antes de continuar.


Preguntas frecuentes

¿Puedo reutilizar el mismo token en varios widgets?

No. Cada token queda ligado al sitekey y a la sesión del widget que lo generó; si lo reutilizas en otro, la validación lo rechaza.

Resuelve siempre un token independiente por cada widget de la página.

¿Puedo mezclar reCAPTCHA y Turnstile en el mismo lote paralelo?

Sí. La cola no distingue el tipo: envías cada CAPTCHA con su method y sondeas todas las tareas por igual. Solo guarda qué tipo va con cada tarea para inyectar bien el token.

¿Qué significa CAPCHA_NOT_READY al sondear?

Que la tarea sigue resolviéndose. Es una respuesta normal, no un error: espera unos segundos y vuelve a consultar. Lanza una excepción solo cuando llega otro código distinto o se agota tu timeout.

¿Y si la página usa un reCAPTCHA invisible junto a uno visible?

Se resuelven igual. Un reCAPTCHA v2 invisible comparte el mismo flujo de sitekey y g-recaptcha-response; solo recuerda que suele activarse mediante callback, así que invócalo tras inyectar el token.


Resolución de problemas

Cuando el flujo falla, casi siempre es una de estas cuatro causas:

Problema Causa Solución
Token inyectado pero el formulario sigue bloqueado No se disparó el callback Revisa data-callback y llámalo con el token
Solo se detecta el primer CAPTCHA El segundo carga de forma diferida Espera a que aparezcan todos los iframes o widgets antes de escanear
Token equivocado en el widget equivocado Desajuste de índices Asigna los tokens por sitekey, no por posición
ERROR_WRONG_GOOGLEKEY Sitekey extraída de forma incorrecta Verifica la clave desde el atributo src del iframe o desde data-sitekey

Guías relacionadas


Resuelve cualquier cantidad de CAPTCHA por página con CaptchaAI

Crea tu cuenta y obtén tu API key en captchaai.com.

Los comentarios están deshabilitados para este artículo.