Casos de Uso

Web Scraping de investigación jurídica con manejo de CAPTCHA

Un equipo de tecnología legal que rastrea cientos de resoluciones al día rara vez se detiene por el scraping en sí: se detiene cuando el buscador judicial devuelve un reCAPTCHA v2 en lugar de resultados. Ese es el cuello de botella real de la investigación jurídica automatizada. CaptchaAI resuelve los dos tipos que dominan estos portales —reCAPTCHA v2 y el CAPTCHA de imagen (OCR)— para que tu pipeline siga corriendo mientras buscas jurisprudencia, vigilas expedientes y agregas datos regulatorios.


Portales jurídicos que usan CAPTCHA y de qué tipo

Antes de escribir una sola línea de scraping conviene saber contra qué CAPTCHA vas a chocar en cada fuente. La mayoría de los repositorios legales se reparten entre reCAPTCHA v2 y CAPTCHA de imagen antiguos:

Fuente Tipo de CAPTCHA Datos Quién lo usa
PACER reCAPTCHA v2 Presentaciones judiciales federales Equipos de litigio
Sistemas judiciales estatales CAPTCHA de imagen / reCAPTCHA Registros de casos estatales Abogados
SEC EDGAR reCAPTCHA v2 Presentaciones corporativas Cumplimiento
Bases de datos de patentes reCAPTCHA v2 Registros de patentes Investigadores de propiedad intelectual
Portales regulatorios CAPTCHA de imagen Normas y guías Cumplimiento
Bases de citas jurídicas reCAPTCHA v2 Citas de casos Legal tech
Directorios de colegios de abogados reCAPTCHA v2 Registros de letrados Debida diligencia

La conclusión práctica: si resuelves bien reCAPTCHA v2 e imagen/OCR, cubres la mayor parte del mapa, y CaptchaAI atiende ambos con la misma clave API.


Buscador de jurisprudencia con Python

El siguiente scraper cubre el flujo completo: detecta el CAPTCHA en la página, lo envía a CaptchaAI, recupera el token y reintenta la solicitud ya validada. La clase LegalResearchScraper separa la búsqueda, el detalle del caso y el seguimiento de expedientes, y exporta a CSV.

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")

El detalle clave: cuando la página trae un reCAPTCHA v2, extraes el sitekey, obtienes el token g-recaptcha-response de CaptchaAI y reenvías la búsqueda con ese token en el POST. Si aparece un CAPTCHA de imagen, _solve_image_and_retry descarga la imagen, la manda en base64 y adjunta la respuesta como texto. La pausa de cinco segundos entre páginas mantiene tu ritmo por debajo de lo que la mayoría de los portales considera abuso.


Monitoreo regulatorio continuo

La investigación jurídica pocas veces es un rastreo único. Cumplimiento y estudios de abogados necesitan enterarse de una nueva presentación el mismo día en que aparece. RegulatoryMonitor reutiliza el scraper anterior, guarda lo ya visto en memoria y solo devuelve las novedades, de modo que puedes programarlo cada hora sin duplicar alertas.

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

Para producción, sustituye el set en memoria por Redis o una tabla en base de datos, de modo que el estado sobreviva a un reinicio.


Escenario: seguimiento de jurisprudencia en el mundo hispanohablante

El mismo patrón se traslada tal cual a los portales oficiales de habla hispana. El CENDOJ (buscador de jurisprudencia del Consejo General del Poder Judicial en España), el BOE, el Diario Oficial de la Federación en México o el SAIJ en Argentina publican resoluciones y normativa de acceso público, y muchos de sus buscadores se protegen con reCAPTCHA o CAPTCHA de imagen. Un despacho que sigue cambios regulatorios en varios países puede apuntar RegulatoryMonitor a cada portal y consolidar las novedades en un CSV o panel.

Dos advertencias válidas en toda la región: respeta los términos de servicio del portal y la normativa de protección de datos aplicable —GDPR y LOPDGDD en España, LFPDPPP en México o su equivalente— y limita tu ritmo a lo que el sitio tolera. Automatizar el acceso a información pública es legítimo; saturar el servicio no lo es.


Cómo evitar bloqueos y errores comunes

La mayoría de los fallos en producción no vienen del solver, sino de detalles operativos: confundir una página CAPTCHA con resultados vacíos, o disparar demasiadas solicitudes seguidas. Esta tabla resume lo más habitual:

Problema Causa Solución
El CAPTCHA de imagen falla una y otra vez Texto muy distorsionado Reporta y reintenta con una imagen nueva
PACER bloquea el acceso Límite de solicitudes superado Espera 30 minutos y baja la frecuencia de solicitudes
Detalles del caso incompletos Contenido tras un muro de pago Paga la tarifa por página cuando sea imprescindible
La búsqueda no devuelve nada Llegó la página CAPTCHA en lugar de resultados Verifica si hay CAPTCHA antes de parsear
El seguimiento omite presentaciones Intervalo de control demasiado largo Reduce el intervalo y aumenta la frecuencia de control

La regla de oro es introducir esperas entre solicitudes y reintentos con retroceso exponencial cuando un portal empieza a responder con lentitud: es más barato ir despacio que quedar en una lista de bloqueo.


Qué plan de CaptchaAI se ajusta a la investigación jurídica

CaptchaAI cobra por thread concurrente, no por CAPTCHA resuelto, y cada plan incluye resoluciones ilimitadas dentro del mes: un thread es un CAPTCHA en curso y, al terminar, queda libre para el siguiente. Para agencias y freelancers de la región que facturan en monedas volátiles, este costo mensual predecible en USD es más fácil de presupuestar que un esquema de pago por resolución.


Preguntas frecuentes

¿Qué tipos de CAPTCHA aparecen en los portales jurídicos y cuáles resuelve CaptchaAI?

Predominan reCAPTCHA v2 en los tribunales federales y de citas, y CAPTCHA de imagen (OCR) en los sistemas estatales antiguos. CaptchaAI resuelve ambos, además de reCAPTCHA v3, grid, Cloudflare Turnstile y GeeTest v3. No resuelve hCaptcha ni FunCaptcha: verifica el tipo antes de integrar.

¿Automatizar la consulta de registros judiciales cumple con la normativa de protección de datos?

Depende del portal y del dato. Los registros judiciales públicos suelen ser accesibles, pero debes respetar los términos de servicio del sitio y las leyes aplicables —GDPR y LOPDGDD en España, LFPDPPP en México y equivalentes—. No es asesoría legal: ante datos personales sensibles, consúltalo con tu equipo jurídico.

¿Cómo controlo el ritmo para no saturar el portal ni que me bloqueen?

Introduce pausas fijas entre páginas (el ejemplo usa cinco segundos), aplica reintentos con retroceso exponencial y respeta cualquier límite de solicitudes publicado. Si un portal empieza a devolver errores o CAPTCHA en cadena, baja la frecuencia antes de que te incluya en una lista de bloqueo.

¿Qué plan de CaptchaAI conviene para investigación jurídica a gran escala?

Para un solo scraper secuencial, BASIC ($15/mes, 5 threads) suele bastar. Si monitoreas varios portales en paralelo o corres varios workers, sube a STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads): al ser ilimitado por thread, pagas por concurrencia, no por volumen de resoluciones.


Guías relacionadas


Agiliza tu investigación jurídica: obtén tu clave de CaptchaAI y automatiza las búsquedas de jurisprudencia y el seguimiento de expedientes.

Los comentarios están deshabilitados para este artículo.