Pour scraper un site protégé par CAPTCHA en Python, pas besoin de navigateur : requests suffit, à condition de déléguer la résolution du défi à un service externe. Le principe tient en trois temps :
- vous repérez le sitekey dans le HTML de la page cible ;
- vous l'envoyez à CaptchaAI, qui vous renvoie un token valide ;
- vous injectez ce token dans votre requête POST, là où le navigateur l'aurait placé.
Ce guide construit une classe solveur réutilisable, puis l'applique à un formulaire reCAPTCHA, à la pagination et aux CAPTCHA image — le tout sans Selenium, avec la seule bibliothèque requests.
Prérequis
| Prérequis | Détails |
|---|---|
| Python 3.7+ | avec pip |
| requests | pip install requests |
| beautifulsoup4 | pip install beautifulsoup4 |
| Clé API CaptchaAI | depuis captchaai.com |
Quand requests suffit, et quand il faut un navigateur
Le mode « sans navigateur » fonctionne tant que le formulaire s'envoie par une requête HTTP classique. C'est le cas le plus fréquent : une page de recherche, un formulaire de connexion, une API interne appelée en POST. Vous y gagnez en vitesse et en mémoire par rapport à Selenium, et votre scraper reste facile à paralléliser.
Réservez le navigateur headless aux pages qui ne fonctionnent pas sans lui :
- le formulaire est monté dynamiquement en JavaScript après le chargement ;
- le site vérifie l'exécution de scripts côté client avant d'accepter l'envoi ;
- le token doit être posé par un callback JavaScript difficile à reproduire en pur HTTP.
Dans tous les autres cas, requests associé à CaptchaAI reste la voie la plus directe.
Une classe solveur CaptchaAI réutilisable
Centralisez l'envoi et le polling dans une seule classe. Elle envoie la tâche à in.php, interroge res.php jusqu'à l'obtention du token, et expose une méthode par type de défi : reCAPTCHA v2, reCAPTCHA v3, Turnstile et CAPTCHA image. Le polling se fait toutes les 5 s, avec un timeout de 300 s.
import requests
import time
class CaptchaSolver:
def __init__(self, api_key):
self.api_key = api_key
self.base = "https://ocr.captchaai.com"
def _submit(self, params):
params["key"] = self.api_key
resp = requests.get(f"{self.base}/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit error: {resp.text}")
return resp.text.split("|")[1]
def _poll(self, task_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(5)
resp = requests.get(f"{self.base}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id
})
if resp.text == "CAPCHA_NOT_READY":
continue
if resp.text.startswith("OK|"):
return resp.text.split("|")[1]
raise Exception(f"Solve error: {resp.text}")
raise TimeoutError("Solve timed out")
def solve_recaptcha_v2(self, site_key, page_url):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_recaptcha_v3(self, site_key, page_url, action="verify"):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"version": "v3",
"action": action
})
return self._poll(task_id)
def solve_turnstile(self, site_key, page_url):
task_id = self._submit({
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_image(self, image_base64):
task_id = self._submit({
"method": "base64",
"body": image_base64
})
return self._poll(task_id)
Elle expose quatre méthodes, une par défi couramment rencontré au scraping :
solve_recaptcha_v2pour les cases « Je ne suis pas un robot » ;solve_recaptcha_v3avec son score et son paramètreaction;solve_turnstilepour Cloudflare Turnstile ;solve_imagepour les CAPTCHA texte en image.
Une fois cette classe en place, chaque scraper se résume à instancier CaptchaSolver avec votre clé API et à appeler la méthode correspondant au défi rencontré. Vous ne réécrivez jamais la logique de polling d'un script à l'autre.
Scraper un formulaire protégé par reCAPTCHA
Le parcours se déroule en cinq temps :
- chargez la page cible et récupérez son HTML ;
- extrayez le sitekey depuis le
divreCAPTCHA (attributdata-sitekey) ; - demandez un token à CaptchaAI pour ce sitekey ;
- renvoyez le formulaire avec le token dans le champ
g-recaptcha-response; - analysez la réponse pour en extraire les données utiles.
Une requests.Session() conserve les cookies entre le GET et le POST, ce qui évite que le site vous redemande le défi à l'envoi.
from bs4 import BeautifulSoup
import requests
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
# Step 1: Load the page
url = "https://example.com/search"
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Step 2: Extract the site key
recaptcha_div = soup.find("div", class_="g-recaptcha")
site_key = recaptcha_div["data-sitekey"]
# Step 3: Solve the CAPTCHA
token = solver.solve_recaptcha_v2(site_key, url)
# Step 4: Submit the form with the token
form_data = {
"q": "search term",
"g-recaptcha-response": token
}
result = session.post(url, data=form_data)
# Step 5: Parse the results
result_soup = BeautifulSoup(result.text, "html.parser")
items = result_soup.find_all("div", class_="result-item")
for item in items:
print(item.text.strip())
Parcourir plusieurs pages paginées
Sur des résultats paginés, résolvez un token par page et bouclez jusqu'à ce qu'une page ne renvoie plus rien. Le time.sleep(2) entre deux pages ménage le serveur cible et lisse votre trafic, ce qui reste plus discret côté anti-abus qu'une rafale de requêtes.
def scrape_all_pages(base_url, site_key, max_pages=10):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
all_results = []
for page_num in range(1, max_pages + 1):
page_url = f"{base_url}?page={page_num}"
# Solve CAPTCHA for each page if needed
token = solver.solve_recaptcha_v2(site_key, page_url)
resp = session.get(page_url, params={
"g-recaptcha-response": token,
"page": page_num
})
soup = BeautifulSoup(resp.text, "html.parser")
items = soup.find_all("div", class_="item")
if not items:
break
all_results.extend([item.text.strip() for item in items])
print(f"Page {page_num}: {len(items)} items")
time.sleep(2) # Polite delay
return all_results
Résoudre les CAPTCHA image
Pour les CAPTCHA texte affichés sous forme d'image : téléchargez l'image, encodez-la en base64, envoyez-la au solveur et récupérez le texte reconnu. CaptchaAI prend aussi en charge les grilles d'images en disponibilité générale, avec la même méthode d'envoi.
import base64
def scrape_with_image_captcha(url):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Find the CAPTCHA image
captcha_img = soup.find("img", {"id": "captcha-image"})
captcha_url = captcha_img["src"]
# Download and encode the image
img_resp = session.get(captcha_url)
img_base64 = base64.b64encode(img_resp.content).decode()
# Solve
captcha_text = solver.solve_image(img_base64)
# Submit
form_data = {
"captcha": captcha_text,
"username": "user"
}
result = session.post(url, data=form_data)
return result.text
Nouvelles tentatives et gestion des erreurs
En production, un token est parfois refusé et une requête échoue sur un incident réseau passager. Enveloppez l'appel au solveur dans une boucle de retry avec backoff exponentiel : quelques tentatives espacées absorbent ces aléas sans faire tomber tout le scraper. Journalisez chaque échec pour distinguer un problème ponctuel d'une panne durable côté cible.
def solve_with_retry(solver, site_key, page_url, max_retries=3):
for attempt in range(max_retries):
try:
return solver.solve_recaptcha_v2(site_key, page_url)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(2)
Périmètre et conformité RGPD
Cadrez votre périmètre avant de lancer un scraper à grande échelle : limitez-vous à des pages publiques dont la collecte est autorisée, et ne conservez que les données strictement nécessaires. Si vous traitez des données personnelles de résidents européens, vérifiez vos obligations RGPD — base légale, durée de conservation, information des personnes concernées. Ce cadrage relève de votre responsabilité éditoriale, pas de l'outil de résolution.
Côté performance, rapprochez vos workers des sites visés : une région AWS eu-west-3 (Paris) ou une instance OVHcloud réduit la latence vers les cibles francophones et limite les timeouts pendant le polling.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
ERROR_WRONG_USER_KEY |
Clé API invalide | Vérifiez la clé dans le tableau de bord |
ERROR_ZERO_BALANCE |
Solde épuisé | Rechargez votre compte |
| Le POST renvoie de nouveau la page CAPTCHA | Token expiré ou mauvais nom de champ | Utilisez le token immédiatement ; vérifiez le nom des champs du formulaire |
ConnectionError |
Incident réseau | Ajoutez une boucle de retry avec backoff exponentiel |
| Résultats vides après la soumission | Le site exige des cookies de session | Utilisez requests.Session() pour conserver les cookies |
FAQ
Faut-il un proxy résidentiel pour scraper derrière un CAPTCHA ?
Pas toujours. Un proxy datacenter suffit sur beaucoup de sites ; passez au résidentiel quand la cible bloque ces plages. Faites tourner vos proxys. Voir la rotation de proxys pour le scraping CAPTCHA.
Comment traiter plusieurs CAPTCHA en parallèle sans saturer l'API ?
Chaque plan CaptchaAI est dimensionné en threads — un thread par CAPTCHA en cours. Pour un fort volume, un client asynchrone envoie plusieurs tâches à la fois, dans la limite de vos threads. Voir l'intégration aiohttp avec CaptchaAI.
CaptchaAI résout-il reCAPTCHA v3 pour le scraping ?
Oui, en disponibilité générale, au même titre que reCAPTCHA v2, Cloudflare Turnstile et GeeTest v3. La classe ci-dessus expose déjà solve_recaptcha_v3 avec son paramètre action.
Pourquoi mon POST renvoie-t-il de nouveau la page du CAPTCHA ?
Le plus souvent, le token a expiré ou n'a pas été placé dans le bon champ. Utilisez-le immédiatement après réception et vérifiez le nom exact du champ attendu par le formulaire (g-recaptcha-response pour reCAPTCHA). Conservez la session pour ne pas perdre les cookies entre le GET et le POST.
Combien de temps le token reste-t-il valide ?
Un token de résolution expire vite, en quelques minutes. Envoyez-le dans la foulée, au sein de la même requête POST, plutôt que de le stocker pour un usage différé : un token périmé se traduit par un formulaire de nouveau protégé.