Anwendungsfälle

CAPTCHA-Verarbeitung für die Erfassung von Sportstatistikdaten

Sie ziehen Spielerprofile, Tabellenstände und Spielprotokolle über eine komplette Saison – und ab der zwanzigsten Anfrage blendet Cloudflare Turnstile vor jeder weiteren Seite eine Abfrage ein. Der zuverlässige Weg: Sie übergeben die CAPTCHA-Abfrage an CaptchaAI, setzen das zurückgelieferte Token in derselben Sitzung ein und führen den Abruf ohne Unterbrechung fort.

Sportstatistikportale schützen ihre Spielerdatenbanken und historischen Archive genau dann, wenn Sie sie am dringendsten brauchen: bei schnellen Abfragen über viele Spieler, Saisons oder Ligen hinweg. Besonders empfindlich reagieren Anbieter, die ihre Daten hinter einer Paywall monetarisieren. Dieser Leitfaden zeigt den kompletten Sammel-Workflow mit Python und Node.js.

Das Wesentliche:

  • Turnstile ist der häufigste Schutz auf Sportportalen; reCAPTCHA v2/v3 und Bild-CAPTCHAs kommen ergänzend vor.
  • CaptchaAI löst die Abfrage, Ihr Skript setzt das zurückgelieferte Token in derselben Sitzung ein.
  • Stabile Session-Cookies und das Sammeln außerhalb der Stoßzeiten senken die Zahl der Abfragen spürbar.

Welche CAPTCHA-Typen auf Sportdatenportalen auftreten

Welche Abfrage Sie treffen, hängt vom Portaltyp und vom Abrufmuster ab:

Datentyp Portaltyp CAPTCHA Auslöser
Spielerstatistiken Referenzseiten Cloudflare Turnstile Schnelles Laden der Spielerseite
Spielbox-Ergebnisse Punkteportale Cloudflare Challenge Massensuche nach Spielen
Saisonstand Ligaseiten reCAPTCHA v2 Automatisierte Navigation
Fantasieprojektionen Fantasy-Plattformen reCAPTCHA v3 Häufiger API-ähnlicher Zugriff
Wettquoten/lines Quotenportale Cloudflare Turnstile Hochfrequenzaktualisierungen
Historische Aufzeichnungen Archivseiten Bild-CAPTCHA Anfragen zum Datenexport

Turnstile dominiert auf Sportseiten, weil es schnell und mit geringer Nutzerreibung ausgeliefert wird. CaptchaAI löst Turnstile ebenso wie reCAPTCHA v2/v3 und Bild-CAPTCHAs; hCaptcha und FunCaptcha unterstützt der Dienst dagegen nicht.

Sportdaten-Collector in Python

Der folgende Collector prüft jede Antwort auf eine Turnstile-Abfrage, löst sie bei Bedarf über CaptchaAI und liest anschließend die Statistiktabellen mit BeautifulSoup aus:

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']}")

Saisondaten aggregieren mit Node.js

Für saisonweite Läufe über mehrere Teams eignet sich ein Aggregator in Node.js. Er hält zwischen den Teams eine Pause ein, damit die Abrufrate niedrig bleibt und seltener Abfragen ausgelöst werden:

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']
);

Beide Skripte folgen demselben zweistufigen Muster: Erst wird geprüft, ob die Antwort eine Turnstile-Abfrage enthält, dann wird nur bei Bedarf gelöst. Das Token, das CaptchaAI zurückgibt, ist an die Seite gebunden und läuft nach kurzer Zeit ab – setzen Sie es deshalb sofort in derselben Sitzung ein und puffern Sie es nicht für spätere Anfragen zwischen.

Sammelstrategie nach Sportart

Die Empfindlichkeit einer Quelle hängt stark vom Spielrhythmus ab. Wer etwa Bundesliga-Daten einmal pro Woche abzieht, trifft auf deutlich weniger Abfragen als jemand, der Basketball-Ergebnisse minütlich aktualisiert. Historische Werte ändern sich ohnehin kaum: Erfassen Sie sie einmal, speichern Sie sie lokal zwischen und beschränken Sie wiederkehrende Läufe auf die aktuelle Saison – das senkt sowohl die Zahl der Abfragen als auch die Kosten.

Sportart Spitzendatenvolumen CAPTCHA-Empfindlichkeit Empfohlener Ansatz
Baseball Tägliche Spielprotokolle Mäßig Nach Spielende einsammeln
Basketball Spieleabende Hoch während der Spiele Zeiten außerhalb der Stoßzeiten nutzen
American Football Wöchentliche Spiele Niedrig zwischen den Spielen Wöchentliche Sammelläufe
Fußball Täglich über Ligen hinweg Mäßig Sitzungen pro Liga trennen
Eishockey Nächtliche Spiele Mäßig Sammlung nach dem Spiel

Hinweis: IP-Adressen und Klarnamen von Spielern gelten als personenbezogene Daten. Prüfen Sie vor produktiven Läufen Ihre Rechtsgrundlage nach DSGVO sowie die Nutzungsbedingungen der jeweiligen Quelle.

Typische Probleme und ihre Lösung

Problem Ursache Lösung
Turnstile bei jeder Seite Keine Cookie-Persistenz Halten Sie das cf_clearance-Cookie in der Session
Zu viele Abfragen in kurzer Zeit Abrufrate oder Parallelität ist für die Quelle zu aggressiv Drosseln Sie die Intervalle, halten Sie Sessions stabil und prüfen Sie die Qualität Ihrer Proxys
Daten fehlen trotz gelöster Abfrage Der Parser liest eine veraltete oder unvollständige Ansicht aus Extrahieren Sie erst nach erfolgreicher Token-Anwendung in derselben Sitzung
Kosten steigen stärker als erwartet Zu viele Wiederholungen oder unnötige Seitenaufrufe lösen zusätzliche Abfragen aus Lösen Sie nur kritische Schritte und protokollieren Sie Wiederholungen pro Quelle

Häufige Fragen

Welche CAPTCHA-Typen kommen auf Sportportalen am häufigsten vor?

Meist Cloudflare Turnstile, dazu reCAPTCHA v2 und v3 sowie vereinzelt Bild-CAPTCHAs beim Datenexport. CaptchaAI löst alle diese Typen zuverlässig; hCaptcha und FunCaptcha gehören nicht zum unterstützten Umfang.

Wie viele Portale kann ich gleichzeitig abfragen?

So viele, wie Ihr Plan an Threads bereitstellt – abgerechnet wird pro gleichzeitiger Abfrage, nicht pro Lösung. BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Lösungen; für saisonweite Läufe über viele Ligen passt ADVANCE (90 $/Monat, 50 Threads) besser.

Ist das Erfassen von Sportstatistiken in der DACH-Region rechtlich zulässig?

Das hängt vom Einzelfall ab. Prüfen Sie die Nutzungsbedingungen der Quelle, halten Sie sich an die robots.txt und beachten Sie: Sobald personenbezogene Daten wie Klarnamen von Spielern verarbeitet werden, greift die DSGVO. Klären Sie Ihre Rechtsgrundlage vor dem produktiven Einsatz.

Brauche ich Proxys für die Sportdatenerfassung?

Für kleine, gelegentliche Läufe nicht zwingend. Bei hoher Frequenz oder vielen parallelen Sessions verteilen Residential- oder Rechenzentrums-Proxys die Last und senken die Zahl der ausgelösten Abfragen. Achten Sie dabei auf eine stabile Zuordnung von Proxy zu Session.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.