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.