Las aplicaciones web progresivas presentan desafíos CAPTCHA únicos. Utilizan renderizado del lado del cliente, Service Workers y navegación de una sola página, lo que significa que los CAPTCHA se cargan dinámicamente en lugar de con el HTML inicial.CaptchaAImaneja la resolución, pero necesita la estrategia de detección correcta para detectar CAPTCHA que se inyectan en el DOM después de que se carga la página.
Esta guía cubre la detección, extracción y resolución de CAPTCHA en contextos PWA con Playwright y CaptchaAI.
Por qué las PWA son diferentes
Los sitios web tradicionales ofrecen CAPTCHA en la respuesta HTML inicial. Las PWA se diferencian en varios aspectos clave:
| Aspecto | Sitio Tradicional | PWA |
|---|---|---|
| Cargando CAPTCHA | En HTML inicial | Representado por JavaScript después de cargar la página |
| Navegación de página | recarga de página completa | Enrutamiento del lado del cliente (sin recarga) |
| Trabajador de servicio | No presente | Almacena en caché los recursos, puede interceptar solicitudes |
| Disponibilidad DOM | Inmediato | Después de renderizar el marco |
| Solicitudes de red | directo | Puede ser interceptado por el trabajador del servicio. |
Paso 1: Espere la representación CAPTCHA dinámica
El mayor error es intentar extraer las claves del sitio antes de que el marco PWA haya representado el widget CAPTCHA. Utilice observadores de mutaciones o señales específicas del marco:
// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function detectCaptchaInPWA(page) {
// Wait for the PWA app shell to render
await page.waitForLoadState('networkidle');
// Use MutationObserver to detect dynamically loaded CAPTCHAs
const captchaInfo = await page.evaluate(() => {
return new Promise((resolve) => {
// Check if CAPTCHA is already present
const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
if (existing) {
resolve({
type: existing.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: existing.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
// Watch for CAPTCHA elements added dynamically
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')
? node
: node.querySelector?.('.g-recaptcha, .cf-turnstile');
if (captcha) {
observer.disconnect();
resolve({
type: captcha.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: captcha.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Timeout after 15 seconds
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 15000);
});
});
return captchaInfo;
}
async function main() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example-pwa.com/login');
const captcha = await detectCaptchaInPWA(page);
if (!captcha) {
console.log('No CAPTCHA detected');
await browser.close();
return;
}
console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);
// Solve con CaptchaAI
const token = await solveCaptcha(captcha);
console.log(`Token: ${token.substring(0, 50)}...`);
// Inject token
await injectToken(page, captcha.type, token);
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
console.log('Form submitted');
await browser.close();
}
async function solveCaptcha(captcha) {
const params = {
key: API_KEY,
pageurl: captcha.pageurl,
json: '1',
};
if (captcha.type === 'recaptcha_v2') {
params.method = 'userrecaptcha';
params.googlekey = captcha.sitekey;
} else {
params.method = 'turnstile';
params.sitekey = captcha.sitekey;
}
const submit = await axios.get(
'https://ocr.captchaai.com/in.php', { params }
);
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(poll.data.request);
}
}
throw new Error('Timeout');
}
async function injectToken(page, type, token) {
if (type === 'recaptcha_v2') {
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
try {
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach((k) => {
Object.keys(clients[k]).forEach((j) => {
if (clients[k][j]?.callback) clients[k][j].callback(t);
});
});
} catch (e) {}
}, token);
} else {
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = t;
const cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](t);
}, token);
}
}
main().catch(console.error);
Paso 2: Manejar el almacenamiento en caché del trabajador de servicio
Los trabajadores de servicios pueden almacenar en caché los scripts CAPTCHA, lo que genera widgets obsoletos. Omita el caché cuando sea necesario:
// Intercept and omisión Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
await page.route('**/turnstile/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
Paso 3: Manejar la navegación del lado del cliente
Las PWA utilizan enrutamiento del lado del cliente: navegar a una ruta protegida por CAPTCHA no activa la carga de la página. Supervise los cambios de ruta:
// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
window.dispatchEvent(new Event('pwa-route-change'));
};
});
page.on('console', async (msg) => {
// React to route changes if needed
});
// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);
Cuándo invalidar el token actual
| Cambio en la PWA | Reutilizar token | Volver a resolver |
|---|---|---|
| Navegación dentro de la misma vista y misma sesión | A veces | Solo si el sitio no recrea el widget |
| Cambio de ruta con nuevo widget | No | Sí, porque suele cambiar el sitekey o el callback |
| Refresco del service worker o hard reload | No | Sí, el estado anterior deja de ser fiable |
Paso 4: Alternativa de Python con Selenium
# pwa_captcha_selenium.py
import time
import requests
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
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")
# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)
sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")
# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]
# Poll
for _ in range(30):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1",
})
if poll.json().get("status") == 1:
token = poll.json()["request"]
break
else:
raise TimeoutError("CAPTCHA not solved")
# Inject token
driver.execute_script(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()
Solución de problemas
| problema | causa | Solución |
|---|---|---|
| El elemento CAPTCHA nunca aparece | PWA aún no ha renderizado la ruta | Utilice waitForSelector con tiempo de espera extendido; garantizar que se complete el enrutamiento del lado del cliente |
| Clave de sitio obsoleta después de la navegación | Service Worker sirvió HTML en caché | Omitir encabezados de caché para recursos relacionados con CAPTCHA |
| No se encontró la devolución de llamada de inyección de token | El marco PWA gestiona el estado de manera diferente | Verifique la gestión del estado de React/Vue/Angular; activar la actualización del estado del formulario |
| El envío del formulario no envía el token | El controlador de formulario SPA lee desde el estado del componente, no desde DOM | Actualice también el estado del marco (por ejemplo, React ref, propiedad reactiva de Vue) |
Preguntas frecuentes
¿Las PWA utilizan diferentes tipos de CAPTCHA que los sitios normales?
No. Las PWA utilizan los mismos widgets reCAPTCHA, Turnstile y otros CAPTCHA. La diferencia es el tiempo: se cargan dinámicamente.
¿Pueden los Service Workers bloquear la resolución de CAPTCHA?
Los Service Workers pueden almacenar en caché los scripts CAPTCHA, pero puedes evitar esto con encabezados que eliminan la caché o deshabilitando el Service Worker durante la automatización.
¿CaptchaAI maneja los tokens específicos de PWA de manera diferente?
No. Los tokens son idénticos ya sea que el CAPTCHA se haya entregado en una PWA o en una página tradicional. CaptchaAI utiliza la clave del sitio y la URL; ambas son iguales.
Artículos relacionados
- Cómo resolver la devolución de llamada de Recaptcha V2 usando Api
- Comparación de Geetest y Cloudflare Turnstile
- Cloudflare Turnstile 403 Después de la corrección del token
Próximos pasos
Comience a resolver CAPTCHA en PWA:obtenga su clave API CaptchaAIe integre la detección dinámica en su automatización.
Guías relacionadas:
- Carga dinámica de CAPTCHA: detección de desafíos cargados de forma diferida
- Cloudflare Turnstile Extracción de claves de sitio
- Extracción de parámetros reCAPTCHA de la fuente de la página