Un crawler qui tombe sur un CAPTCHA ne renvoie pas une erreur : il renvoie une page HTML valide, mais vide des données attendues. C'est pour cette raison qu'un pipeline de collecte se dégrade en silence pendant des jours. La réponse tient en trois gestes : reconnaître le défi, envoyer ses paramètres à l'API CaptchaAI, réinjecter le token dans la requête suivante.
Trois montages couvrent la quasi-totalité des cas de production : résolution à la demande, résolution en amont, et Cloudflare Challenge avec cookie cf_clearance.
Reconnaître le défi CAPTCHA avant de le résoudre
Votre scraper doit d'abord savoir dire « cette réponse n'est pas la page attendue ». Trois signaux suffisent :
- un code HTTP 403 ou 503 avec un corps HTML anormalement court ;
- un conteneur
g-recaptchaoucf-turnstile, ou un script servi depuischallenges.cloudflare.com; - l'absence du sélecteur métier attendu par votre parseur (le
div.itemde votre extraction).
Le troisième est le plus robuste : il survit aux changements de marqueurs HTML. Traitez-le comme une exception explicite, pas comme une liste vide — une page sans résultat et une page bloquée méritent deux entrées de logs distinctes. Pour les défis affichés en fenêtre modale, voir la détection des CAPTCHA injectés en modale.
Les types de CAPTCHA que vous croiserez vraiment
| Type de CAPTCHA | Où il apparaît | Méthode CaptchaAI |
|---|---|---|
| reCAPTCHA v2 | Connexion, pages de recherche | method=userrecaptcha |
| reCAPTCHA v3 | Score en arrière-plan | method=userrecaptcha&version=v3 |
| Cloudflare Turnstile | Sites derrière Cloudflare | method=turnstile |
| Cloudflare Challenge | Blocage plein écran | method=cloudflare_challenge |
| CAPTCHA image / OCR | Portails anciens | method=base64 |
| GeeTest v3 | Inscription, recherche | method=geetest |
CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) existent aussi, mais leur statut bêta les écarte du chemin critique d'un crawl. Trois familles restent hors périmètre :
| Type | Statut côté CaptchaAI | Conséquence pour la collecte |
|---|---|---|
| hCaptcha | ❌ pas pris en charge | Prévoyez une source alternative |
| FunCaptcha (Arkose Labs) | ❌ pas pris en charge | Site hors périmètre automatisable |
| GeeTest v4 | ❌ à venir | Vérifiez la version du widget |
Approche 1 : détecter puis résoudre à la demande
Montage par défaut, et le moins coûteux : vous scrapez normalement et n'occupez un thread que lorsqu'un défi apparaît. Notez la même requests.Session entre la page de défi et la soumission du token : c'est le cookie de session qui rend la réponse acceptable côté serveur.
import requests
import time
from bs4 import BeautifulSoup
API_KEY = "YOUR_API_KEY"
class ProtectedScraper:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def scrape(self, url):
resp = self.session.get(url)
# Check for CAPTCHA
if self._has_captcha(resp.text):
resp = self._handle_captcha(resp.text, url)
return resp.text
def _has_captcha(self, html):
indicators = ["g-recaptcha", "cf-turnstile", "h-captcha", "captcha"]
return any(ind in html.lower() for ind in indicators)
def _handle_captcha(self, html, url):
soup = BeautifulSoup(html, "html.parser")
# reCAPTCHA v2
rc = soup.find("div", class_="g-recaptcha")
if rc:
token = self._solve_recaptcha(rc["data-sitekey"], url)
return self.session.post(url, data={"g-recaptcha-response": token})
# Cloudflare Turnstile
ts = soup.find("div", class_="cf-turnstile")
if ts:
token = self._solve_turnstile(ts["data-sitekey"], url)
return self.session.post(url, data={"cf-turnstile-response": token})
raise Exception("Unknown CAPTCHA type")
def _solve_recaptcha(self, site_key, page_url):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY, "method": "userrecaptcha",
"googlekey": site_key, "pageurl": page_url
})
return self._poll(resp.text.split("|")[1])
def _solve_turnstile(self, site_key, page_url):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY, "method": "turnstile",
"sitekey": site_key, "pageurl": page_url
})
return self._poll(resp.text.split("|")[1])
def _poll(self, task_id):
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
})
if result.text == "CAPCHA_NOT_READY": continue
if result.text.startswith("OK|"): return result.text.split("|")[1]
raise Exception(result.text)
raise TimeoutError()
# Usage
scraper = ProtectedScraper()
html = scraper.scrape("https://example.com/data")
L'interrogation du résultat se fait toutes les 5 secondes, avec un plafond de 60 tentatives. Turnstile se termine généralement en moins de 10 secondes ; reCAPTCHA v2 demande davantage. Journalisez le temps de résolution réel : c'est votre indicateur avancé quand un site change de configuration.
Approche 2 : résoudre en amont sur les pages à défi connu
Quand une page affiche systématiquement un défi — recherche, connexion —, inutile de la charger une première fois pour le découvrir. Récupérez le sitekey une fois, mettez-le en cache, puis envoyez directement la requête accompagnée du token.
def scrape_known_captcha_page(url, site_key):
# Solve before even loading the page
token = solve_recaptcha(site_key, url)
# Submit directly with token
resp = requests.post(url, data={
"g-recaptcha-response": token,
"query": "search term"
})
return resp.text
Le gain est d'un aller-retour HTTP par page. La contrepartie : un sitekey mis en cache trop longtemps finit invalidé lors d'une refonte du site. Rafraîchissez-le dès qu'une soumission échoue deux fois d'affilée.
Approche 3 : franchir un Cloudflare Challenge et conserver cf_clearance
Un Cloudflare Challenge ne produit pas un token de formulaire mais un cookie cf_clearance, lié à un couple précis d'adresse IP et de User-Agent. D'où le fameux « pourtant le défi a bien été résolu » : cookie obtenu via le proxy A, rejoué via le proxy B.
def get_cloudflare_clearance(url, proxy):
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": API_KEY,
"method": "cloudflare_challenge",
"pageurl": url,
"proxy": proxy,
"proxytype": "HTTP"
})
task_id = resp.text.split("|")[1]
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
})
if result.text == "CAPCHA_NOT_READY": continue
if "cf_clearance" in result.text:
# Parse cf_clearance and user_agent from response
return result.text
raise TimeoutError()
Transmettez le même proxy à l'API et à votre client HTTP, et reprenez le User-Agent renvoyé avec le cookie. Un worker, un proxy, un User-Agent, un cookie : cette règle élimine l'essentiel des blocages résiduels.
Passer à l'échelle : boucle multipage et budget de threads
def scrape_multiple_pages(base_url, site_key, pages):
scraper = ProtectedScraper()
results = []
for page in pages:
url = f"{base_url}?page={page}"
try:
html = scraper.scrape(url)
soup = BeautifulSoup(html, "html.parser")
items = soup.find_all("div", class_="item")
results.extend([item.text.strip() for item in items])
print(f"Page {page}: {len(items)} items")
except Exception as e:
print(f"Page {page} failed: {e}")
time.sleep(random.uniform(2, 5))
return results
Deux détails comptent en production : l'échec d'une page n'interrompt jamais la boucle, et la pause aléatoire évite un rythme de requêtes trop régulier, signal en soi pour les défenses anti-automatisation.
Côté coût, CaptchaAI facture des threads simultanés, pas des résolutions : un thread correspond à un défi en cours, et chaque plan inclut un nombre illimité de résolutions par thread.
- crawl séquentiel nocturne : BASIC ($15/mois, 5 threads) ;
- flotte de workers sur plusieurs domaines : ADVANCE ($90/mois, 50 threads).
Dimensionnez d'après les défis simultanés, jamais d'après le volume mensuel de pages (facturation en dollars US).
Scénario : veille tarifaire depuis un worker européen
Cas courant dans les équipes data francophones : relever chaque nuit les prix publics d'une centaine de références sur dix sites marchands, depuis des workers OVHcloud, Scaleway ou AWS eu-west-3 (Paris). Sur ces dix domaines, deux servent du Turnstile, trois un reCAPTCHA v2 sur la recherche, les autres rien.
Le dimensionnement se fait donc sur cinq domaines, pas sur dix : cinq défis simultanés au pic, absorbés par un plan d'entrée. Côté conformité, trois règles :
- données publiques de catalogue uniquement ;
robots.txtet conditions d'utilisation respectés ;- aucune donnée personnelle conservée au passage — la minimisation RGPD allège aussi votre rétention de logs.
Dépannage d'un scraping sous CAPTCHA
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Un défi sur chaque page | Rythme trop soutenu, IP marquée | Répartissez sur plusieurs proxys, espacez |
| Token refusé après résolution | Token expiré, session différente | Utilisez-le dans les 120 s, même session |
| Blocage Cloudflare malgré le cookie | Proxy ou User-Agent différents | Reprenez le couple proxy + User-Agent |
| Page différente après résolution | Redirection, cookie supplémentaire | Suivez les redirections, rejouez avec les cookies |
CAPCHA_NOT_READY jusqu'au timeout |
Paramètres ou pageurl erronés |
Vérifiez le sitekey et l'URL transmise |
Questions fréquentes
CaptchaAI résout-il hCaptcha pour mes crawls ?
Non, hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs). Si un site cible s'appuie sur l'un des deux, cherchez une source de données alternative.
Combien de threads faut-il pour 50 000 pages par nuit ?
Comptez les défis simultanés, pas les pages. Avec 10 % de pages protégées et huit workers en parallèle, huit threads suffisent : chaque thread enchaîne les résolutions sans limite de volume.
Combien de temps un token reste-t-il valide ?
Environ 120 secondes pour reCAPTCHA et Turnstile. Envoyez-le juste après réception ; si votre file d'attente introduit un délai, résolvez au moment de la soumission.
Faut-il un navigateur headless pour chaque page ?
Rarement. Une session HTTP suffit tant que les paramètres du défi sont lisibles dans le HTML servi. Réservez Selenium, Puppeteer ou Playwright aux pages rendues en JavaScript, et consultez la gestion des CAPTCHA en navigateur headless pour lire les paramètres dans le DOM.