Use Cases

Automatiser des soumissions de formulaires malgré les CAPTCHA

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.

Les commentaires sont désactivés pour cet article.