Ihr Scraper liest den Quelltext aus, findet aber weder ein .g-recaptcha-Div noch einen Sitekey – und trotzdem erscheint im Browser eine Abfrage. Der Grund ist fast immer Lazy Loading: Die Seite rendert das CAPTCHA erst nach einem Klick, Fokuswechsel, Scroll-Event oder Timer.
Die Lösung: den Auslöser bedienen und dann auf das Element warten, statt den Quelltext sofort zu erfassen. Dieser Leitfaden zeigt drei Erkennungsmethoden und den Lösungsschritt mit CaptchaAI.
Warum Seiten CAPTCHAs verzögert laden
Verzögertes Laden ist kein Zufall, sondern eine bewusste Design-Entscheidung. Websites sparen damit Ladezeit und blenden die Abfrage erst ein, wenn jemand eine schützenswerte Aktion auslöst – etwa das Absenden eines Login-Formulars. Für Ihre Automatisierung heißt das: Der erste Quelltext ist absichtlich leer.
Drei Muster erklären nahezu jeden Fall:
- Ereignisgesteuert: Ein Klick, ein Fokuswechsel oder ein Scroll-Event fügt das Widget nachträglich ein.
- Zeitgesteuert: Ein Timer oder eine
setTimeout-Logik rendert das CAPTCHA erst nach einigen Sekunden. - Zustandsgesteuert: Erst eine vorausgehende AJAX-Antwort signalisiert der Seite, dass die Abfrage nötig ist.
Wer den Auslöser kennt, erspart sich blindes Warten und trifft das Element schon beim ersten Versuch.
Typische Auslöser für Lazy-Loaded CAPTCHAs
| Auslöser | Beispiel | So aktivieren Sie ihn |
|---|---|---|
| Button-Klick | „Absenden“ fügt dem Formular reCAPTCHA hinzu | Zuerst auf den Button klicken |
| Formularfokus | CAPTCHA erscheint, sobald das Eingabefeld fokussiert ist | E-Mail-/Passwortfeld fokussieren |
| Scroll-Position | CAPTCHA lädt, sobald der Abschnitt sichtbar wird | Zum Formular scrollen |
| Timer | CAPTCHA lädt nach 3 Sekunden | Verzögerung abwarten |
| JavaScript-Bedingung | CAPTCHA lädt nach einer AJAX-Antwort | Voraussetzungs-Request auslösen |
Methode 1: Das DOM mit einem MutationObserver überwachen
Ein MutationObserver meldet neue CAPTCHA-Elemente im DOM, sobald sie eingefügt werden.
Puppeteer: Observer einrichten
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/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);
Den Ladevorgang auslösen
// 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();
});
Methode 2: Auf die Skript-Injektion warten
Statt auf das DOM-Element warten Sie auf das globale Objekt der Bibliothek – bei reCAPTCHA window.grecaptcha.
// 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');
});
Turnstile: auf window.turnstile warten
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');
});
Methode 3: Render-Aufrufe der CAPTCHA-Bibliothek abfangen
Diese Variante hängt sich vor dem Rendern ein und liest Sitekey und Callback aus dem render()-Aufruf.
// 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' }
Welche Methode passt zu welchem Fall?
Alle drei Ansätze führen zum Ziel, spielen aber unterschiedliche Stärken aus. Die folgende Übersicht hilft bei der Auswahl:
| Methode | Am besten geeignet für | Zu beachten |
|---|---|---|
| MutationObserver | Widgets, die dynamisch ins DOM eingefügt werden | Vor dem Auslösen einrichten, sonst entgeht Ihnen das Element |
| Skript-Injektion abwarten | Div ist da, aber der Sitekey fehlt noch | Wartet auf window.grecaptcha bzw. window.turnstile |
| Render-Aufrufe abfangen | Sitekey und Callback direkt beim Rendern lesen | Muss vor den Seitenskripten injiziert werden |
In der Praxis kombinieren Sie die Ansätze: Der MutationObserver meldet das neue Element, und die Skript-Abfrage stellt sicher, dass der Sitekey auch wirklich gesetzt ist, bevor Sie ihn auslesen.
Python mit Selenium: auf Lazy-Loaded CAPTCHAs warten
In Selenium übernimmt WebDriverWait die Wartelogik: Trigger auslösen, dann bis zu 30 Sekunden warten.
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://example.com/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")
Auf den reCAPTCHA-Iframe warten
Häufiger Stolperstein: Das Div ist da, aber data-sitekey bleibt null, weil die Bibliothek noch lädt – warten Sie zusätzlich auf den Iframe.
# 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")
Vollständiger Ablauf: erkennen, lösen, Token einfügen
Die folgende Funktion fasst alles zusammen – auslösen, abwarten, lösen, Token eintragen:
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. Mit CaptchaAI lösen
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
Der Selektor .h-captcha dient nur der Erkennung – CaptchaAI löst hCaptcha nicht. Der Ablauf greift für reCAPTCHA v2/v3, Turnstile, GeeTest v3 sowie Bild- und Grid-CAPTCHAs. Das Token ist meist nur rund 120 Sekunden gültig; lösen Sie es unmittelbar vor der Übermittlung.
Praxisszenario: Turnstile hinter einem Login-Fokus
Ein typischer Fall aus dem DACH-Raum: Ihr Monitoring läuft auf einem selbst gehosteten Worker – etwa einer Hetzner- oder netcup-VPS-Instanz – und die Login-Seite enthält im ersten Quelltext kein CAPTCHA. Erst beim Fokus auf das E-Mail-Feld injiziert die Seite das Cloudflare-Turnstile-Widget; der Sitekey bleibt null. Der Ablauf in vier Schritten:
- Die Login-Seite laden und nicht sofort den Quelltext auslesen.
- Mit
page.focus('#email')den Auslöser bedienen. - Auf
window.turnstilewarten, bis die Bibliothek vollständig geladen ist. data-sitekeysamt Page-URL an CaptchaAI übergeben und das zurückgelieferte Token einfügen.
Bei DSGVO-relevanten Workflows gilt zusätzlich: Prüfen Sie vorab Rechtsgrundlage und Datenflüsse, da IP-Adressen personenbezogen sein können. Diese Sorgfaltspflicht liegt bei Ihnen als Betreiber, nicht beim Lösungsdienst.
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| CAPTCHA erscheint nie | Falsche Trigger-Aktion | Seite inspizieren, um den Auslöser zu finden |
| Sitekey ist null | Element existiert, aber das Skript lief noch nicht | Auf den Iframe warten, nicht nur auf das Div |
| Observer verpasst das Element | CAPTCHA war bereits vorhanden | Vor dem Observer auf bestehende Elemente prüfen |
| Timeout | CAPTCHA lädt nur für echte Nutzer | Vollwertigen Browser mit realistischem Signalprofil verwenden |
Checkliste vor dem Produktivbetrieb
Bevor Sie den Ablauf auf reale Workflows loslassen, prüfen Sie diese Punkte:
- Wird der Observer eingerichtet, bevor der Auslöser bedient wird?
- Warten Sie auf den Iframe bzw. das globale Objekt – und nicht nur auf das Div?
- Ist ein Timeout gesetzt, damit hängende Seiten den Prozess nicht blockieren?
- Lösen Sie das Token unmittelbar vor der Übermittlung, da es nur rund 120 Sekunden gültig ist?
- Läuft der Zielprozess in einer eigenen oder ausdrücklich autorisierten Umgebung?
Häufige Fragen
Warum findet mein Scraper den Sitekey nicht, obwohl das CAPTCHA sichtbar ist?
Weil das Widget beim Auslesen noch nicht im DOM steht oder die Bibliothek den Sitekey noch nicht gesetzt hat. Lösen Sie zuerst den Trigger aus und warten Sie auf Element bzw. Iframe, bevor Sie data-sitekey lesen.
Funktioniert die Erkennung auch im Headless-Modus?
Ja, mit Einschränkungen. Manche Seiten laden CAPTCHAs nur für nicht-headless Sitzungen. Nutzen Sie headless: 'new' in Puppeteer oder ein realistisches Browser-Signalprofil, wenn ein Widget ausbleibt.
Löst CaptchaAI hCaptcha, wenn es dynamisch nachgeladen wird?
Nein, CaptchaAI unterstützt hCaptcha derzeit nicht – ob sofort oder verzögert geladen. Für reCAPTCHA v2/v3, Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild- und Grid-CAPTCHAs greift der Ablauf dagegen zuverlässig.
Welche Wartezeit sollte ich für Lazy-Loaded CAPTCHAs ansetzen?
30 Sekunden sind ein robuster Standard – so lange warten auch die Beispiele in diesem Leitfaden. Setzen Sie den Wert nicht zu niedrig an: Bei langsamer Verbindung oder verzögerter AJAX-Antwort erscheint das Widget sonst erst nach Ihrem Timeout.
Worin unterscheiden sich MutationObserver und WebDriverWait?
Der MutationObserver reagiert direkt im Browser auf jede DOM-Änderung und eignet sich für dynamisch eingefügte Widgets. WebDriverWait fragt aus Selenium heraus in Intervallen ab, ob das Element vorhanden ist – einfacher zu schreiben, aber etwas träger. Für reines Python-Tooling reicht WebDriverWait meist aus.
Kann ich mehrere Auslöser kombinieren?
Ja. Manche Seiten laden das CAPTCHA erst nach Klick und Scroll. Bedienen Sie die Auslöser nacheinander und richten Sie den Observer davor ein, damit kein Ereignis verloren geht.
Alle unterstützten CAPTCHAs mit CaptchaAI lösen
Holen Sie sich Ihren API-Schlüssel unter captchaai.com und binden Sie den Lösungsablauf direkt in Ihre Automatisierung ein.