Un projet Django croise les CAPTCHA de deux côtés opposés, et on les confond vite. Selon le sens, le travail n'a rien à voir :
- Vérifier un CAPTCHA que votre formulaire affiche, pour filtrer le trafic automatisé : la validation se fait auprès du fournisseur (Cloudflare, Google), sans service tiers.
- Résoudre un CAPTCHA rencontré sur un site tiers, quand vos scripts collectent des données ou lancent des tests d'intégration : c'est là qu'intervient CaptchaAI.
Ce guide traite les deux cas, avec le code Django réutilisable pour chacun. Commençons par vos propres formulaires.
Cas 1 : vérifier les CAPTCHA de vos propres formulaires Django
Quand vous ajoutez Turnstile ou reCAPTCHA à un formulaire Django, le navigateur renvoie un token que votre vue doit confirmer côté serveur avant d'accepter la soumission. Trois fichiers suffisent : le formulaire, la vue et le gabarit.
Ajouter Turnstile à un formulaire Django
D'abord, le formulaire déclare un champ caché qui recevra le token Turnstile :
# 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,
)
Ensuite, la vue transmet ce token à l'endpoint siteverify de Cloudflare et n'accepte la soumission que si la réponse est positive :
# 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,
})
Enfin, le gabarit charge le widget Turnstile et son script :
<!-- 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>
Aucun appel à CaptchaAI dans ce cas : vérifier un CAPTCHA que vous affichez vous-même passe toujours par le fournisseur.
Cas 2 : résoudre les CAPTCHA de sites tiers avec CaptchaAI
C'est le rôle de CaptchaAI. Lorsque votre application Django doit interagir avec un site externe protégé par un CAPTCHA — un portail, une source de données, un environnement de test — vous déléguez la résolution à l'API, qui renvoie un token valide à réinjecter dans votre requête.
Si cette collecte touche des données personnelles, limitez-vous au strict nécessaire et vérifiez vos obligations RGPD avant tout traitement à grande échelle.
Une classe de service CaptchaAI
Encapsulez l'échange submit/poll dans une classe unique plutôt que de le disperser dans vos vues. Elle expose trois points d'entrée :
solve_recaptcha_v2()pour reCAPTCHA v2 ;solve_turnstile()pour Cloudflare Turnstile ;solve_image()pour les CAPTCHA image/OCR.
# 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
Configurer les clés dans settings.py
Déclarez la clé API et les clés Turnstile dans vos réglages. En production, chargez-les depuis des variables d'environnement plutôt que de les écrire en dur (voir la FAQ).
# settings.py
CAPTCHAAI_API_KEY = "YOUR_API_KEY"
TURNSTILE_SITE_KEY = "0x4AAAAAAAC3DHQhMMQ_Rxrg"
TURNSTILE_SECRET_KEY = "0x4AAAAAAAC3DHQhYYY_secret"
Appeler le service depuis vos vues
Le service se branche partout où votre code a besoin d'un token. Deux points d'entrée reviennent le plus souvent :
- une vue déclenchée par une requête utilisateur ;
- une commande de gestion pour les scripts et les tâches planifiées.
Une vue de collecte de données externe
La vue reçoit une URL cible, résout le CAPTCHA, puis réutilise le token pour accéder à la ressource protégée :
# 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)
Une commande de gestion Django
Pour les scripts ponctuels et les tâches planifiées, exposez le service via manage.py :
# 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}")
Lancez-la ainsi :
python manage.py solve_captcha --type turnstile --sitekey 0x4AAA... --url https://example.com
Vues asynchrones Django avec CaptchaAI
Django 4.1+ prend en charge les vues asynchrones, précieuses quand vous résolvez plusieurs CAPTCHA en parallèle sans monopoliser un worker. Le principe reste le même — soumettre puis interroger le résultat — mais avec aiohttp à la place de requests, pour ne pas figer la boucle d'événements :
# 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)
Résolution en arrière-plan avec Celery
Pour les résolutions longues, sortez-les du cycle requête/réponse : la vue rend la main immédiatement et un worker Celery traite le CAPTCHA en tâche de fond. Réservez ce schéma à quelques cas précis :
- les vues web où l'utilisateur ne doit pas attendre ;
- les lots volumineux à traiter en parallèle ;
- les nouvelles tentatives automatiques après un échec.
La capacité de votre offre entre alors en jeu. CaptchaAI facture au thread simultané, chaque thread traitant un CAPTCHA à la fois : l'offre BASIC ($15/mois, 5 threads) autorise cinq résolutions en parallèle, alors dimensionnez vos workers Celery en conséquence. Ces workers se déploient sans peine sur un VPS OVHcloud ou Scaleway proche de vos utilisateurs.
# 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)
Côté application, la vue lance la tâche et un second endpoint interroge son état :
# 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"})
Dépannage
| Symptôme | Cause | Correctif |
|---|---|---|
CaptchaSolveError en production |
Clé API absente des réglages | Ajoutez CAPTCHAAI_API_KEY à votre settings.py Django |
| La tâche Celery se relance en boucle | CAPTCHA insoluble ou mauvais sitekey | Plafonnez max_retries et validez les entrées |
| La vue asynchrone se fige | Code synchrone dans une vue async | Utilisez aiohttp, jamais requests |
| Token expiré avant l'envoi du formulaire | Résolution trop lente | Résolvez juste avant l'usage, pas à l'avance |
| Erreurs d'import dans la commande de gestion | Application non déclarée | Vérifiez l'enregistrement dans INSTALLED_APPS |
Questions fréquentes
Quels types de CAPTCHA cette classe de service peut-elle résoudre ?
reCAPTCHA v2, Cloudflare Turnstile et les CAPTCHA image/OCR, tous pris en charge par CaptchaAI. La même méthode _submit_and_poll les gère : il suffit de changer le paramètre method envoyé à l'API.
CaptchaAI peut-il résoudre un hCaptcha affiché par un site tiers ?
Non — hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs). En revanche, si c'est votre formulaire qui affiche hCaptcha, vous le vérifiez côté serveur avec l'endpoint de vérification d'hCaptcha, exactement comme pour Turnstile au cas 1.
Où stocker la clé API CaptchaAI dans un projet Django ?
Dans une variable d'environnement, chargée via django-environ ou os.environ. Ne committez jamais la clé dans Git et ne l'écrivez pas en dur dans settings.py.
Faut-il résoudre en synchrone ou passer par Celery ?
Pour une vue exposée à l'utilisateur, déléguez à Celery afin qu'il n'attende pas 15 secondes ou plus. Gardez la résolution synchrone dans les commandes de gestion et les scripts par lots.
Combien de résolutions simultanées mon offre autorise-t-elle ?
Autant que de threads dans votre offre. CaptchaAI facture au thread simultané avec des résolutions illimitées par thread : BASIC ($15/mois, 5 threads) traite cinq CAPTCHA en parallèle, STANDARD ($30/mois, 15 threads) en traite quinze. Alignez le nombre de workers Celery sur ce chiffre.
En résumé
Les deux faces du problème se règlent différemment :
- côté formulaires, Django vérifie les tokens directement auprès du fournisseur ;
- côté sites tiers, CaptchaAI fait le gros du travail via une classe de service qui encapsule le cycle submit/poll.
Réservez la résolution synchrone aux commandes de gestion, l'asynchrone aux vues Django 4.1+, et Celery aux traitements de fond. Le même service couvre reCAPTCHA, Turnstile et les CAPTCHA image.