Tutoriales

Carga dinámica de CAPTCHA: detección de CAPTCHA cargados de forma diferida

Tu automatización lee el HTML nada más cargar la página y no encuentra ningún CAPTCHA, pero en el navegador ahí está, visible. No es un fallo de tu selector: el sitio inyecta el CAPTCHA de forma diferida (lazy load) y solo aparece tras un clic, al enfocar un campo, al hacer scroll o al cumplirse un temporizador. La solución: no captures el DOM de inmediato; observa los cambios y espera a que el elemento se monte. Esta guía lo cubre con Puppeteer, Selenium y CaptchaAI.


Qué eventos disparan un CAPTCHA diferido

Disparador Ejemplo Cómo dispararlo
Clic en botón "Enviar" agrega reCAPTCHA al formulario Haz clic en el botón primero
Foco en formulario El CAPTCHA aparece al enfocar el input Enfoca el campo email/password
Posición de scroll El CAPTCHA se carga cuando la sección es visible Haz scroll hasta el formulario
Temporizador El CAPTCHA se carga después de 3 segundos Espera el retraso
Condición JavaScript El CAPTCHA se carga tras una respuesta AJAX Activa la solicitud requerida

Es habitual en portales de cita previa y en marketplaces tipo MercadoLibre o Amazon.es: el reCAPTCHA solo se monta al pulsar "Reservar" o al enfocar el formulario. Si automatizas la QA de ese flujo, reproduce esa interacción antes de buscar el desafío.


Cómo confirmar que el CAPTCHA se carga de forma diferida

Antes de escribir código, verifica el comportamiento a mano y así identificas el disparador exacto en lugar de esperar a ciegas:

  1. Abre el código fuente de la página (Ctrl+U): si no ves el div ni el script del CAPTCHA, pero ambos aparecen al interactuar con la página, es carga diferida.
  2. Recarga con las herramientas de desarrollo abiertas y observa la pestaña de red: el script de reCAPTCHA o Turnstile se solicita más tarde, no en la carga inicial.
  3. Repite la interacción que sospechas (clic, foco o scroll) y anota en qué momento se monta el elemento.

Con el disparador claro, elige el método de detección que mejor encaje: observar el DOM, esperar al script o interceptar el render.


Método 1: observar el DOM con MutationObserver

MutationObserver avisa en cuanto el CAPTCHA entra en el DOM. Configúralo antes de disparar la carga:

Puppeteer

Arma el observer y luego dispara la carga; este es el patrón base:

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://staging.example.com/qa-login');

// Set up MutationObserver before triggering the CAPTCHA
const captchaInfo = await page.evaluate(() => {
  return new Promise((resolve) => {
    // Check if already present
    const existing = document.querySelector('.g-recaptcha, .cf-turnstile, .h-captcha');
    if (existing) {
      resolve({
        type: existing.className,
        sitekey: existing.getAttribute('data-sitekey'),
      });
      return;
    }

    // Watch for new elements
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;

          const captcha = node.matches?.('.g-recaptcha, .cf-turnstile, .h-captcha')
            ? node
            : node.querySelector?.('.g-recaptcha, .cf-turnstile, .h-captcha');

          if (captcha) {
            observer.disconnect();
            resolve({
              type: captcha.className,
              sitekey: captcha.getAttribute('data-sitekey'),
            });
            return;
          }
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Timeout after 30 seconds
    setTimeout(() => {
      observer.disconnect();
      resolve(null);
    }, 30000);
  });
});

console.log('Detected CAPTCHA:', captchaInfo);

Disparar la carga del CAPTCHA

Con el observer ya armado, reproduce la interacción que lo monta:

// Click the submit button to trigger CAPTCHA
await page.click('#submit-btn');

// Or focus the input
await page.focus('#email');

// Or scroll to the form
await page.evaluate(() => {
  document.querySelector('#signup-form').scrollIntoView();
});

Método 2: esperar a que se inyecte el script

El CAPTCHA necesita su biblioteca JavaScript antes de renderizarse. Esperar al objeto global suele ser más limpio que vigilar el DOM:

// Wait for reCAPTCHA script to load
await page.waitForFunction(() => {
  return typeof window.grecaptcha !== 'undefined' 
    && typeof window.grecaptcha.render === 'function';
}, { timeout: 30000 });

// Now extract parameters
const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.g-recaptcha');
  return el?.getAttribute('data-sitekey');
});

Para Cloudflare Turnstile

El mismo patrón sirve para Turnstile; solo cambian el objeto global y el selector:

await page.waitForFunction(() => {
  return typeof window.turnstile !== 'undefined';
}, { timeout: 30000 });

const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.cf-turnstile');
  return el?.getAttribute('data-sitekey');
});

Método 3: interceptar la llamada de renderizado

Para obtener los parámetros exactos del render (sitekey, callback, contenedor), engancha la biblioteca antes de que se ejecute la página:

// Inject before page scripts run
await page.evaluateOnNewDocument(() => {
  window.__captchaDetected = null;

  // Hook grecaptcha.render
  let _grecaptcha;
  Object.defineProperty(window, 'grecaptcha', {
    set(val) {
      _grecaptcha = val;
      const origRender = val.render;
      val.render = function(container, params) {
        window.__captchaDetected = {
          type: 'recaptcha',
          sitekey: params.sitekey,
          callback: params.callback?.name,
          container: typeof container === 'string' ? container : container.id,
        };
        return origRender.apply(this, arguments);
      };
    },
    get() { return _grecaptcha; },
  });
});

await page.goto('https://example.com/signup');

// Trigger the CAPTCHA (click, scroll, etc.)
await page.click('#show-form');

// Wait for detection
await page.waitForFunction(() => window.__captchaDetected !== null, {
  timeout: 30000,
});

const detected = await page.evaluate(() => window.__captchaDetected);
console.log('Detected:', detected);
// { type: 'recaptcha', sitekey: '6Le-wvkS...', callback: 'onCaptcha', container: 'recaptcha-box' }

Python con Selenium: esperar a CAPTCHAs diferidos

En Python con Selenium el patrón es el mismo: dispara la interacción y usa WebDriverWait hasta que el elemento esté presente.

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

driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Trigger the CAPTCHA loading
submit = driver.find_element(By.ID, "submit-btn")
submit.click()

# Wait for CAPTCHA to appear
try:
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )
    sitekey = captcha_el.get_attribute("data-sitekey")
    captcha_class = captcha_el.get_attribute("class")

    if "g-recaptcha" in captcha_class:
        captcha_type = "recaptcha"
    elif "cf-turnstile" in captcha_class:
        captcha_type = "turnstile"
    else:
        captcha_type = "hcaptcha"

    print(f"Type: {captcha_type}, Sitekey: {sitekey}")
except Exception:
    print("No CAPTCHA appeared within 30 seconds")

Esperar el iframe (reCAPTCHA)

El div puede existir mucho antes de que el script termine de cargar. Para reCAPTCHA conviene esperar al iframe, no solo al contenedor:

# reCAPTCHA loads an iframe even when the div exists but the script is still loading
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((
        By.CSS_SELECTOR,
        "iframe[src*='recaptcha'], iframe[src*='challenges.cloudflare.com']"
    ))
)
print("CAPTCHA iframe loaded")

Flujo completo: detectar, resolver e inyectar el token

Todo junto: dispara el CAPTCHA, espera a que aparezca, detecta el tipo, lo resuelve con CaptchaAI e inyecta el token.

import requests
import time

def detect_and_solve(driver, api_key, trigger_action=None):
    """Detect a lazy-loaded CAPTCHA, solve it, and inject the token."""

    # 1. Trigger the CAPTCHA
    if trigger_action:
        trigger_action(driver)

    # 2. Wait for it to appear
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )

    sitekey = captcha_el.get_attribute("data-sitekey")
    page_url = driver.current_url
    captcha_class = captcha_el.get_attribute("class")

    # 3. Determine type and method
    if "g-recaptcha" in captcha_class:
        method, key_param, token_field = "userrecaptcha", "googlekey", "g-recaptcha-response"
    elif "cf-turnstile" in captcha_class:
        method, key_param, token_field = "turnstile", "sitekey", "cf-turnstile-response"
    else:
        method, key_param, token_field = "hcaptcha", "sitekey", "h-captcha-response"

    # 4. Solve con CaptchaAI
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": method,
        key_param: sitekey, "pageurl": page_url, "json": "1",
    }).json()

    task_id = resp["request"]
    for _ in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": "1"
        }).json()
        if result["status"] == 1:
            token = result["request"]
            break

    # 5. Inject
    driver.execute_script(f"""
        const el = document.querySelector('textarea[name="{token_field}"], input[name="{token_field}"]');
        if (el) el.value = arguments[0];
    """, token)

    return token

La rama por clase asigna el method correcto; CaptchaAI resuelve reCAPTCHA v2/v3, Cloudflare Turnstile y Cloudflare Challenge, entre otros tipos compatibles.


Buenas prácticas al esperar CAPTCHAs diferidos

Lista de verificación

  • Configura el observer o la espera antes de disparar la interacción, nunca después: si el CAPTCHA se monta antes de que empieces a observar, lo pierdes.
  • Usa esperas explícitas con un tiempo de espera acotado (30 segundos suele bastar) en lugar de un sleep fijo que ralentiza el flujo o falla de forma intermitente.
  • Para reCAPTCHA, espera al iframe o a que grecaptcha.render sea una función, no solo al contenedor.
  • Reproduce la misma secuencia que haría una persona: primero el clic o el foco y luego la lectura del desafío.
  • Respeta los términos de servicio del sitio y la normativa de protección de datos aplicable.

Solución de problemas frecuentes

Problema Causa Solución
El CAPTCHA no aparece nunca La acción de disparo no es la correcta Inspecciona la página para ver qué interacción monta el CAPTCHA
La sitekey es nula El elemento existe pero el script aún no se ha ejecutado Espera al iframe del CAPTCHA, no solo al div
El observer no lo detecta El CAPTCHA ya estaba en el DOM Comprueba si el elemento existe antes de montar el observer
Se agota el tiempo de espera El sitio solo carga el CAPTCHA para usuarios reales Usa un navegador completo con una configuración realista

Qué método conviene en cada caso

Los tres enfoques resuelven el mismo problema desde ángulos distintos:

Comparativa rápida

Método Cuándo usarlo
MutationObserver El CAPTCHA se inyecta como un nodo nuevo y no sabes en qué momento llegará
Espera del objeto global (grecaptcha/turnstile) La opción más sencilla y estable para reCAPTCHA v2/v3 y Turnstile
Interceptar el render Necesitas los parámetros exactos (sitekey, callback, contenedor) antes de resolver

Preguntas frecuentes

¿Cuánto tiempo debo esperar antes de asumir que no hay CAPTCHA?

Depende del disparador. Un margen de 30 segundos cubre la mayoría de los casos. Si el CAPTCHA depende de una llamada de red, espera primero a esa respuesta y luego al elemento.

¿Por qué la sitekey aparece vacía aunque el div ya existe?

Porque el contenedor se monta antes de que el script termine de ejecutarse. Espera al iframe (o a que window.grecaptcha.render sea una función) antes de leer la sitekey.

¿Cuándo uso MutationObserver y cuándo waitForFunction?

Usa MutationObserver cuando el CAPTCHA se inserta como un nodo nuevo del DOM. Usa waitForFunction cuando esperas a que exista el objeto global (grecaptcha, turnstile): es más simple y basta para reCAPTCHA v2/v3 y Turnstile.

¿Puedo detectar varios CAPTCHAs que aparecen en momentos distintos?

Sí. Mantén el MutationObserver activo y no lo desconectes tras la primera coincidencia: cada vez que se monte un nuevo desafío, tu callback recibirá el nodo. Guarda cada sitekey detectada y resuélvelas por separado según su tipo.

¿Necesito un proxy para detectar CAPTCHAs diferidos?

No para detectarlos: la detección ocurre en tu propio navegador. El proxy solo influye en si el sitio decide mostrar el desafío, no en tu capacidad de observarlo.


Guías relacionadas

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