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:
- Abre el código fuente de la página (Ctrl+U): si no ves el
divni el script del CAPTCHA, pero ambos aparecen al interactuar con la página, es carga diferida. - 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.
- 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
sleepfijo que ralentiza el flujo o falla de forma intermitente. - Para reCAPTCHA, espera al iframe o a que
grecaptcha.rendersea 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.