Le bon moment pour résoudre un CAPTCHA dans un formulaire, c'est juste avant le clic de soumission, jamais au chargement de la page. Un token reCAPTCHA ou Turnstile a une durée de vie courte : si vous le récupérez trop tôt, il expire pendant que le script remplit les derniers champs, et le serveur refuse l'envoi. La fiabilité d'un formulaire automatisé tient d'abord à cet ordre des opérations. Ce guide montre comment traiter la résolution comme une étape à part entière d'un flux Selenium associé à CaptchaAI : détecter le type de défi, récupérer le token ou le texte correspondant, l'injecter au bon endroit, puis envoyer. La démarche couvre reCAPTCHA v2, Cloudflare Turnstile et les CAPTCHA image (OCR), les types les plus fréquents sur les formulaires de contact, de connexion et d'inscription.
Pourquoi un CAPTCHA casse la séquence de soumission
Un formulaire protégé n'ajoute pas seulement une vérification : il impose un ordre. La plupart des échecs viennent d'une séquence mal placée, pas d'une résolution ratée. Quatre pièges reviennent en boucle :
- Résolution trop précoce. Le token est obtenu avant le remplissage, puis expire avant le clic. La parade : résoudre en dernier, au plus près de la soumission.
- Détection erronée. La page contient plusieurs éléments
[data-sitekey]et le script confond Turnstile avec reCAPTCHA. Identifiez le type avant d'envoyer la tâche. - DOM dynamique. Le champ ou le widget n'est pas encore rendu ; sans attente explicite,
find_elementéchoue silencieusement. - Injection au mauvais endroit. Sur un formulaire AJAX, écrire dans le champ caché ne suffit pas si le JavaScript lit le token ailleurs avant l'envoi.
Architecture du flux de soumission
Le flux tient en quatre étapes, dans un ordre strict : charger le formulaire, remplir les champs, détecter puis résoudre le CAPTCHA, et enfin soumettre. La résolution reste volontairement en avant-dernière position, au plus près du clic final.
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Load Form │────▶│ Fill Fields │────▶│ Detect & │────▶│ Submit Form │
│ (Selenium) │ │ │ │ Solve │ │ │
│ │ │ │ │ CAPTCHA │ │ │
└────────────┘ └──────────────┘ └────────────┘ └──────────────┘
Les trois composants du flux
Le code se répartit en trois responsabilités : un solveur qui parle à l'API CaptchaAI, un détecteur qui identifie le type de défi, et un orchestrateur qui remplit puis soumet le formulaire.
Le solveur encapsule l'API : il envoie la tâche à in.php, puis interroge res.php jusqu'au résultat. Le paramètre json=1 simplifie le parsing, et la boucle gère la réponse CAPCHA_NOT_READY sans marteler l'API — un premier délai de 10 s, puis une interrogation toutes les 5 s. Le même solveur sert pour reCAPTCHA, Turnstile et les CAPTCHA image ; seuls les paramètres de la tâche changent.
import time
import requests
class FormCaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(f"Submit error: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(f"Solve error: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out")
Le détecteur inspecte le DOM dans un ordre précis : Turnstile d'abord (classe cf-turnstile), reCAPTCHA ensuite ([data-sitekey] accompagné d'une mention recaptcha dans le HTML), puis les CAPTCHA image. Cet ordre n'est pas cosmétique : Turnstile et reCAPTCHA exposent tous deux l'attribut data-sitekey, donc le test le plus spécifique doit passer en premier.
import re
from selenium.webdriver.common.by import By
class CaptchaDetector:
def __init__(self, driver):
self.driver = driver
def detect(self):
"""Detect CAPTCHA type on current page."""
html = self.driver.page_source
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
for el in turnstile:
if "cf-turnstile" in (el.get_attribute("class") or ""):
return "turnstile", el.get_attribute("data-sitekey")
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha:
sitekey = recaptcha[0].get_attribute("data-sitekey")
if "recaptcha" in html.lower():
return "recaptcha_v2", sitekey
# Image CAPTCHA
img = self.driver.find_elements(By.CSS_SELECTOR, "img[src*='captcha'], img.captcha")
if img:
return "image", img[0].get_attribute("src")
return "none", None
L'orchestrateur FormAutomator assemble le tout. fill_field attend que chaque champ soit présent avant d'écrire, solve_captcha route vers la bonne méthode selon le type détecté, et submit_form enchaîne remplissage, résolution et clic. Le token est injecté dans g-recaptcha-response pour reCAPTCHA v2, dans cf-turnstile-response pour Turnstile ; pour un CAPTCHA image, le texte résolu est saisi dans le champ correspondant.
import base64
import requests as req
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
class FormAutomator:
def __init__(self, api_key):
self.solver = FormCaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.detector = CaptchaDetector(self.driver)
def fill_field(self, selector, value):
field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
field.clear()
field.send_keys(value)
def select_option(self, selector, value):
from selenium.webdriver.support.ui import Select
select = Select(self.driver.find_element(By.CSS_SELECTOR, selector))
select.select_by_value(value)
def solve_captcha(self):
captcha_type, data = self.detector.detect()
page_url = self.driver.current_url
if captcha_type == "recaptcha_v2":
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return True
if captcha_type == "turnstile":
token = self.solver.solve({
"method": "turnstile",
"sitekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return True
if captcha_type == "image":
img_data = req.get(data).content
img_b64 = base64.b64encode(img_data).decode()
text = self.solver.solve({"method": "base64", "body": img_b64})
captcha_input = self.driver.find_element(
By.CSS_SELECTOR, "input[name*='captcha']"
)
captcha_input.clear()
captcha_input.send_keys(text)
return True
return False # No CAPTCHA detected
def submit_form(self, url, fields, submit_selector="button[type='submit']"):
"""
fields: list of (selector, value) tuples
"""
self.driver.get(url)
for selector, value in fields:
self.fill_field(selector, value)
self.solve_captcha()
submit = self.driver.find_element(By.CSS_SELECTOR, submit_selector)
submit.click()
return self.driver.current_url
def close(self):
self.driver.quit()
Pièges fréquents et correctifs
Avant de dérouler des exemples, gardez cette table sous la main : la plupart des soumissions qui échouent se ramènent à l'une de ces cinq causes.
| Problème | Cause probable | Correctif |
|---|---|---|
| Token rejeté | Le token a expiré avant le clic final | Résolvez le CAPTCHA juste avant la soumission, jamais au chargement |
| Champ introuvable | Rendu dynamique de la page | Ajoutez des attentes explicites (WebDriverWait) sur chaque sélecteur |
| Mauvais type détecté | Plusieurs widgets partagent data-sitekey |
Placez le test Turnstile avant celui de reCAPTCHA |
| Le formulaire se recharge sans confirmation | Validation serveur incomplète | Contrôlez tous les champs obligatoires et lisez les messages d'erreur retournés |
| Le callback reCAPTCHA ne se déclenche pas | La page attend une fonction JS spécifique | Appelez le callback attendu après l'injection du token |
Exemple : automatiser un formulaire de contact
Cas typique en QA : valider le parcours d'un formulaire de contact sur un environnement de préproduction, par exemple un site hébergé chez OVHcloud ou Scaleway. Le script remplit les champs, résout le défi présent et confirme la redirection qui suit l'envoi. Remplacez YOUR_API_KEY par votre clé et adaptez les sélecteurs à votre page.
automator = FormAutomator("YOUR_API_KEY")
try:
result_url = automator.submit_form(
url="https://example.com/contact",
fields=[
("#name", "John Doe"),
("#email", "john@example.com"),
("#subject", "Sales inquiry"),
("#message", "I'd like to learn more about your services."),
],
submit_selector="#submit-btn",
)
print(f"Form submitted. Redirected to: {result_url}")
finally:
automator.close()
Réutiliser le même flux sur d'autres formulaires
Le même FormAutomator couvre la connexion, l'inscription et la recherche sans changer de logique : seuls les sélecteurs et les valeurs varient.
Formulaire de connexion
result = automator.submit_form(
url="https://example.com/login",
fields=[
("#username", "testuser"),
("#password", "testpass123"),
],
submit_selector="#login-btn",
)
Formulaire d'inscription
result = automator.submit_form(
url="https://example.com/register",
fields=[
("#first-name", "Jane"),
("#last-name", "Smith"),
("#email", "jane@example.com"),
("#password", "SecurePass!123"),
("#confirm-password", "SecurePass!123"),
],
submit_selector="#register-btn",
)
Formulaire de recherche protégé
result = automator.submit_form(
url="https://example.com/search",
fields=[
("#query", "python developer"),
("#location", "San Francisco"),
],
submit_selector="#search-btn",
)
RGPD et périmètre d'usage
CaptchaAI n'intervient que sur la résolution du défi ; la conformité du traitement des données saisies dans le formulaire reste de votre responsabilité. Automatiser des formulaires qui collectent des données personnelles (nom, e-mail, message) relève du RGPD, un réflexe naturel pour les équipes en France, en Belgique, en Suisse ou au Maghreb :
- N'automatisez que vos propres formulaires ou des environnements de test explicitement autorisés — jamais ceux de tiers sans accord.
- En préproduction, utilisez des jeux de données fictifs plutôt que de vraies coordonnées.
- Minimisez ce que vous collectez et purgez les logs de test ; documentez la finalité si vous les conservez.
FAQ
À quel moment faut-il résoudre le CAPTCHA pour éviter que le token expire ?
Le plus tard possible : après avoir rempli tous les champs, juste avant le clic de soumission. Les tokens reCAPTCHA et Turnstile ne vivent que quelques minutes, donc plus vous les récupérez tôt, plus le risque de rejet au moment de l'envoi augmente.
Comment le script distingue-t-il reCAPTCHA v2, Turnstile et un CAPTCHA image ?
Le détecteur teste le DOM dans un ordre fixe et renvoie le premier type reconnu, en commençant par le plus spécifique. Turnstile passe avant reCAPTCHA car les deux exposent l'attribut data-sitekey ; les CAPTCHA image sont repérés par la source de l'image.
Faut-il vraiment un navigateur, ou une requête HTTP suffit-elle ?
Pour un formulaire statique qui accepte une soumission HTTP classique, vous pouvez résoudre le CAPTCHA puis envoyer un POST sans navigateur. Dès qu'il y a de la validation JavaScript, des callbacks ou un DOM construit dynamiquement, Selenium reste le chemin le plus fiable.
Quel plan CaptchaAI choisir pour automatiser des formulaires en volume ?
La facturation se fait au thread, pas à la résolution. Le plan BASIC ($15/mois, 5 threads) suffit pour des campagnes de QA ponctuelles ; passez à STANDARD ($30/mois, 15 threads) ou au-dessus si vous soumettez de nombreux formulaires en parallèle. Chaque thread traite un défi à la fois, sans limite de résolutions.
Guides connexes
Quand vos tests ou vos workflows butent sur des formulaires protégés, récupérez votre clé CaptchaAI et faites de la résolution du CAPTCHA une étape native de la soumission, plutôt qu'un correctif de dernière minute.