Cloudflare Turnstile verrät sich im Seitenquelltext durch drei Signale: das Skript challenges.cloudflare.com/turnstile, einen cf-turnstile-Container und das Attribut data-sitekey. Wer die Abfrage automatisiert verarbeiten will, muss diese Signale finden und den Sitekey auslesen – ohne Sitekey und Seiten-URL lässt sich das Widget nicht lösen. Wie aufwendig das ist, hängt von der Einbindung ab: statisches HTML, turnstile.render() im JavaScript oder dynamisches Nachladen. Für jeden Fall zeigt dieser Leitfaden eine konkrete Methode.
Das Wichtigste in Kürze
- Turnstile zeigt sich über das Skript
challenges.cloudflare.com/turnstile, dencf-turnstile-Container und das Attributdata-sitekey. - Statisches HTML lesen Sie mit einem GET-Request und Regex aus; dynamisch nachgeladene Widgets erfordern Selenium oder Puppeteer.
- Zum Lösen brauchen Sie nur Sitekey und Seiten-URL – den Rest übernimmt CaptchaAI mit
method=turnstile.
Wie Turnstile in Seiten eingebettet wird
Webseiten binden Turnstile auf drei Arten ein – jede verlangt einen anderen Erkennungsansatz:
| Methode | Funktionsweise | Erkennungsaufwand |
|---|---|---|
| HTML implizit | <div class="cf-turnstile" data-sitekey="..."> direkt im Quelltext |
Gering (statisches HTML) |
| JavaScript explizit | Aufruf von turnstile.render() im Skript |
Mittel (JavaScript parsen) |
| Dynamisches Laden | Widget erscheint nach Nutzeraktion oder XHR | Hoch (JS-Ausführung nötig) |
Praxisbeispiel: Ein bei Hetzner gehosteter Scraper überwacht die Login-Seite eines Shopware-Shops. Welche Methode greift, entscheidet die Einbindung:
- Steht der
cf-turnstile-Container im HTML, genügt Methode 1. - Nach Umstellung auf
turnstile.render()greift Methode 2. - Lädt das Widget erst per Klick nach, brauchen Sie Methode 3.
DSGVO-Hinweis: Fallen beim Scraping personenbezogene Daten an – IP-Adressen zählen dazu –, prüfen Sie vorab Rechtsgrundlage und Datenflüsse.
Methode 1: Turnstile im statischen HTML erkennen
Die einfachste Integration steht direkt im HTML: die Klasse cf-turnstile samt Attribut data-sitekey. Ein GET-Request und ein paar Regex-Muster genügen:
import re
import requests
def detect_turnstile_html(url):
"""Detect Turnstile from static HTML."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(url, headers=headers, timeout=15)
html = response.text
result = {
"turnstile_found": False,
"sitekey": None,
"mode": None,
"theme": None,
"action": None,
"script_loaded": False,
}
# Check for Turnstile script
if "challenges.cloudflare.com/turnstile" in html:
result["script_loaded"] = True
# Check for widget container
if "cf-turnstile" in html:
result["turnstile_found"] = True
# Extract sitekey
sitekey_match = re.search(
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html
)
if sitekey_match:
result["sitekey"] = sitekey_match.group(1)
# Extract mode
if 'data-size="invisible"' in html:
result["mode"] = "invisible"
elif 'data-appearance="interaction-only"' in html:
result["mode"] = "non-interactive"
else:
result["mode"] = "managed"
# Extract theme
theme_match = re.search(r'data-theme=["\'](\w+)["\']', html)
if theme_match:
result["theme"] = theme_match.group(1)
# Extract action
action_match = re.search(r'data-action=["\']([^"\']+)["\']', html)
if action_match:
result["action"] = action_match.group(1)
return result
# Usage
info = detect_turnstile_html("https://example.com/login")
if info["turnstile_found"]:
print(f"Sitekey: {info['sitekey']}")
print(f"Mode: {info['mode']}")
Methode 2: Turnstile über die JavaScript-API erkennen
Manche Seiten rendern das Widget per JavaScript. Dann steckt der Sitekey im Konfigurationsobjekt des turnstile.render()-Aufrufs. Darin finden Sie meist:
sitekey– der einzige Pflichtwert für die Lösung,actionundappearance– optionale Parameter,callback– die Funktion, die das Token weiterverarbeitet.
Ein paar gezielte Regex-Muster genügen, um diese Werte auszulesen:
import re
def detect_turnstile_js_api(html):
"""Detect Turnstile from JavaScript render calls."""
patterns = [
# turnstile.render('#element', {sitekey: '...'})
r"turnstile\.render\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*\{([^}]+)\}",
# turnstile.render(element, {sitekey: '...'})
r"turnstile\.render\s*\([^,]+,\s*\{([^}]+)\}",
]
for pattern in patterns:
match = re.search(pattern, html, re.DOTALL)
if match:
config_text = match.group(match.lastindex)
# Extract sitekey from config object
sitekey_match = re.search(
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", config_text
)
# Extract callback
callback_match = re.search(
r"callback\s*:\s*(\w+|function)", config_text
)
# Extract action
action_match = re.search(
r"action\s*:\s*['\"]([^'\"]+)['\"]", config_text
)
# Extract appearance
appearance_match = re.search(
r"appearance\s*:\s*['\"]([^'\"]+)['\"]", config_text
)
return {
"found": True,
"method": "javascript_api",
"sitekey": sitekey_match.group(1) if sitekey_match else None,
"callback": callback_match.group(1) if callback_match else None,
"action": action_match.group(1) if action_match else None,
"appearance": appearance_match.group(1) if appearance_match else None,
}
return {"found": False, "method": None}
Methode 3: Dynamisch geladene Turnstile-Widgets mit Selenium und Puppeteer erkennen
Erscheint das Widget erst nach dem initialen HTML, hilft nur ein echter Browser. Selenium und Puppeteer decken dabei zwei Fälle ab, die statisches Parsing verfehlt:
- Widgets, die per JavaScript erst nach
DOMContentLoadedgerendert werden. - Sitekeys, die aus einer XHR-Antwort in den DOM geschrieben werden.
Beide Werkzeuge rendern die Seite vollständig und lesen den Sitekey anschließend aus dem DOM:
Python (Selenium)
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
import re
def detect_turnstile_dynamic(url):
"""Detect dynamically loaded Turnstile using Selenium."""
options = webdriver.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(options=options)
try:
driver.get(url)
# Wait for page to fully load
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
result = {
"turnstile_found": False,
"sitekey": None,
"iframe_present": False,
"response_field": False,
}
# Check for Turnstile iframe
iframes = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='challenges.cloudflare.com']")
if iframes:
result["turnstile_found"] = True
result["iframe_present"] = True
# Check for cf-turnstile container
containers = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
for container in containers:
sitekey = container.get_attribute("data-sitekey")
if sitekey:
result["turnstile_found"] = True
result["sitekey"] = sitekey
# Check for hidden response field
response_fields = driver.find_elements(
By.CSS_SELECTOR, "[name='cf-turnstile-response'], [name='g-recaptcha-response']"
)
if response_fields:
result["response_field"] = True
# Check page source for JS API render
page_source = driver.page_source
js_match = re.search(
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", page_source
)
if js_match and not result["sitekey"]:
result["sitekey"] = js_match.group(1)
result["turnstile_found"] = True
return result
finally:
driver.quit()
Node.js (Puppeteer)
const puppeteer = require("puppeteer");
async function detectTurnstileDynamic(url) {
const browser = await puppeteer.launch({
headless: "new",
args: ["--disable-blink-features=AutomationControlled"],
});
const page = await browser.newPage();
const result = {
turnstileFound: false,
sitekey: null,
iframePresent: false,
responseField: false,
scriptUrl: null,
};
// Monitor network for Turnstile script
page.on("response", (response) => {
if (response.url().includes("challenges.cloudflare.com/turnstile")) {
result.scriptUrl = response.url();
}
});
await page.goto(url, { waitUntil: "networkidle2" });
// Check for Turnstile container
const sitekey = await page.evaluate(() => {
const el = document.querySelector(
".cf-turnstile, [data-sitekey]"
);
return el ? el.getAttribute("data-sitekey") : null;
});
if (sitekey) {
result.turnstileFound = true;
result.sitekey = sitekey;
}
// Check for Turnstile iframe
const iframes = await page.$$("iframe[src*='challenges.cloudflare.com']");
if (iframes.length > 0) {
result.turnstileFound = true;
result.iframePresent = true;
}
// Check for response field
const responseField = await page.$(
"[name='cf-turnstile-response']"
);
result.responseField = !!responseField;
await browser.close();
return result;
}
detectTurnstileDynamic("https://example.com/login").then(console.log);
Randfälle bei der Sitekey-Extraktion
In der Praxis sitzt der Sitekey nicht immer im HTML:
| Szenario | Problem | Lösung |
|---|---|---|
| Sitekey in externer JS-Datei | Nicht im Seiten-HTML enthalten | Verknüpfte JavaScript-Dateien nach den Sitekey-Mustern durchsuchen |
| Sitekey aus API-Antwort | Erst nach XHR-Aufruf geladen | Netzwerkanfragen überwachen und den Sitekey aus den JSON-Antworten lesen |
| Mehrere Turnstile-Widgets | Unterschiedliche Sitekeys auf derselben Seite | Sitekey dem Formular zuordnen, das Sie tatsächlich absenden |
| Turnstile im Shadow DOM | Über normale Selektoren nicht erreichbar | shadowRoot.querySelector im Browser-Kontext verwenden |
| Serverseitig gerenderter Sitekey | In Template-Variablen eingebettet | <script>-Tags auf Konfigurationsobjekte prüfen |
| Turnstile hinter Authentifizierung | Auf der öffentlichen Seite nicht sichtbar | Zuerst anmelden, dann erkennen |
Fehlerbehebung bei der Turnstile-Erkennung
| Symptom | Ursache | Behebung |
|---|---|---|
| Skript-Tag da, aber kein Sitekey | JS-API-Rendering, Konfiguration aus anderer Quelle | Alle verknüpften JS-Dateien und XHR-Antworten prüfen |
| Falscher Sitekey extrahiert | Mehrere CAPTCHA-Widgets auf der Seite | Sitekeys den umliegenden Formularelementen zuordnen |
| Erkennung klappt, aber das Lösen scheitert | Aktionsparameter für die Validierung nötig | Wert von data-action in die Lösungsanforderung aufnehmen |
| Widget fehlt im ersten HTML | Dynamisches Laden nach Nutzerinteraktion | Selenium/Puppeteer für die vollständige Darstellung nutzen |
Feld cf-turnstile-response bleibt leer |
Widget noch nicht fertig geladen | Warten, bis das Widget vollständig initialisiert ist |
Alle Methoden in einer Detektor-Klasse
Die folgende Klasse bündelt alle drei Ansätze – URL rein, Sitekey und Modus zurück:
import re
import requests
class TurnstileDetector:
"""Detect Cloudflare Turnstile across all implementation methods."""
TURNSTILE_SCRIPT = "challenges.cloudflare.com/turnstile"
SITEKEY_PATTERNS = [
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
r"TURNSTILE_SITE_KEY\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
]
def __init__(self, url, html=None):
self.url = url
self.html = html
if not self.html:
self._fetch()
def _fetch(self):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(self.url, headers=headers, timeout=15)
self.html = response.text
def detect(self):
"""Run all detection methods and return results."""
return {
"url": self.url,
"turnstile_present": self.has_turnstile(),
"sitekey": self.extract_sitekey(),
"mode": self.detect_mode(),
"implementation": self.detect_implementation(),
"script_loaded": self.has_script(),
"response_field": self.has_response_field(),
"action": self.extract_action(),
"theme": self.extract_theme(),
}
def has_turnstile(self):
return (
self.has_script()
or "cf-turnstile" in self.html
or self.extract_sitekey() is not None
)
def has_script(self):
return self.TURNSTILE_SCRIPT in self.html
def has_response_field(self):
return "cf-turnstile-response" in self.html
def extract_sitekey(self):
for pattern in self.SITEKEY_PATTERNS:
match = re.search(pattern, self.html)
if match:
return match.group(1)
return None
def detect_mode(self):
if 'data-size="invisible"' in self.html or "size: 'invisible'" in self.html:
return "invisible"
if 'data-appearance="interaction-only"' in self.html:
return "non-interactive"
if "cf-turnstile" in self.html:
return "managed"
return "unknown"
def detect_implementation(self):
if "cf-turnstile" in self.html and re.search(r"data-sitekey=", self.html):
return "html_implicit"
if "turnstile.render" in self.html:
return "javascript_explicit"
if self.has_script() and not "cf-turnstile" in self.html:
return "dynamic_loading"
return "unknown"
def extract_action(self):
match = re.search(r'data-action=["\']([^"\']+)["\']', self.html)
if match:
return match.group(1)
match = re.search(r"action\s*:\s*['\"]([^'\"]+)['\"]", self.html)
return match.group(1) if match else None
def extract_theme(self):
match = re.search(r'data-theme=["\'](\w+)["\']', self.html)
return match.group(1) if match else "auto"
# Usage
detector = TurnstileDetector("https://example.com/login")
info = detector.detect()
if info["turnstile_present"]:
print(f"Sitekey: {info['sitekey']}")
print(f"Mode: {info['mode']}")
print(f"Implementation: {info['implementation']}")
Nach der Erkennung: Turnstile mit CaptchaAI lösen
Für die Lösungsanforderung an CaptchaAI (method=turnstile) brauchen Sie nur wenige Werte:
- den extrahierten
sitekey, - die
pageurlder Seite mit dem Widget, - optional
data-action, falls die Seite es serverseitig prüft.
Der Solver liefert ein cf-turnstile-response-Token zurück, das Sie ins Formular eintragen und absenden:
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_detected_turnstile(detection_result):
"""Solve Turnstile using detection results."""
if not detection_result["turnstile_present"]:
raise ValueError("No Turnstile detected")
if not detection_result["sitekey"]:
raise ValueError("Sitekey not found — may need browser-based extraction")
params = {
"key": API_KEY,
"method": "turnstile",
"sitekey": detection_result["sitekey"],
"pageurl": detection_result["url"],
"json": 1,
}
# Include action if present
if detection_result.get("action"):
params["action"] = detection_result["action"]
submit = requests.post("https://ocr.captchaai.com/in.php", data=params)
task_id = submit.json()["request"]
for _ in range(60):
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.get("status") == 1:
return result["request"]
raise TimeoutError("Turnstile solve timed out")
# Full workflow
detector = TurnstileDetector("https://example.com/signup")
info = detector.detect()
if info["turnstile_present"]:
token = solve_detected_turnstile(info)
print(f"Token: {token[:50]}...")
Häufige Fragen
Woran erkenne ich Turnstile im Invisible-Modus?
Das sichtbare Widget fehlt, die Signale bleiben. Achten Sie auf data-size="invisible" oder size: 'invisible' sowie auf das versteckte Feld cf-turnstile-response. Der Modus ändert nichts am Lösungsweg.
Kann eine Seite mehrere Turnstile-Widgets enthalten?
Ja, etwa ein Widget pro Formular – jedes mit eigenem cf-turnstile-Container und mitunter eigenem Sitekey. Ordnen Sie den Sitekey immer dem Formular zu, das Sie absenden.
Wie unterscheide ich Turnstile von reCAPTCHA im Quelltext?
Am Klassennamen und am Skript: Turnstile nutzt cf-turnstile und challenges.cloudflare.com/turnstile, reCAPTCHA g-recaptcha und google.com/recaptcha. Auch die Token-Felder verraten den Typ – cf-turnstile-response statt g-recaptcha-response.
Was tun, wenn der Sitekey nicht im HTML steht?
Dann steckt er meist in einer externen JS-Datei, einer API-Antwort oder wird zur Laufzeit erzeugt. Öffnen Sie die DevTools (Tab „Netzwerk“) oder lassen Sie die Seite von Selenium/Puppeteer rendern und lesen den Sitekey aus dem DOM. Ist data-action gesetzt, geben Sie den Wert beim Lösen mit.
Fazit
Cloudflare Turnstile erkennen Sie an vier Merkmalen: dem Skript-Tag, dem cf-turnstile-Container, dem data-sitekey-Attribut und turnstile.render()-Aufrufen. Statische Widgets erfassen Sie per HTML-Parsing, dynamisch nachgeladene mit Selenium oder Puppeteer. Steht der Sitekey fest, lösen Sie das Widget mit dem Turnstile-Solver von CaptchaAI – unabhängig vom Modus und mit hoher Erfolgsquote.