Portal statistik olahraga melindungi database pemain, log permainan, dan catatan sejarah dengan Cloudflare Turnstile dan reCAPTCHA. CAPTCHA terpicu selama cookieri cepat di beberapa pemain, musim, atau pertandingan – terutama di situs yang memonetisasi data mereka di balik paywall. Berikut cara mengumpulkan data olahraga dengan andal.
Pola CAPTCHA di Portal Olahraga
| Tipe data | Jenis portal | CAPTCHA | Pemicu |
|---|---|---|---|
| Statistik pemain | Situs referensi | Cloudflare Turnstile | Pemuatan halaman pemutar cepat |
| Skor kotak permainan | Portal skor | Cloudflare Challenge | Pencarian game massal |
| Klasemen musim | Situs liga | reCAPTCHA v2 | Navigasi otomatis |
| Proyeksi fantasi | Platform fantasi | reCAPTCHA v3 | Akses seperti API yang sering |
| Peluang taruhan/lines | Portal peluang | Cloudflare Turnstile | Penyegaran frecookiensi tinggi |
| Catatan sejarah | Situs arsip | Gambar CAPTCHA | Request ekspor data |
Pengumpul Data Olahraga
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']}")
Agregator Data Musim (JavaScript)
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']
);
Strategi Pengumpulan berdasarkan Olahraga
| Olahraga | Volume data puncak | Sensitivitas CAPTCHA | Pendekatan yang disarankan |
|---|---|---|---|
| Bisbol | Log permainan harian | Sedang | Kumpulkan setelah pertandingan berakhir |
| Bola Basket | Malam permainan | Tinggi selama pertandingan | Gunakan jam di luar jam sibuk |
| Sepak bola | Pertandingan mingguan | Rendah di antara pertandingan | Pengumpulan massal mingguan |
| Sepak Bola | Setiap hari di seluruh liga | Sedang | Sesi per liga |
| Hoki | Setiap malam | Sedang | Koleksi pasca pertandingan |
Pemecahan Masalah
| Masalah | Penyebab | Solusi |
|---|---|---|
| Turnstile di setiap halaman | Tidak ada kegigihan cookie | Pertahankan qa_validation_cookie dalam sesi |
| Halaman pemain menunjukkan statistik yang berbeda | Tombol Musim/career | Sertakan parameter musim di URL |
| Halaman skor kosong | Permainan belum dimainkan | Periksa jadwal permainan sebelum bertanya |
| Tarif dibatasi setelah 50 request | Batas harian Portal | Distribusikan ke seluruh waktu, gunakan proxy |
Pertanyaan Umum
Seberapa sering saya harus mengumpulkan data permainan langsung?
Selama permainan aktif, skor diperbarui setiap beberapa menit. Gunakan interval 2–5 menit dengan persistensi sesi untuk meminimalkan CAPTCHA. CaptchaAI menangani Turnstile dengan tingkat keberhasilan 100%.
Bisakah saya mengumpulkan statistik historis selama beberapa musim?
Ya. Data historis jarang berubah, jadi kumpulkan sekali dan simpan dalam cache secara lokal. CAPTCHA dapat terpicu selama pengumpulan massal awal namun tidak akan memengaruhi data yang disimpan.
Apakah API olahraga menghilangkan kebutuhan akan solve CAPTCHA?
Beberapa liga menawarkan API resmi, tetapi sering kali harganya mahal, tarifnya terbatas, atau tidak ada statistik lanjutan yang tersedia di situs referensi. CaptchaAI memungkinkan Anda mengakses sumber data tambahan yang dilindungi oleh CAPTCHA.
Artikel Terkait
- Pengumpulan Data Riset Pasar
- Perbandingan Geetest Vs Cloudflare Turnstile
- Cloudflare Turnstile 403 Setelah Perbaikan Token
Langkah Selanjutnya
Kumpulkan data olahraga dengan andal –dapatkan kunci API CaptchaAI Andadan menangani CAPTCHA portal olahraga secara otomatis.