Use Cases

Gestion des CAPTCHA pour l'automatisation de la recherche dans les documents publics

Sur un portail de documents publics, le défi qui bloque votre script n'est presque jamais un CAPTCHA moderne : c'est une image de texte déformé, une addition à recopier ou un code à quatre chiffres. La procédure tient en quatre gestes : ouvrir la page dans une session persistante, télécharger l'image avec les mêmes cookies, l'envoyer en base64 à l'endpoint OCR de CaptchaAI, puis renvoyer la réponse avant expiration du formulaire.

La difficulté ne vient pas d'un portail isolé, mais de la multiplication des sources : chaque administration a son formulaire et sa façon de perdre votre session.

Pourquoi les portails de documents publics restent au CAPTCHA image

Les systèmes d'information publics vivent longtemps, et trois contraintes figent le défi en place :

  • le portail est antérieur à reCAPTCHA et à Cloudflare Turnstile ;
  • le CAPTCHA maison est généré côté serveur, dans le socle applicatif ;
  • le remplacer suppose un marché public, une recette et une validation d'accessibilité.

Conséquence pratique : la variété. Deux portails voisins servent un texte déformé, une opération arithmétique en image ou une grille reCAPTCHA v2 — un client qui ne traite qu'un format casse à la première source ajoutée.

Quels CAPTCHA servent les portails de documents publics

Triez vos sources par type de défi avant la première ligne de code : ces familles reviennent presque toujours.

Famille de portail CAPTCHA typique Exemple de défi
Recherche d'affaires judiciaires CAPTCHA texte sur mesure 5 à 6 caractères alphanumériques déformés
Registres fonciers et cadastre CAPTCHA arithmétique « Combien font 4 + 7 ? »
Registre des entreprises Texte incrusté dans une image Lettres ondulées avec bruit de lignes
Actes d'état civil reCAPTCHA v2 Sélection dans une grille d'images
Permis de construire CAPTCHA texte simple Code numérique à 4 chiffres
Sûretés et nantissements CAPTCHA OCR sur mesure Majuscules et minuscules sur fond bruité

Les paramètres API décisifs

Une lecture erronée vient rarement du moteur de reconnaissance, mais d'un envoi sans contexte : si le code fait six caractères ou n'a que des chiffres, dites-le à l'API.

Paramètre Valeur Quand l'utiliser
method base64 L'image a été téléchargée en mémoire
method post Vous envoyez directement le fichier image
language 0 Texte latin
numeric 1 Défi composé uniquement de chiffres
min_len / max_len Variable La longueur du code est prévisible
textinstructions Consigne libre CAPTCHA arithmétiques ou formats particuliers

Interroger un portail judiciaire sans perdre la session

Le point de rupture n'est pas la résolution, c'est le cookie : l'image du défi ne vaut que pour la session qui vient de charger la page.

Trois règles à tenir dans la même session

  • chargez la page, puis téléchargez l'image avec le même objet Session ;
  • relisez les champs masqués du formulaire au même chargement que l'image ;
  • renvoyez la réponse à l'endpoint du formulaire avant son expiration.
import requests
import base64
import time
from urllib.parse import urljoin

class PublicRecordsSearcher:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def search_court_records(self, portal_url, case_number):
        """Search court records, solving image CAPTCHAs as needed."""
        # Load the search page
        page = self.session.get(f"{portal_url}/search")

        # Extract CAPTCHA image
        captcha_img_url = self._extract_captcha_url(page.text, portal_url)
        if not captcha_img_url:
            # No CAPTCHA on this page
            return self._submit_search(portal_url, case_number)

        # Download and solve CAPTCHA
        img_response = self.session.get(captcha_img_url)
        captcha_text = self._solve_image_captcha(img_response.content)

        # Submit search with solved CAPTCHA
        return self._submit_search(portal_url, case_number, captcha_text)

    def _extract_captcha_url(self, html, base_url):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        # Look for common CAPTCHA image patterns
        captcha_img = (
            soup.find("img", {"id": "captchaImage"}) or
            soup.find("img", {"class": "captcha"}) or
            soup.find("img", attrs={"src": lambda s: s and "captcha" in s.lower()})
        )

        if captcha_img and captcha_img.get("src"):
            return urljoin(base_url, captcha_img["src"])
        return None

    def _solve_image_captcha(self, image_bytes):
        img_base64 = base64.b64encode(image_bytes).decode("utf-8")

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "base64",
            "body": img_base64,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(30):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("CAPTCHA solve timed out")

    def _submit_search(self, portal_url, case_number, captcha_text=None):
        form_data = {"caseNumber": case_number}
        if captcha_text:
            form_data["captcha"] = captcha_text

        response = self.session.post(
            f"{portal_url}/search/results",
            data=form_data
        )
        return response.text

# Usage
searcher = PublicRecordsSearcher("YOUR_API_KEY")
results = searcher.search_court_records(
    "https://courts.example.gov",
    "2024-CV-12345"
)

Traiter les CAPTCHA arithmétiques des registres fonciers

Beaucoup de portails fonciers affichent une opération en image plutôt qu'un mot déformé. Le traitement est identique, à un détail près : textinstructions demande le résultat du calcul, pas les caractères lus.

def solve_math_captcha(self, image_bytes):
    """Solve math CAPTCHAs like '4 + 7 = ?'"""
    img_base64 = base64.b64encode(image_bytes).decode("utf-8")

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": self.api_key,
        "method": "base64",
        "body": img_base64,
        "textinstructions": "solve the math equation and return only the number",
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(30):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": self.api_key,
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]

    raise TimeoutError("Math CAPTCHA solve timed out")

Agréger plusieurs portails dans une même campagne

Une source indisponible ne doit pas interrompre la campagne : isolez chaque portail dans son bloc d'erreur, tracez l'échec avec le nom de la source et rejouez les sources fautives en seconde passe.

class RecordsAggregator {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  async searchAcrossPortals(query, portals) {
    const results = [];

    for (const portal of portals) {
      try {
        const data = await this.searchPortal(portal, query);
        results.push({ portal: portal.name, records: data });
      } catch (error) {
        results.push({ portal: portal.name, error: error.message });
      }
    }

    return results;
  }

  async searchPortal(portal, query) {
    const pageResponse = await fetch(portal.searchUrl);
    const html = await pageResponse.text();

    // Check for image CAPTCHA
    const captchaMatch = html.match(/captcha[^"]*\.(?:png|jpg|gif)/i);
    let captchaAnswer = null;

    if (captchaMatch) {
      const imgUrl = new URL(captchaMatch[0], portal.searchUrl).href;
      const imgData = await fetch(imgUrl);
      const buffer = await imgData.arrayBuffer();
      const base64 = Buffer.from(buffer).toString('base64');

      captchaAnswer = await this.solveImageCaptcha(base64);
    }

    // Submit search
    const formData = new URLSearchParams({ q: query });
    if (captchaAnswer) formData.append('captcha', captchaAnswer);

    const response = await fetch(portal.searchUrl, {
      method: 'POST',
      body: formData
    });

    return response.text();
  }

  async solveImageCaptcha(base64Image) {
    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'base64',
        body: base64Image,
        json: '1'
      })
    });

    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) return data.request;
    }

    throw new Error('CAPTCHA solve timed out');
  }
}

// Usage
const aggregator = new RecordsAggregator('YOUR_API_KEY');
const results = await aggregator.searchAcrossPortals('Smith LLC', [
  { name: 'State Business Registry', searchUrl: 'https://sos.example.gov/search' },
  { name: 'County Court Records', searchUrl: 'https://courts.example.gov/search' }
]);

Dimensionner vos threads : un exemple chiffré

Les ordres de grandeur ci-dessous reposent sur des mesures observées ; ils varient selon l'environnement, le volume et l'heure de la journée.

Prenons une équipe qui rafraîchit chaque nuit 4 000 fiches d'entreprises sur six portails. Un cycle complet — page, image, résolution, envoi — prend une dizaine de secondes : en séquentiel la campagne déborde de la fenêtre nocturne, sur cinq requêtes simultanées elle y rentre.

La facturation porte sur les threads simultanés, pas sur le nombre de résolutions, illimitées par thread. BASIC ($15/mois, 5 threads) couvre ce scénario ; STANDARD ($30/mois, 15 threads) laisse de la marge pour tripler les sources. Facturation en dollars US : dimensionnez sur le pic.

Dépannage

Symptôme Cause probable Correctif
L'image du CAPTCHA renvoie 403 Cookie de session absent Chargez la page dans la même session avant l'image
Réponse refusée malgré une lecture correcte Image bruitée ou trop petite Prétraitez l'image ou fixez min_len / max_len
Le CAPTCHA se régénère à l'envoi Token de formulaire expiré Relisez les champs masqués au même chargement que l'image
Résultats vides après un défi validé Cookies perdus à la redirection POST Gardez allow_redirects=True et persistez la session

Périmètre autorisé et obligations RGPD

Les données publiques restent des données : dès qu'un extrait contient un nom ou une date de naissance, le RGPD s'applique, même si la source est librement consultable.

  • vérifiez les conditions de réutilisation du portail ;
  • limitez la collecte aux champs strictement utiles ;
  • fixez une durée de conservation et documentez votre finalité.

FAQ

Quel plan faut-il pour interroger plusieurs portails en parallèle ?

Comptez un thread par requête simultanée : BASIC ($15/mois, 5 threads) autorise cinq résolutions en vol, STANDARD ($30/mois, 15 threads) quinze. Les résolutions étant illimitées par thread, c'est le parallélisme qui détermine le plan.

Le portail affiche un nouveau CAPTCHA à chaque tentative : que faire ?

C'est presque toujours un problème de session, pas de reconnaissance : réutilisez le même objet Session pour la page, l'image et l'envoi. Si le portail impose un délai maximal, resserrez l'intervalle d'interrogation du résultat.

CaptchaAI prend-il en charge hCaptcha si un portail y bascule ?

Non — hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs) ; GeeTest v4 est annoncé comme à venir. Sont couverts : reCAPTCHA v2 et v3, Cloudflare Turnstile et Challenge, GeeTest v3, l'image/OCR et les grilles d'images, plus CaptchaFox, Friendly Captcha et Lemin, tous trois en bêta.

Faut-il prétraiter l'image avant de l'envoyer ?

Seulement quand la qualité source est mauvaise. Niveaux de gris, contraste et suppression du bruit aident sur les scans anciens et les très petites images ; sur une image nette, ils n'apportent rien. Techniques détaillées dans le guide de prétraitement des images.

Prochaines étapes

Commencez par un portail et un type de défi, mesurez le taux de réussite sur cent recherches, puis ajoutez les sources une par une. Récupérez votre clé API CaptchaAI et branchez la résolution du CAPTCHA sur votre pipeline de collecte.


Étapes suivantes

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