Casos de Uso

Manejo de CAPTCHA para la recopilación de datos de estadísticas deportivas

¿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:

  1. Detecta el desafío: un código 403 o la presencia de cf-turnstile en el HTML te dice que hay un Turnstile en medio.
  2. Resuelve el CAPTCHA: envías el sitekey y la URL a CaptchaAI y esperas el token con un sondeo corto.
  3. 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.

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

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.

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