Use Cases

Recherche juridique Web Scraping avec gestion CAPTCHA

Une recherche de jurisprudence automatisée s'arrête presque toujours au même endroit : le CAPTCHA qui protège le portail. Sur PACER, SEC EDGAR ou un système judiciaire d'État, chaque série de requêtes un peu soutenue déclenche un reCAPTCHA v2 ou un CAPTCHA image, et le script se bloque avant même d'avoir récupéré une décision. CaptchaAI résout ces défis via son API pour que la collecte de jurisprudence, de dépôts et de données réglementaires reste continue.

Ce guide montre comment l'intégrer dans un scraper Python : détecter le défi, obtenir le token, réinjecter la réponse, puis reprendre l'extraction — sans perdre de vue le débit ni le RGPD.


Quelles sources juridiques déclenchent des CAPTCHA

Avant d'écrire du code, cartographiez vos portails cibles : chacun présente un défi différent et impose son propre rythme d'accès. Le tableau ci-dessous recense les sources les plus fréquentes d'une veille juridique et le CAPTCHA qu'elles opposent.

Source Type de CAPTCHA Données Utilisateurs
PACER reCAPTCHA v2 Dépôts devant les tribunaux fédéraux Équipes contentieux
Systèmes judiciaires d'État CAPTCHA image / reCAPTCHA Dossiers d'État Avocats
SEC EDGAR reCAPTCHA v2 Dépôts d'entreprises Conformité
Bases de brevets reCAPTCHA v2 Dossiers de brevets Chercheurs en PI
Portails réglementaires CAPTCHA image Règles, avis Conformité
Bases de citations juridiques reCAPTCHA v2 Références de décisions Legal tech
Annuaires des barreaux reCAPTCHA v2 Fiches d'avocats Diligence raisonnable

Une équipe legal tech à Paris qui surveille la jurisprudence RGPD combine souvent plusieurs de ces sources — un portail européen pour les décisions, EDGAR pour les dépôts d'entreprises cotées, une base de brevets pour la PI — et chacune ajoute son propre défi CAPTCHA au pipeline.


Ce que CaptchaAI résout sur ces portails

Deux familles de CAPTCHA couvrent la quasi-totalité des portails juridiques, et CaptchaAI prend les deux en charge :

  • reCAPTCHA v2 (case « Je ne suis pas un robot » et défis image) via la méthode userrecaptcha. C'est le défi dominant sur PACER, EDGAR et la plupart des bases de citations.
  • CAPTCHA image / OCR, encore répandu sur les vieux systèmes judiciaires d'État, résolu en envoyant l'image encodée en base64.

CaptchaAI résout aussi reCAPTCHA v3, Cloudflare Turnstile et GeeTest v3, mais ces types restent rares côté juridique. À l'inverse, hCaptcha et FunCaptcha ne sont pas pris en charge : si un portail les impose, prévoyez une autre voie d'accès.

La facturation se fait au thread — un thread correspond à un CAPTCHA en cours de résolution, avec un nombre de résolutions illimité dans le mois. Une veille ponctuelle tient dans le plan BASIC ($15/mois, 5 threads) ; une collecte parallèle sur plusieurs portails passe au plan ADVANCE ($90/mois, 50 threads).


Construire le moteur de recherche de jurisprudence

Le cœur du scraper est une classe qui garde une session HTTP, détecte un CAPTCHA dans la réponse, appelle le solveur adapté, puis relance la requête avec le token. La logique reste identique d'une base à l'autre : parcourir les résultats, extraire les décisions, s'arrêter dès qu'une page est vide.

import requests
import time
import re
import base64
from bs4 import BeautifulSoup
import csv

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_recaptcha(sitekey, pageurl):
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    })
    task_id = resp.json()["request"]
    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


def solve_image_captcha(image_bytes):
    img_b64 = base64.b64encode(image_bytes).decode()
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "base64",
        "body": img_b64, "json": 1,
    })
    task_id = resp.json()["request"]
    for _ in range(20):
        time.sleep(3)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


class LegalResearchScraper:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
        })

    def search_cases(self, search_url, query, sitekey=None, max_pages=5):
        """Search case law database."""
        all_cases = []

        for page in range(max_pages):
            url = f"{search_url}?q={query}&page={page + 1}"
            resp = self.session.get(url, timeout=30)

            if self._has_captcha(resp.text):
                if sitekey:
                    token = solve_recaptcha(sitekey, url)
                    resp = self.session.post(url, data={
                        "q": query,
                        "g-recaptcha-response": token,
                    })
                else:
                    resp = self._solve_image_and_retry(resp.text, url, query)

            cases = self._parse_cases(resp.text)
            if not cases:
                break

            all_cases.extend(cases)
            print(f"Page {page + 1}: {len(cases)} cases")
            time.sleep(5)

        return all_cases

    def get_case_details(self, case_url):
        """Fetch full case details."""
        resp = self.session.get(case_url, timeout=30)

        if self._has_captcha(resp.text):
            sitekey = self._extract_sitekey(resp.text)
            if sitekey:
                token = solve_recaptcha(sitekey, case_url)
                resp = self.session.post(case_url, data={
                    "g-recaptcha-response": token,
                })

        soup = BeautifulSoup(resp.text, "html.parser")
        return {
            "title": self._text(soup, "h1, .case-title"),
            "citation": self._text(soup, ".citation, .case-cite"),
            "court": self._text(soup, ".court, .jurisdiction"),
            "date": self._text(soup, ".decision-date, .date-decided"),
            "judge": self._text(soup, ".judge, .authored-by"),
            "summary": self._text(soup, ".summary, .headnote"),
            "url": case_url,
        }

    def monitor_docket(self, docket_url, case_number, sitekey=None):
        """Monitor a specific case docket for new filings."""
        resp = self.session.get(docket_url, timeout=30)

        data = {"case_number": case_number}
        if sitekey and self._has_captcha(resp.text):
            token = solve_recaptcha(sitekey, docket_url)
            data["g-recaptcha-response"] = token

        resp = self.session.post(docket_url, data=data)
        return self._parse_docket(resp.text)

    def export_results(self, cases, filename):
        """Export case results to CSV."""
        if not cases:
            return
        with open(filename, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=cases[0].keys())
            writer.writeheader()
            writer.writerows(cases)

    def _has_captcha(self, html):
        return any(tag in html.lower() for tag in [
            'data-sitekey', 'g-recaptcha', 'captcha',
        ])

    def _extract_sitekey(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        return match.group(1) if match else None

    def _solve_image_and_retry(self, html, url, query):
        match = re.search(r'src="(/captcha[^"]+)"', html)
        if match:
            img_url = url.split("?")[0].rstrip("/") + match.group(1)
            img = self.session.get(img_url)
            answer = solve_image_captcha(img.content)
            return self.session.post(url, data={
                "q": query,
                "captcha": answer,
            })
        return self.session.get(url)

    def _parse_cases(self, html):
        soup = BeautifulSoup(html, "html.parser")
        cases = []
        for item in soup.select(".case-result, .search-result, tr.result"):
            title_el = item.select_one("a, .case-name")
            if title_el:
                cases.append({
                    "title": title_el.get_text(strip=True),
                    "url": title_el.get("href", ""),
                    "citation": self._text(item, ".citation, .cite"),
                    "date": self._text(item, ".date"),
                    "court": self._text(item, ".court"),
                })
        return cases

    def _parse_docket(self, html):
        soup = BeautifulSoup(html, "html.parser")
        entries = []
        for row in soup.select(".docket-entry, tr.filing"):
            entries.append({
                "date": self._text(row, ".date, td:first-child"),
                "entry": self._text(row, ".description, td:nth-child(2)"),
                "filed_by": self._text(row, ".filer, td:nth-child(3)"),
            })
        return entries

    def _text(self, el, selector):
        found = el.select_one(selector)
        return found.get_text(strip=True) if found else ""


# Usage
scraper = LegalResearchScraper(
    proxy="http://user:pass@residential.proxy.com:5000"
)

# Search case law
cases = scraper.search_cases(
    search_url="https://caselaw.example.com/search",
    query="data privacy GDPR",
    max_pages=5,
)

# Get details for relevant cases
for case in cases[:10]:
    if case["url"]:
        details = scraper.get_case_details(case["url"])
        print(f"{details['citation']}: {details['title']}")
        time.sleep(3)

# Export results
scraper.export_results(cases, "gdpr_cases.csv")

Trois méthodes portent l'essentiel du travail. search_cases parcourt les résultats et, dès qu'un défi apparaît, appelle solve_recaptcha ou bascule sur la résolution d'image. get_case_details répète la même logique pour ouvrir une décision, en extrayant le sitekey du HTML au besoin. export_results écrit le tout dans un CSV UTF-8. Le token reCAPTCHA revient toujours dans le champ g-recaptcha-response, exactement comme l'attend le formulaire.


Surveiller les nouveaux dépôts réglementaires

La recherche ponctuelle ne suffit pas quand il faut détecter le jour même une nouvelle décision ou un nouveau dépôt. La classe suivante réutilise le scraper pour interroger périodiquement une liste de flux et ne remonter que les entrées jamais vues, en s'appuyant sur la référence ou le titre comme clé de déduplication.

class RegulatoryMonitor:
    def __init__(self, proxy=None):
        self.scraper = LegalResearchScraper(proxy=proxy)
        self.seen_entries = set()

    def check_new_filings(self, feeds):
        """Check regulatory portals for new filings."""
        new_filings = []

        for feed in feeds:
            try:
                cases = self.scraper.search_cases(
                    feed["url"], feed["query"],
                    sitekey=feed.get("sitekey"),
                    max_pages=2,
                )

                for case in cases:
                    key = case.get("citation") or case.get("title")
                    if key and key not in self.seen_entries:
                        self.seen_entries.add(key)
                        case["source"] = feed["name"]
                        new_filings.append(case)

            except Exception as e:
                print(f"Error checking {feed['name']}: {e}")

            time.sleep(5)

        return new_filings

Chaque passage se limite à deux pages par flux (max_pages=2) : sur une surveillance récurrente, l'objectif est de repérer les nouveautés en tête de liste, pas de re-parcourir l'historique. En production, remplacez l'ensemble seen_entries par un magasin persistant (Redis, SQLite) pour survivre aux redémarrages.


Débit, proxys et conformité RGPD

Un scraper juridique qui fonctionne en démo peut se faire bannir en production s'il ignore le rythme du portail. Trois réglages font la différence :

  • Espacez les requêtes. Les temporisations (time.sleep) ne sont pas décoratives : PACER et les portails d'État coupent l'accès dès qu'ils détectent une cadence anormale. Un délai entre les pages et un backoff exponentiel après erreur gardent la collecte stable.
  • Faites tourner les proxys résidentiels. L'exemple injecte un proxy dans la session ; pour un volume soutenu, une rotation évite qu'une seule adresse concentre tout le trafic.
  • Minimisez les données personnelles. Les dossiers contiennent des noms, des adresses, parfois des données sensibles. Côté RGPD, ne collectez que les champs utiles à votre finalité et vérifiez vos obligations avant de constituer une base durable.

L'accès à un portail public ne vaut d'ailleurs pas autorisation d'en aspirer tout le contenu : respectez les conditions d'utilisation.


Dépannage

Problème Cause Correctif
Le CAPTCHA image échoue à répétition Texte trop déformé Signalez la réponse et relancez : une nouvelle image est servie
PACER bloque l'accès Limite de débit dépassée Attendez 30 min et réduisez la fréquence des requêtes
Détails de décision incomplets Contenu derrière un paywall Réglez les frais par page lorsque c'est requis
La recherche ne renvoie rien Une page CAPTCHA est renvoyée à la place Vérifiez la présence d'un CAPTCHA avant de parser
La veille rate des dépôts Intervalle de vérification trop long Rapprochez les passages de surveillance

FAQ

Quels types de CAPTCHA CaptchaAI résout-il sur les portails juridiques ?

Surtout reCAPTCHA v2 (méthode userrecaptcha) et les CAPTCHA image/OCR, qui couvrent PACER, EDGAR et les vieux systèmes d'État. hCaptcha et FunCaptcha ne sont pas pris en charge.

Comment éviter de se faire bloquer par PACER ou un portail judiciaire ?

Espacez les requêtes de plusieurs secondes, ajoutez un backoff exponentiel après chaque erreur et répartissez le trafic sur des proxys résidentiels. Un accès trop rapide déclenche des limites de débit, pas seulement des CAPTCHA.

Quel plan CaptchaAI choisir pour une collecte juridique en masse ?

Une veille légère tient dans le plan BASIC ($15/mois, 5 threads). Pour interroger plusieurs portails en parallèle, ADVANCE ($90/mois, 50 threads) autorise cinquante résolutions simultanées, sans limite du nombre de résolutions dans le mois.

La collecte automatisée de jurisprudence est-elle compatible avec le RGPD ?

Les décisions publiques sont généralement consultables, mais elles contiennent des données personnelles. Minimisez les champs collectés, documentez votre finalité et vérifiez vos obligations RGPD avant de constituer une base durable.


Guides connexes


Fluidifiez votre veille juridique : récupérez votre clé CaptchaAI et automatisez vos recherches de jurisprudence et de dépôts.

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