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:
- Revisa los términos de servicio de la fuente y respeta su
robots.txt. - Cumple la normativa aplicable: RGPD y LOPDGDD en España, LFPDPPP en México y sus equivalentes en la región.
- 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.