In Django-Projekten taucht CAPTCHA-Handling aus zwei Richtungen auf: Sie schützen eigene Formulare vor Bots, und gelegentlich müssen Sie CAPTCHAs auf fremden Seiten lösen – etwa bei Datenerfassung, Monitoring oder automatisierten End-to-End-Tests. Die erste Aufgabe erledigen Cloudflare oder Google serverseitig, für die zweite übernimmt CaptchaAI die eigentliche Lösung über eine einzige Serviceklasse. Diese Anleitung deckt beide Muster ab. Konkret geht es um zwei Aufgaben:
- Eigene Formulare absichern: Sie prüfen serverseitig, ob ein Turnstile- oder reCAPTCHA-Token gültig ist, bevor die App das Formular verarbeitet.
- Fremde CAPTCHAs lösen: CaptchaAI löst die Abfrage und gibt ein Token zurück, mit dem Ihre App die geschützte Ressource abruft.
Szenario 1: CAPTCHAs auf den eigenen Django-Formularen prüfen
Wenn Sie Turnstile oder reCAPTCHA in Ihre eigenen Django-Formulare einbauen, liefert das Widget im Browser ein Token. Dieses Token müssen Sie serverseitig gegenprüfen, bevor Sie das Formular verarbeiten – sonst ist der Schutz wirkungslos. Für Turnstile läuft die Prüfung direkt gegen die siteverify-Endpunkte von Cloudflare, ganz ohne CaptchaAI.
Turnstile in ein Django-Formular einbinden
Ein verstecktes Feld nimmt das Widget-Token entgegen, die View validiert es gegen Cloudflare, und erst danach wird das Formular verarbeitet:
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
cf_turnstile_response = forms.CharField(
widget=forms.HiddenInput(),
required=True,
)
# views.py
import requests
from django.conf import settings
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact_view(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
# Verify Turnstile token with Cloudflare
token = form.cleaned_data["cf_turnstile_response"]
verification = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={
"secret": settings.TURNSTILE_SECRET_KEY,
"response": token,
"remoteip": request.META.get("REMOTE_ADDR"),
},
).json()
if verification.get("success"):
# Process the form
return redirect("success")
else:
form.add_error(None, "CAPTCHA verification failed")
else:
form = ContactForm()
return render(request, "contact.html", {
"form": form,
"turnstile_sitekey": settings.TURNSTILE_SITE_KEY,
})
<!-- templates/contact.html -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="cf-turnstile" data-sitekey="{{ turnstile_sitekey }}"></div>
<button type="submit">Send</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
Szenario 2: CAPTCHAs auf fremden Seiten mit CaptchaAI lösen
Hier kommt CaptchaAI ins Spiel: Sobald Ihre Django-App mit einer CAPTCHA-geschützten Fremdseite interagieren muss, brauchen Sie eine Komponente, die das CAPTCHA löst und das fertige Token zurückgibt.
Hinweis für DACH-Teams: Wer fremde Seiten automatisiert abruft, verarbeitet in der Regel personenbezogene Daten – schon IP-Adressen zählen dazu. Prüfen Sie Zweck und Rechtsgrundlage nach DSGVO, bevor Sie einen Scraping-Workflow produktiv schalten.
Die CaptchaAI-Serviceklasse
Die folgende Klasse kapselt den kompletten Submit-und-Poll-Ablauf gegen die CaptchaAI-API: eine Methode je CAPTCHA-Typ, eine gemeinsame Poll-Schleife und eine Guthaben-Abfrage. Sie bleibt zustandslos und lässt sich in jeder View, jedem Command und jeder Celery-Aufgabe wiederverwenden:
# services/captcha_solver.py
import time
import requests
from django.conf import settings
class CaptchaSolverService:
"""Django service for solving CAPTCHAs via CaptchaAI."""
API_BASE = "https://ocr.captchaai.com"
def __init__(self):
self.api_key = settings.CAPTCHAAI_API_KEY
def solve_recaptcha_v2(self, sitekey, page_url, invisible=False):
"""Solve reCAPTCHA v2."""
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if invisible:
params["invisible"] = 1
return self._submit_and_poll(params)
def solve_turnstile(self, sitekey, page_url, action=None):
"""Solve Cloudflare Turnstile."""
params = {
"key": self.api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if action:
params["action"] = action
return self._submit_and_poll(params)
def solve_image(self, image_base64):
"""Solve image/text CAPTCHA."""
return self._submit_and_poll({
"key": self.api_key,
"method": "base64",
"body": image_base64,
"json": 1,
})
def get_balance(self):
"""Check API balance."""
response = requests.get(f"{self.API_BASE}/res.php", params={
"key": self.api_key,
"action": "getbalance",
"json": 1,
}, timeout=30)
return float(response.json().get("request", 0))
def _submit_and_poll(self, params, timeout=120):
"""Submit task and poll for result."""
# Submit
response = requests.post(f"{self.API_BASE}/in.php", data=params, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("status") != 1:
raise CaptchaSolveError(f"Submit failed: {data.get('request')}")
task_id = data["request"]
# Poll
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
result = requests.get(f"{self.API_BASE}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise CaptchaSolveError("CAPTCHA unsolvable")
raise CaptchaSolveError("Solve timed out")
class CaptchaSolveError(Exception):
pass
Die Klasse bietet je Anwendungsfall eine klar benannte Methode:
solve_recaptcha_v2()– reCAPTCHA v2, optional die unsichtbare Variante.solve_turnstile()– Cloudflare Turnstile, optional mitaction-Parameter.solve_image()– Bild- und Text-CAPTCHAs per Base64.get_balance()– aktuelles Guthaben abfragen, praktisch für Monitoring.
Django-Einstellungen
Legen Sie den API-Schlüssel und die Turnstile-Schlüssel in den Einstellungen ab – im echten Betrieb aus Umgebungsvariablen gelesen, nicht als Klartext im Repository:
# settings.py
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
TURNSTILE_SITE_KEY = "0x4AAAAAAAC3DHQhMMQ_Rxrg"
TURNSTILE_SECRET_KEY = "0x4AAAAAAAC3DHQhYYY_secret"
Die Serviceklasse in Views und Commands einsetzen
Dieselbe Serviceklasse lässt sich aus drei Einstiegspunkten aufrufen:
- aus einer normalen View, wenn eine kurze Lösung akzeptabel ist;
- aus einem Management-Command für Cronjobs und manuelle Läufe;
- aus einer Celery-Aufgabe, wenn die Lösung im Hintergrund laufen soll.
View für die externe Datenerfassung
Diese View löst zuerst das Turnstile-CAPTCHA der Zielseite und ruft die geschützte Ressource anschließend mit dem frischen Token ab:
# views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .services.captcha_solver import CaptchaSolverService, CaptchaSolveError
@require_POST
def scrape_external_data(request):
"""Solve CAPTCHA and fetch data from external CAPTCHA-protected site."""
url = request.POST.get("target_url")
if not url:
return JsonResponse({"error": "target_url required"}, status=400)
solver = CaptchaSolverService()
try:
# Solve the CAPTCHA
token = solver.solve_turnstile(
sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg",
page_url=url,
)
# Use token to access the protected resource
import requests as http_requests
response = http_requests.post(url, data={
"cf-turnstile-response": token,
}, timeout=30)
return JsonResponse({
"status": "success",
"data": response.text[:1000],
})
except CaptchaSolveError as e:
return JsonResponse({"error": str(e)}, status=500)
Management-Command für Cronjobs und manuelle Läufe
Für geplante Jobs und manuelle Aufrufe ist ein Management-Command handlicher als eine View. Er löst ein CAPTCHA, gibt das Token aus und zeigt das verbleibende Guthaben:
# management/commands/solve_captcha.py
from django.core.management.base import BaseCommand
from myapp.services.captcha_solver import CaptchaSolverService
class Command(BaseCommand):
help = "Solve a CAPTCHA and print the token"
def add_arguments(self, parser):
parser.add_argument("--type", choices=["recaptcha", "turnstile"], required=True)
parser.add_argument("--sitekey", required=True)
parser.add_argument("--url", required=True)
def handle(self, *args, **options):
solver = CaptchaSolverService()
self.stdout.write(f"Solving {options['type']} for {options['url']}...")
if options["type"] == "recaptcha":
token = solver.solve_recaptcha_v2(options["sitekey"], options["url"])
else:
token = solver.solve_turnstile(options["sitekey"], options["url"])
self.stdout.write(self.style.SUCCESS(f"Token: {token[:50]}..."))
# Check balance
balance = solver.get_balance()
self.stdout.write(f"Remaining balance: ${balance:.2f}")
Aufruf:
python manage.py solve_captcha --type turnstile --sitekey 0x4AAA... --url https://example.com
Asynchrone Views mit CaptchaAI
Ab Django 4.1 lassen sich Views asynchron schreiben. Das lohnt sich genau dann, wenn eine Anfrage mehrere Sekunden auf das Poll-Ergebnis wartet: Statt einen Worker-Thread zu blockieren, gibt die Coroutine die Kontrolle während des Wartens ab. Wichtig ist, in einer async View konsequent aiohttp statt des synchronen requests zu verwenden:
# views.py (async)
import aiohttp
import asyncio
from django.http import JsonResponse
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
async def solve_captcha_async(request):
"""Async view for solving CAPTCHAs."""
sitekey = request.GET.get("sitekey")
page_url = request.GET.get("url")
if not sitekey or not page_url:
return JsonResponse({"error": "sitekey and url required"}, status=400)
async with aiohttp.ClientSession() as session:
# Submit
async with session.post("https://ocr.captchaai.com/in.php", data={
"key": CAPTCHAAI_API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}) as resp:
data = await resp.json()
if data.get("status") != 1:
return JsonResponse({"error": data.get("request")}, status=500)
task_id = data["request"]
# Poll
for _ in range(30):
await asyncio.sleep(5)
async with session.get("https://ocr.captchaai.com/res.php", params={
"key": CAPTCHAAI_API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}) as resp:
result = await resp.json()
if result.get("status") == 1:
return JsonResponse({"token": result["request"]})
return JsonResponse({"error": "timeout"}, status=504)
Celery für die Hintergrundlösung
Für webbasierte Views gilt die Faustregel: Lassen Sie Nutzer nicht 15 Sekunden oder länger auf eine Antwort warten. Verschieben Sie längere Lösungen stattdessen in eine Celery-Aufgabe und liefern Sie sofort eine Task-ID zurück, die der Client anschließend abfragt.
Ein Hinweis zur Parallelität: Jede gleichzeitig laufende Lösung belegt einen Thread in Ihrem CaptchaAI-Plan – abgerechnet wird pro Thread, nicht pro Lösung. Wenn also mehrere Celery-Worker parallel lösen, sollte die Thread-Anzahl Ihres Plans zur Worker-Concurrency passen:
- BASIC (15 $/Monat, 5 Threads) – genügt für kleine Hintergrund-Queues.
- ADVANCE (90 $/Monat, 50 Threads) – für parallelisierte Pipelines.
- PREMIUM (170 $/Monat, 100 Threads) – für hohe gleichzeitige Last.
Viele DACH-Teams betreiben ihre Worker ohnehin auf Hetzner oder netcup – die Thread-Zahl ist dann die relevante Obergrenze, nicht die Größe des VPS.
# tasks.py
from celery import shared_task
from .services.captcha_solver import CaptchaSolverService, CaptchaSolveError
@shared_task(bind=True, max_retries=2, default_retry_delay=10)
def solve_captcha_task(self, captcha_type, sitekey, page_url):
"""Background CAPTCHA solving with Celery."""
solver = CaptchaSolverService()
try:
if captcha_type == "recaptcha_v2":
token = solver.solve_recaptcha_v2(sitekey, page_url)
elif captcha_type == "turnstile":
token = solver.solve_turnstile(sitekey, page_url)
else:
raise ValueError(f"Unknown type: {captcha_type}")
return {"success": True, "token": token}
except CaptchaSolveError as e:
self.retry(exc=e)
# Usage in views
from .tasks import solve_captcha_task
def start_solve(request):
result = solve_captcha_task.delay("turnstile", "0x4AAA...", "https://example.com")
return JsonResponse({"task_id": result.id})
def check_solve(request, task_id):
from celery.result import AsyncResult
result = AsyncResult(task_id)
if result.ready():
return JsonResponse(result.get())
return JsonResponse({"status": "pending"})
Fehlerbehebung
Die häufigsten Stolpersteine bei der CaptchaAI-Integration in Django und ihre Lösung:
| Symptom | Ursache | Lösung |
|---|---|---|
CaptchaSolveError in Produktion |
API-Schlüssel fehlt in den Einstellungen | CAPTCHAAI_API_KEY in den Django-Einstellungen ergänzen |
| Celery-Aufgabe wiederholt sich endlos | unlösbares CAPTCHA oder falscher Sitekey | max_retries setzen und Eingaben validieren |
| Async View blockiert | synchroner Code in der async View | aiohttp statt requests verwenden |
| Token vor dem Absenden abgelaufen | Lösung hat zu lange gedauert | Just-in-Time lösen, nicht auf Vorrat |
| Importfehler im Management-Command | Service nicht in INSTALLED_APPS registriert |
App-Registrierung prüfen |
Häufige Fragen
Wie viele CAPTCHAs kann ich parallel lösen?
So viele, wie Ihr Plan Threads hat. Jede gleichzeitige Lösung belegt einen Thread; BASIC bietet 5, ADVANCE 50 und PREMIUM 100 gleichzeitige Threads – abgerechnet wird pro Thread, nicht pro Lösung. Richten Sie Ihre Celery-Concurrency an dieser Zahl aus.
Gehört das CAPTCHA-Lösen in eine synchrone View oder in Celery?
In Celery, sobald ein Nutzer im Browser wartet. Eine Lösung dauert je nach Typ mehrere Sekunden; verschieben Sie sie in eine Hintergrundaufgabe und antworten Sie sofort mit einer Task-ID. Synchrones Lösen bleibt Management-Commands und Skripten vorbehalten.
Welche CAPTCHA-Typen deckt die Serviceklasse ab?
reCAPTCHA v2, Cloudflare Turnstile und Bild-CAPTCHAs über dieselbe Klasse. Für weitere Typen wie GeeTest v3 ergänzen Sie eine Methode mit dem passenden method-Parameter – der Submit-und-Poll-Ablauf bleibt identisch.
Warum läuft mein Token vor dem Absenden ab?
Weil Tokens nur kurz gültig sind: reCAPTCHA-Tokens verfallen nach 120 Sekunden, Turnstile-Tokens nach 300 Sekunden. Lösen Sie deshalb erst unmittelbar vor der Verwendung und nie auf Vorrat – Caching bringt hier nichts.
Kann ich CaptchaAI auch mit Django REST Framework nutzen?
Ja. Rufen Sie die Serviceklasse in einer APIView oder einem ViewSet genauso auf wie in einer normalen View. Für langlaufende Lösungen geben Sie eine Task-ID zurück und lassen den Client den Status abfragen, statt den Request offen zu halten.
Fazit
Django-Anwendungen binden CaptchaAI über eine schlanke Serviceklasse ein, die den Submit-und-Poll-Ablauf kapselt. Synchrones Lösen passt in Management-Commands und Skripte, asynchrones Lösen in die async Views von Django 4.1+ und längere Läufe in Celery-Aufgaben. Dieselbe Serviceklasse verarbeitet reCAPTCHA, Turnstile und Bild-CAPTCHAs – und den Token-Check für Ihre eigenen Formulare erledigt Cloudflare weiterhin serverseitig.