Progressive Web-Apps stellen einzigartige CAPTCHA-Herausforderungen dar. Sie verwenden clientseitiges Rendering, Service Worker und Einzelseitennavigation – das bedeutet, dass CAPTCHAs dynamisch und nicht mit dem ursprünglichen HTML geladen werden. CaptchaAI übernimmt die Lösung, aber Sie benötigen die richtige Erkennungsstrategie, um CAPTCHAs abzufangen, die nach dem Laden der Seite in das DOM eingefügt werden.
Dieser Leitfaden behandelt das Erkennen, Extrahieren und Lösen von CAPTCHAs in PWA-Kontexten mit Playwright und CaptchaAI.
Woran erkennen Sie, dass das Problem eher die PWA-Struktur ist?
| Beobachtung | Wahrscheinlichere Ursache |
|---|---|
| Im ersten HTML ist kein CAPTCHA sichtbar, später aber schon | Das Widget wird erst clientseitig gerendert |
| Nach Navigation innerhalb der App erscheint plötzlich ein neues CAPTCHA | Routing läuft ohne echten Page-Reload |
| Dasselbe Widget verhält sich nach Updates inkonsistent | Service Worker oder Cache liefern veraltete Ressourcen |
| Sitekey-Extraktion klappt nur manchmal | Der DOM-Zeitpunkt stimmt nicht mit dem Framework-Lifecycle überein |
Warum PWAs anders sind
Herkömmliche Websites stellen CAPTCHAs in der ersten HTML-Antwort bereit. PWAs unterscheiden sich in mehreren wesentlichen Punkten:
| Aspekt | Traditionelle Seite | PWA |
|---|---|---|
| CAPTCHA wird geladen | Im anfänglichen HTML | Nach dem Laden der Seite von JavaScript gerendert |
| Seitennavigation | Komplette Seite neu laden | Clientseitiges Routing (kein Neuladen) |
| Servicemitarbeiter | Nicht vorhanden | Speichert Ressourcen im Cache und kann Anfragen abfangen |
| DOM-Verfügbarkeit | Sofort | Nach dem Rendern des Frameworks |
| Netzwerkanfragen | Direkt | Kann vom Servicemitarbeiter abgefangen werden |
Schritt 1: Warten Sie auf das dynamische CAPTCHA-Rendering
Der größte Fehler besteht darin, Sitekeys zu extrahieren, bevor das PWA-Framework das CAPTCHA-Widget gerendert hat. Verwenden Sie Mutationsbeobachter oder Framework-spezifische Signale:
// 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}`);
// Mit CaptchaAI lösen
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);
Schritt 2: Behandeln Sie das Service-Worker-Caching
Servicemitarbeiter können CAPTCHA-Skripte zwischenspeichern, was zu veralteten Widgets führt. Umgehen Sie den Cache bei Bedarf:
// Intercept and bypass 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' } });
});
Schritt 3: Behandeln Sie die clientseitige Navigation
PWAs verwenden clientseitiges Routing – das Navigieren zu einer CAPTCHA-geschützten Route löst keinen Seitenladevorgang aus. Überwachen Sie Routenänderungen:
// 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);
Schritt 4: Python-Alternative mit 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()
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| Token wird erzeugt, aber vom Ziel abgelehnt | sitekey, pageurl oder Session-Kontext stimmen nicht | Erfasse Parameter erneut und verwende den Token in derselben Browser- oder HTTP-Sitzung |
| Polling endet im Timeout | Intervall, Wartezeit oder Fehlerbehandlung sind zu eng gesetzt | Poll alle 5-10 Sekunden, trenne Timeout von echten Fehlercodes und logge die Ursache |
| Beispiel funktioniert lokal, aber nicht im Workflow | Callback, Form-Feld oder Token-Injektion fehlt in der echten Zielkette | Prüfe den exakten Übergabepfad vom Solver bis zur finalen Zielanfrage |
Verwandte Leitfäden
- Shadow DOM CAPTCHA-Verwaltung in Web Components
- React Native WebView CAPTCHA-Lösung
- Mobile Browser-Automatisierung mit CAPTCHA