¿Necesitas estadísticas de jugadores, marcadores o clasificaciones de un portal deportivo que se bloquea con un CAPTCHA? La respuesta corta: detecta el desafío, resuélvelo con la API de CaptchaAI y reintenta la solicitud con el token que te devuelve. La mayoría de los portales de estadísticas se apoyan en Cloudflare Turnstile —y a veces en reCAPTCHA— que saltan cuando consultas muchos jugadores, temporadas o partidos seguidos, sobre todo en sitios que monetizan sus datos tras un muro de pago. En esta guía tienes el flujo completo, con código listo para copiar en Python y JavaScript.
Piensa en una agencia de analítica deportiva en Madrid o Ciudad de México que sigue a la vez La Liga, la Liga MX y la NBA para alimentar un panel de proyecciones. Con un coste mensual predecible en USD por thread —en lugar de pagar por cada resolución— puede planificar el gasto aunque facture a sus clientes en moneda local.
El flujo en tres pasos
Todo el código de esta guía sigue el mismo patrón, sin importar el deporte ni el portal:
- Detecta el desafío: un código 403 o la presencia de
cf-turnstileen el HTML te dice que hay un Turnstile en medio. - Resuelve el CAPTCHA: envías el
sitekeyy la URL a CaptchaAI y esperas el token con un sondeo corto. - Reintenta la solicitud original adjuntando el token en
cf-turnstile-response.
Con ese bucle claro, el resto es elegir cuándo recopilar y cómo aparcar los datos.
Qué CAPTCHA protegen los datos deportivos y por qué
Antes de escribir una línea de código conviene saber qué desafío te vas a encontrar en cada tipo de portal. El patrón se repite bastante: cuanto más rápido y automatizado es el acceso, más agresivo es el CAPTCHA.
| Tipo de dato | Tipo de portal | CAPTCHA | Qué lo dispara |
|---|---|---|---|
| Estadísticas de jugadores | Sitios de referencia | Cloudflare Turnstile | Cargas rápidas de fichas de jugador |
| Marcadores de partidos | Portales de resultados | Cloudflare Challenge | Consultas masivas de partidos |
| Clasificaciones de temporada | Sitios de liga | reCAPTCHA v2 | Navegación automatizada |
| Proyecciones de fantasy | Plataformas de fantasy | reCAPTCHA v3 | Accesos frecuentes tipo API |
| Cuotas de apuestas | Portales de cuotas | Cloudflare Turnstile | Actualizaciones de alta frecuencia |
| Registros históricos | Sitios de archivo | CAPTCHA de imagen | Peticiones de exportación de datos |
Planifica la recopilación según el calendario deportivo
La forma más sencilla de reducir los CAPTCHA es no pelearte con ellos: recopila cuando el portal está tranquilo. Cada deporte tiene su ritmo, y ese ritmo marca cuándo el tráfico automatizado destaca y cuándo pasa desapercibido.
| Deporte | Pico de volumen | Sensibilidad al CAPTCHA | Enfoque recomendado |
|---|---|---|---|
| Béisbol | Registros de juego diarios | Moderada | Recopila cuando terminen los partidos |
| Baloncesto | Noches de partido | Alta durante los partidos | Usa las horas de menor actividad |
| Fútbol americano | Partidos semanales | Baja entre semanas | Recolección masiva semanal |
| Fútbol | A diario en varias ligas | Moderada | Sesiones por liga |
| Hockey | Cada noche | Moderada | Recopila tras cada partido |
Recopilador de estadísticas en Python
Este recolector detecta la página de CAPTCHA, resuelve el Turnstile con CaptchaAI y reintenta la solicitud original con el token. Reutiliza una requests.Session() para conservar las cookies entre peticiones y así minimizar los desafíos.
import requests
import time
import re
from dataclasses import dataclass, field
@dataclass
class PlayerStats:
name: str
team: str
position: str
stats: dict = field(default_factory=dict)
season: str = ""
source: str = ""
class SportsDataCollector:
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 get_player_stats(self, portal_url, player_slug, season=None):
"""Fetch player statistics, solving CAPTCHAs as needed."""
url = f"{portal_url}/players/{player_slug}"
if season:
url += f"/{season}"
response = self.session.get(url)
if self._is_captcha_page(response):
response = self._solve_turnstile_and_retry(response, url)
return self._parse_player_stats(response.text)
def get_game_scores(self, portal_url, date):
"""Fetch all game scores for a specific date."""
url = f"{portal_url}/scores/{date}"
response = self.session.get(url)
if self._is_captcha_page(response):
response = self._solve_turnstile_and_retry(response, url)
return self._parse_scores(response.text)
def collect_team_roster(self, portal_url, team_slug, season):
"""Collect stats for all players on a team roster."""
roster_url = f"{portal_url}/teams/{team_slug}/{season}/roster"
response = self.session.get(roster_url)
if self._is_captcha_page(response):
response = self._solve_turnstile_and_retry(response, roster_url)
player_slugs = self._extract_player_links(response.text)
all_stats = []
for slug in player_slugs:
try:
stats = self.get_player_stats(portal_url, slug, season)
all_stats.append(stats)
time.sleep(2) # Respectful delay
except Exception as e:
print(f"Failed for {slug}: {e}")
return all_stats
def _is_captcha_page(self, response):
return (
response.status_code == 403 or
"cf-turnstile" in response.text or
"challenges.cloudflare.com" in response.text
)
def _solve_turnstile_and_retry(self, response, url):
match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
if not match:
raise ValueError("Turnstile sitekey not found")
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "turnstile",
"sitekey": match.group(1),
"pageurl": url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
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 self.session.post(url, data={
"cf-turnstile-response": data["request"]
})
raise TimeoutError("Turnstile solve timed out")
def _parse_player_stats(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
# Extract stat rows from tables
stats = {}
stat_table = soup.select_one("table.stats, #stats-table")
if stat_table:
headers = [th.text.strip() for th in stat_table.select("thead th")]
for row in stat_table.select("tbody tr"):
cells = [td.text.strip() for td in row.select("td")]
if len(cells) == len(headers):
for header, value in zip(headers, cells):
stats[header] = value
def text_or_empty(node):
return node.text.strip() if node and node.text else ""
return PlayerStats(
name=text_or_empty(soup.select_one("h1, .player-name")),
team=text_or_empty(soup.select_one(".team-name, .team")),
position=text_or_empty(soup.select_one(".position, .pos")),
stats=stats
)
def _parse_scores(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
games = []
def text_or_none(node):
return node.text.strip() if node and node.text else None
for game in soup.select(".game-card, .scoreboard-item"):
games.append({
"away": text_or_none(game.select_one(".away-team")),
"home": text_or_none(game.select_one(".home-team")),
"away_score": text_or_none(game.select_one(".away-score")),
"home_score": text_or_none(game.select_one(".home-score")),
"status": text_or_none(game.select_one(".game-status"))
})
return games
def _extract_player_links(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
links = []
for a in soup.select("a[href*='/players/']"):
slug = a["href"].rstrip("/").split("/")[-1]
if slug and slug not in links:
links.append(slug)
return links
# Usage
collector = SportsDataCollector("YOUR_API_KEY")
# Get player stats
stats = collector.get_player_stats(
"https://sports.example.com", "lebron-james", "2024"
)
print(f"{stats.name} ({stats.team}): {stats.stats}")
# Get all scores for a date
scores = collector.get_game_scores("https://sports.example.com", "2024-12-25")
for game in scores:
print(f"{game['away']} {game['away_score']} @ {game['home']} {game['home_score']}")
Agregador de temporada en JavaScript
Cuando quieres recorrer varios equipos de una temporada completa, un agregador asíncrono te deja espaciar las solicitudes y aislar los fallos por equipo. La lógica de resolución es la misma: detecta el Turnstile, pídele el token a CaptchaAI y reenvía la petición.
class SportsAggregator {
constructor(apiKey) {
this.apiKey = apiKey;
}
async collectSeasonData(portalUrl, sport, season, teams) {
const allData = {};
for (const team of teams) {
try {
const roster = await this.getTeamStats(portalUrl, team, season);
allData[team] = roster;
} catch (error) {
allData[team] = { error: error.message };
}
// Rate limit between teams
await new Promise(r => setTimeout(r, 3000));
}
return allData;
}
async getTeamStats(portalUrl, teamSlug, season) {
const url = `${portalUrl}/teams/${teamSlug}/${season}`;
const response = await fetch(url);
const html = await response.text();
if (html.includes('cf-turnstile') || response.status === 403) {
return this.solveAndFetch(url, html);
}
return this.parseTeamPage(html);
}
async solveAndFetch(url, html) {
const match = html.match(/data-sitekey="(0x[^"]+)"/);
if (!match) throw new Error('Turnstile sitekey not found');
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: new URLSearchParams({
key: this.apiKey,
method: 'turnstile',
sitekey: match[1],
pageurl: url,
json: '1'
})
});
const { request: taskId } = await submitResp.json();
for (let i = 0; i < 60; 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) {
const response = await fetch(url, {
method: 'POST',
body: new URLSearchParams({ 'cf-turnstile-response': data.request })
});
return this.parseTeamPage(await response.text());
}
}
throw new Error('Turnstile solve timed out');
}
parseTeamPage(html) {
const players = [];
const rowMatches = html.matchAll(/<tr[^>]*class="[^"]*player[^"]*"[^>]*>([\s\S]*?)<\/tr>/gi);
for (const row of rowMatches) {
const cells = [...row[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)]
.map(m => m[1].replace(/<[^>]+>/g, '').trim());
if (cells.length >= 3) {
players.push({
name: cells[0],
position: cells[1],
stats: cells.slice(2)
});
}
}
return { players, count: players.length };
}
}
// Usage
const aggregator = new SportsAggregator('YOUR_API_KEY');
const seasonData = await aggregator.collectSeasonData(
'https://sports.example.com', 'basketball', '2024',
['lakers', 'celtics', 'warriors']
);
Diagnóstico de problemas habituales
Casi todos los fallos en producción se reducen a cuatro causas. Esta tabla te da el atajo del síntoma a la solución.
| Problema | Causa | Solución |
|---|---|---|
| Turnstile en cada página | Sin persistencia de cookies | Conserva la cookie de sesión entre solicitudes con requests.Session() |
| La ficha del jugador muestra estadísticas distintas | Alterna entre temporada y carrera | Incluye el parámetro de temporada en la URL |
| Página de marcadores vacía | Partidos aún no jugados | Consulta el calendario antes de pedir los datos |
| Bloqueo tras 50 solicitudes | Tope diario del portal | Reparte las peticiones a lo largo del día desde una salida de red autorizada |
Preguntas frecuentes
¿Qué plan de CaptchaAI conviene para recopilar varias ligas a la vez?
Depende de cuántas resoluciones simultáneas necesites. Los planes se cobran por thread (un CAPTCHA en curso), con resoluciones ilimitadas por thread y sin tope diario. Con precios desde $15/mes (BASIC, 5 threads) cubres un recolector modesto; si agregas varias ligas en paralelo, ADVANCE ($90/mes, 50 threads) te da más margen de concurrencia.
¿Cómo evito que el Turnstile salte en cada solicitud?
Reutiliza la misma sesión. Al conservar las cookies entre peticiones, el portal te trata como una visita continua y no como una ráfaga de accesos nuevos. Combínalo con un ritmo humano (una pausa de 2 a 5 segundos entre solicitudes) y verás muchos menos desafíos.
¿Puedo guardar en caché las estadísticas históricas para no volver a resolver CAPTCHA?
Sí. Los registros históricos casi nunca cambian, así que recópilalos una vez y guárdalos en tu propia base de datos. Los CAPTCHA solo aparecerán durante la carga inicial; a partir de ahí sirves los datos desde tu caché sin tocar el portal.
¿Es legal automatizar la recopilación de estadísticas deportivas?
Depende del portal. Revisa siempre sus términos de servicio y respeta la normativa de protección de datos aplicable (GDPR y LOPDGDD en España, LFPDPPP en México). Limítate a datos públicos, mantén un ritmo razonable y no redistribuyas contenido con licencia sin permiso.
Artículos relacionados
- Recopilación de datos para investigación de mercado
- GeeTest frente a Cloudflare Turnstile: comparativa
- Cloudflare Turnstile devuelve 403 tras corregir el token
Próximos pasos
Recopila estadísticas deportivas sin frenarte en cada desafío. Obtén tu clave API de CaptchaAI y resuelve el Turnstile de los portales deportivos de forma automática.