Un usuario hace clic en "Enviar" y aparece un modal con un desafío CAPTCHA. La sitekey no está en el código fuente de la página inicial: se carga dinámicamente cuando se abre el modal. Tu script de automatización debe activar el modal, esperar a que el CAPTCHA se renderice, extraer los parámetros, resolver e inyectar el token antes de que expire el modal o caduque la sesión.
Patrones modales CAPTCHA
| Patrón | gatillo | Desafío |
|---|---|---|
| Iniciar sesión modal | Haga clic en el botón "Iniciar sesión" | CAPTCHA se carga dentro del div superpuesto |
| Intersticial anti-bot | Automático después de un comportamiento sospechoso | Página de bloques modales en pantalla completa |
| Confirmación de pago | Enviar formulario de pago | Aparece modal para verificación |
| Diálogo de límite de velocidad | Se detectaron demasiadas solicitudes | Modal con puerta CAPTCHA |
| Consentimiento de cookies + CAPTCHA | Primera visita | CAPTCHA integrado en el diálogo de consentimiento |
Python: controlador CAPTCHA modal de dramaturgo
import requests
import time
from playwright.sync_api import sync_playwright
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
"""Submit and poll a CAPTCHA."""
params = {
"key": API_KEY,
"method": method,
"json": 1,
}
if method == "userrecaptcha":
params["googlekey"] = sitekey
params["pageurl"] = pageurl
elif method == "turnstile":
params["sitekey"] = sitekey
params["pageurl"] = pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def detect_modal_captcha(page):
"""
Detect CAPTCHA inside a visible modal/dialog.
Returns (sitekey, method) or (None, None).
"""
return page.evaluate("""
() => {
// Find visible modals
const modalSelectors = [
'.modal.show',
'.modal[style*="display: block"]',
'dialog[open]',
'[role="dialog"]:not([aria-hidden="true"])',
'.overlay.visible',
'.popup.active',
'[class*="modal"][class*="open"]',
];
let modal = null;
for (const sel of modalSelectors) {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
modal = el;
break;
}
}
// If no modal found, search entire document
const searchRoot = modal || document;
// Check for reCAPTCHA
const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
if (recaptcha) {
return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
}
// Check for Turnstile
const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
}
// Check for hCaptcha
const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
if (hcaptcha) {
return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
}
return null;
}
""")
def inject_token_in_modal(page, token, method="userrecaptcha"):
"""Inject token into the CAPTCHA inside the modal."""
if method == "userrecaptcha":
page.evaluate("""
(token) => {
// Find response textarea (may be inside modal)
const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
textareas.forEach(ta => {
ta.value = token;
ta.style.display = 'block';
});
// Trigger callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.values(___grecaptcha_cfg.clients).forEach(client => {
Object.values(client).forEach(val => {
if (val && typeof val === 'object') {
Object.values(val).forEach(v => {
if (v && typeof v.callback === 'function') v.callback(token);
});
}
});
});
}
}
""", token)
elif method == "turnstile":
page.evaluate("""
(token) => {
const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
inputs.forEach(inp => { inp.value = token; });
if (typeof window.turnstileCallback === 'function') {
window.turnstileCallback(token);
}
}
""", token)
def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
"""
Full workflow: trigger modal, detect CAPTCHA, solve, inject.
"""
# Step 1: Trigger the modal if needed
if trigger_selector:
print(f"Clicking trigger: {trigger_selector}")
page.click(trigger_selector)
# Step 2: Wait for modal to become visible
print("Waiting for modal...")
modal_selectors = [
".modal.show",
"dialog[open]",
'[role="dialog"]:not([aria-hidden="true"])',
".popup.active",
]
modal_visible = False
for selector in modal_selectors:
try:
page.wait_for_selector(selector, timeout=timeout)
modal_visible = True
print(f" Modal detected: {selector}")
break
except Exception:
continue
if not modal_visible:
print(" No modal detected")
return None
# Step 3: Wait for CAPTCHA to render inside modal
time.sleep(2) # Brief pause for dynamic CAPTCHA loading
captcha_info = detect_modal_captcha(page)
if not captcha_info:
print(" No CAPTCHA found in modal")
return None
sitekey = captcha_info["sitekey"]
method = captcha_info["method"]
print(f" Found {method} CAPTCHA: {sitekey[:20]}...")
# Step 4: Solve via CaptchaAI
token = solve_captcha(sitekey, page.url, method)
print(f" Solved: {token[:30]}...")
# Step 5: Inject token
inject_token_in_modal(page, token, method)
print(" Token injected")
return token
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_load_state("networkidle")
# Handle CAPTCHA that appears in login modal
token = handle_modal_captcha(
page,
trigger_selector="button#login-btn",
timeout=10000,
)
if token:
# Fill form fields inside modal
page.fill('dialog input[name="email"]', "user@example.com")
page.fill('dialog input[name="password"]', "password123")
# Submit modal form
page.click('dialog button[type="submit"]')
page.wait_for_load_state("networkidle")
browser.close()
main()
JavaScript: controlador modal titiritero
const puppeteer = require("puppeteer");
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function waitForModal(page, timeout = 10000) {
const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
for (const sel of selectors) {
try {
await page.waitForSelector(sel, { visible: true, timeout });
return sel;
} catch {}
}
return null;
}
async function detectModalCaptcha(page) {
return page.evaluate(() => {
const checks = [
{ sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
{ sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
];
for (const { sel, method } of checks) {
const el = document.querySelector(sel);
if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
}
return null;
});
}
async function handleModalCaptcha(page, triggerSelector) {
// Trigger modal
if (triggerSelector) await page.click(triggerSelector);
// Wait for modal
const modalSel = await waitForModal(page);
if (!modalSel) { console.log("No modal found"); return null; }
console.log(`Modal visible: ${modalSel}`);
// Wait for CAPTCHA render
await new Promise((r) => setTimeout(r, 2000));
const info = await detectModalCaptcha(page);
if (!info) { console.log("No CAPTCHA in modal"); return null; }
console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
const token = await solveCaptcha(info.sitekey, page.url(), info.method);
console.log(`Solved: ${token.substring(0, 30)}...`);
// Inject token
await page.evaluate((t, method) => {
if (method === "userrecaptcha") {
document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
} else if (method === "turnstile") {
document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
}
}, token, info.method);
return token;
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://example.com", { waitUntil: "networkidle2" });
const token = await handleModalCaptcha(page, "button#login-btn");
if (token) {
await page.type('dialog input[name="email"]', "user@example.com");
await page.click('dialog button[type="submit"]');
await page.waitForNavigation();
}
await browser.close();
})();
Consideraciones de sincronización modal
| factores | Impacto | Mitigación |
|---|---|---|
| Tiempo de espera de cierre automático modal | Modal puede cerrarse antes de que se complete la resolución | Comience a resolver inmediatamente después de la detección |
| Caducidad de la sesión durante la resolución | La sesión del servidor caduca en espera modal | Mantenga viva la sesión con latidos de fondo |
| CAPTCHA renderiza retraso en modal | El widget tarda entre 1 y 3 segundos en cargarse en modal | Espere 2 segundos después de que el modal sea visible antes de extraer la clave del sitio |
| Caducidad del token durante el llenado del formulario | El token caduca al completar el formulario modal | Resuelva CAPTCHA al final, después de completar otros campos |
Solución de problemas
| Problema | causa | Solución |
|---|---|---|
| Modal detectado pero no se encontró CAPTCHA | CAPTCHA se carga de forma asincrónica después de que se abre modal | Aumentar el tiempo de espera; use MutationObserver para detectar la inserción de widgets |
| Token inyectado pero modal no se cierra. | Función de devolución de llamada no activada | Busque e invoque la devolución de llamada CAPTCHA explícitamente |
| Modal se cierra durante la resolución. | Tiempo de espera de descarte automático | Deshabilite el tiempo de espera modal a través de JS: clearTimeout() en el temporizador modal |
| La clave del sitio CAPTCHA es diferente cada vez | Modal genera instancias CAPTCHA dinámicas | Extraiga siempre la clave del sitio nueva del DOM modal, nunca la almacene en caché |
| El disparador de clic no abre modal | Elemento no interactivo o detrás de superposición | Utilice page.dispatchEvent o espere a que se pueda hacer clic en el elemento |
Preguntas frecuentes
¿Cómo detecto un modal que se abre automáticamente sin un disparador de clic?
Utilice un MutationObserver para observar la aparición de nuevos elementos en el DOM. Configúrelo antes de navegar a la página. Cuando se agrega un elemento modal y se vuelve visible, su observador se activa y puede iniciar el flujo de detección de CAPTCHA.
¿Qué pasa si el CAPTCHA está dentro de un iframe modal?
Si el modal contiene un iframe con CAPTCHA, combine este enfoque con el manejo de iframe. Después de detectar el modal, cambie al contexto del iframe dentro del modal para extraer la clave del sitio.
¿Debo completar los campos del formulario antes o después de resolver el CAPTCHA?
Antes. Primero complete todos los demás campos del formulario y luego resuelva el CAPTCHA al final. Esto minimiza el tiempo entre la obtención del token y el envío del formulario, lo que reduce el riesgo de caducidad.
Artículos relacionados
- Cómo resolver callback de reCAPTCHA v2 con la API
- Manejo de reCAPTCHA v2 y Turnstile en el mismo sitio
Maneja CAPTCHA en modales emergentes sin problemas: obtén tu API key de CaptchaAI e implementa la detección modal.
Guías relacionadas:
- Manejo de múltiples CAPTCHA en una sola página
- Manejo de Shadow DOM CAPTCHA
- Extracción de CAPTCHA en iframes anidados