Casos de Uso

Recopilación de datos de investigación de mercado con manejo de CAPTCHA

El informe de mercado no se rompe en el parser: se rompe la noche en que el colector devuelve doscientas páginas menos y nadie lo nota hasta la reunión. La causa suele ser la misma: la fuente empezó a servir un desafío CAPTCHA y tu script guardó esa página como si fuera contenido válido. Arreglarlo no exige rediseñar nada: detectas el desafío en el HTML, lo envías a la API de CaptchaAI, recuperas el token y reenvías la solicitud con él. El parseo y la exportación a CSV siguen igual.

Abajo tienes ese pipeline en Python, el ritmo que aguanta una ejecución diaria y el coste real al pasar de cien a mil páginas.

Dónde se rompe la recopilación (y por qué no lo ves)

Un colector que falla por CAPTCHA rara vez lanza una excepción: recibe un 200 OK, guarda el HTML y devuelve una lista vacía. Por eso el primer cambio útil no es resolver nada, sino registrar el motivo por página: URL, tamaño de la respuesta, filas extraídas y si había sitekey. Con eso separas los dos fallos que se parecen:

  • Desafío CAPTCHA: la página carga, pero trae un widget (data-sitekey, cf-turnstile). Se resuelve con la API.
  • Bloqueo por reputación de la IP o frecuencia de solicitudes: 403, 429 o respuestas vacías sin widget. Ahí se ajusta el ritmo, no el solver.

Qué desafío sirve cada tipo de fuente

Inventaría las fuentes antes de escribir código: el método cambia según el tipo.

Tipo de fuente Ejemplos Tipo de CAPTCHA
Sitios de reseñas G2, Trustpilot, Yelp reCAPTCHA v2/v3
Bolsas de trabajo LinkedIn, Indeed Cloudflare Challenge
Directorios de empresas YellowPages, Crunchbase Turnstile, reCAPTCHA
Redes sociales Twitter/X, Reddit Varios
Bases de datos de patentes USPTO, Google Patents reCAPTCHA v2
Datos gubernamentales Registros de la SEC, censo CAPTCHA de imagen

CaptchaAI cubre reCAPTCHA v2 y v3, Turnstile y Cloudflare Challenge, GeeTest v3 e imagen/OCR: justo el reparto de esa tabla. Y conviene saber qué queda fuera: hCaptcha y FunCaptcha (Arkose Labs) no son compatibles, GeeTest v4 figura como próximamente, y CaptchaFox (beta), Friendly Captcha (beta) y Lemin (beta) están solo en beta.

El pipeline de recopilación en Python

La pieza central es una función reutilizable: envía la tarea a in.php, sondea res.php cada 5 segundos mientras llegue CAPCHA_NOT_READY y devuelve el token. La clase colectora mantiene una Session de requests, detecta el sitekey y reenvía el formulario con el campo correcto: g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile. El token no se almacena: se pide, se usa y se descarta.

import requests
import time
import re
import csv
import os

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


def solve_captcha(params):
    params["key"] = API_KEY
    resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
    if not resp.text.startswith("OK|"):
        raise Exception(f"Submit: {resp.text}")

    task_id = resp.text.split("|")[1]
    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id,
        })
        if result.text == "CAPCHA_NOT_READY":
            continue
        if result.text.startswith("OK|"):
            return result.text.split("|", 1)[1]
        raise Exception(f"Solve: {result.text}")
    raise TimeoutError()


class MarketResearchCollector:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/120.0.0.0"
        )

    def fetch(self, url):
        """Fetch a page, solving CAPTCHAs as needed."""
        resp = self.session.get(url)

        # Detect reCAPTCHA
        match = re.search(
            r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text
        )
        if match:
            token = solve_captcha({
                "method": "userrecaptcha",
                "googlekey": match.group(1),
                "pageurl": url,
            })
            resp = self.session.post(url, data={
                "g-recaptcha-response": token,
            })

        # Detect Turnstile
        match = re.search(
            r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', resp.text
        )
        if match and "cf-turnstile" in resp.text:
            token = solve_captcha({
                "method": "turnstile",
                "sitekey": match.group(1),
                "pageurl": url,
            })
            resp = self.session.post(url, data={
                "cf-turnstile-response": token,
            })

        return resp.text

    def collect_reviews(self, urls):
        """Collect review data from multiple pages."""
        reviews = []
        for url in urls:
            try:
                html = self.fetch(url)
                page_reviews = self._parse_reviews(html)
                reviews.extend(page_reviews)
                print(f"  Collected {len(page_reviews)} reviews from {url}")
                time.sleep(2)  # Polite delay
            except Exception as e:
                print(f"  Error on {url}: {e}")
        return reviews

    def collect_company_profiles(self, urls):
        """Collect company profile data."""
        profiles = []
        for url in urls:
            try:
                html = self.fetch(url)
                profile = self._parse_profile(html)
                if profile:
                    profiles.append(profile)
                    print(f"  Collected: {profile.get('name', 'Unknown')}")
                time.sleep(2)
            except Exception as e:
                print(f"  Error on {url}: {e}")
        return profiles

    def _parse_reviews(self, html):
        """Extract review data from HTML."""
        reviews = []
        # Generic review extraction patterns
        for match in re.finditer(
            r'class="review-text"[^>]*>(.*?)</div>', html, re.DOTALL
        ):
            reviews.append({
                "text": match.group(1).strip()[:500],
            })
        return reviews

    def _parse_profile(self, html):
        """Extract company profile from HTML."""
        name = re.search(r'<h1[^>]*>(.*?)</h1>', html)
        desc = re.search(
            r'class="description"[^>]*>(.*?)</div>', html, re.DOTALL
        )
        return {
            "name": name.group(1).strip() if name else None,
            "description": desc.group(1).strip()[:300] if desc else None,
        }

    def export_csv(self, data, filename):
        """Export collected data to CSV."""
        if not data:
            return
        keys = data[0].keys()
        with open(filename, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=keys)
            writer.writeheader()
            writer.writerows(data)
        print(f"Exported {len(data)} records to {filename}")

Ejecutar el colector sobre un lote de URL

Con la clase montada, una campaña es una lista de URL y dos llamadas. Cada método atrapa sus excepciones para que una página caída no tire la ejecución, y exporta a CSV para que el analista trabaje sin abrir Python.

collector = MarketResearchCollector()

# Collect competitor reviews
review_urls = [
    "https://example-reviews.com/product/competitor-a",
    "https://example-reviews.com/product/competitor-b",
    "https://example-reviews.com/product/competitor-c",
]
reviews = collector.collect_reviews(review_urls)
collector.export_csv(reviews, "competitor_reviews.csv")

# Collect company profiles
profile_urls = [
    "https://example-directory.com/company/alpha-corp",
    "https://example-directory.com/company/beta-inc",
]
profiles = collector.collect_company_profiles(profile_urls)
collector.export_csv(profiles, "company_profiles.csv")

Cuatro escenarios donde esto se usa a diario

Precios y catálogo de la competencia

El caso más frecuente en España y Latinoamérica: seguir precios, promociones y disponibilidad en marketplaces como MercadoLibre o Amazon.es con una ejecución diaria de madrugada. Más detalle en la guía de monitoreo de precios en e-commerce.

Reputación de marca en plataformas de reseñas

Recopilas reseñas y calificaciones de varias plataformas y consolidas el sentimiento por producto y trimestre. Aquí el CAPTCHA no aparece de entrada, sino al paginar:

  • las primeras páginas de cada ficha pasan limpias;
  • el desafío empieza a partir de la tercera.

Señales del mercado laboral

Las ofertas publicadas son el indicador más rápido de dónde invierte un competidor. Una agencia con clientes en México y Argentina puede seguir vacantes, tecnologías pedidas y rangos salariales, con una advertencia: la región mezcla rangos en USD y en moneda local, así que normaliza antes de comparar.

Panorama de patentes e I+D

Las bases públicas de patentes permiten seguir la innovación de un sector. Son lentas y con protección de imagen: ejecútalas semanalmente.

Ritmo, concurrencia y programación

La estabilidad depende más del ritmo que del solver. Punto de partida, ajustable según la fuente:

Factor Recomendación
Separación entre solicitudes 2–5 segundos entre páginas
Colectores concurrentes 5–10 para escala moderada
Rotación de proxy Necesaria por encima de 100 páginas/hora
Deduplicación Hash del contenido antes de almacenar
Programación Diaria o semanal según la fuente

Por encima de cien páginas por hora contra un mismo dominio, la salida de red pasa a ser el cuello de botella: revisa la guía de rotación de proxy y cómo mantener un scraping fiable.

Cuánto cuesta: threads, no resoluciones

Aquí es donde más presupuestos se calculan mal. CaptchaAI no factura por CAPTCHA resuelto, sino por thread concurrente, con resoluciones ilimitadas dentro del plan. Un thread es un desafío en vuelo: al terminar, queda libre para el siguiente. No dimensionas resoluciones al mes, sino cuántas quieres en paralelo:

  • BASIC ($15/mes, 5 threads): recopilación diaria de unos cientos de páginas.
  • STANDARD ($30/mes, 15 threads): varias fuentes a la vez en la ventana nocturna.
  • ADVANCE ($90/mes, 50 threads): estudios continuos con miles de páginas al día.

Para un equipo que factura en moneda local, el atractivo es el coste mensual predecible en USD, no el pago por resolución.

Cumplimiento: los términos, antes que el HTML

La recopilación de datos públicos suele estar permitida, pero la responsabilidad de comprobarlo es tuya:

  1. Revisa los términos de servicio de la fuente y respeta su robots.txt.
  2. Cumple la normativa aplicable: RGPD y LOPDGDD en España, LFPDPPP en México y sus equivalentes en la región.
  3. No recopiles datos personales sin base legal.

Para validar tus propios flujos, ve a pruebas autorizadas con manejo de CAPTCHA.

Preguntas frecuentes

¿Qué tipos de CAPTCHA puedo resolver en estas fuentes?

reCAPTCHA v2 y v3, Turnstile, Cloudflare Challenge, GeeTest v3 e imagen/OCR. hCaptcha y FunCaptcha no son compatibles; GeeTest v4 figura como próximamente, y CaptchaFox, Friendly Captcha y Lemin están en beta.

¿Cuántos threads necesito para 1.000 páginas al día?

Depende de la concurrencia, no del total. Con 2–5 segundos de separación y 5–10 colectores en paralelo, los 5 threads de BASIC suelen bastar; con varias fuentes simultáneas, sube a STANDARD ($30/mes, 15 threads).

¿Necesito un navegador headless o me basta con requests?

Empieza con requests. Solo si la fuente construye el contenido con JavaScript y no devuelve nada útil sin renderizar compensa un navegador headless, que multiplica CPU y tiempo por página.

¿Puedo dejar la recopilación programada sin vigilarla?

Sí, si registras el motivo de cada página fallida y alertas cuando la proporción de páginas con desafío se dispare. Un salto repentino suele indicar un cambio de protección en la fuente, no un fallo de la API.

Guías relacionadas

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