Use Cases

Spor İstatistikleri Veri Toplama için CAPTCHA Kullanımı

Spor portallarında toplama işinizi durduran şey genellikle sayfa yapısı değil, üçüncü oyuncudan sonra devreye giren Cloudflare Turnstile doğrulamasıdır. Kısa cevap: doğrulamayı bir hata gibi değil, akışın normal bir adımı gibi ele alın — sayfayı çekin, data-sitekey değerini okuyun, token'ı CaptchaAI API'sine çözdürün, aynı oturumla isteği tekrarlayın. Bu yazıda bu döngünün spor verisine özgü halini, hangi portalın neyi tetiklediğini ve toplama takvimini maç saatlerine göre nasıl kuracağınızı bulacaksınız.

Fark şurada: spor verisinin bir saati vardır. Maçın son çeyreğinde skor portalına atılan istek ile ertesi sabah atılan istek aynı portalda farklı davranır. Toplama mimarinizi bu ritme göre kurarsanız çözmeniz gereken CAPTCHA sayısı kendiliğinden düşer.

Spor Portallarında CAPTCHA'yı Ne Tetikler?

Portal türüne göre karşınıza çıkan doğrulama tipi ve tetikleyici değişir:

Veri türü Portal türü CAPTCHA Tetikleyici
Oyuncu istatistikleri Referans siteleri Cloudflare Turnstile Hızlı ardışık oyuncu sayfası yüklemeleri
Maç skor kartları Skor portalları Cloudflare doğrulama akışı Toplu maç sorguları
Sezon puan durumu Lig siteleri reCAPTCHA v2 Otomatik sayfa gezinmesi
Fantezi lig projeksiyonları Fantezi platformlar reCAPTCHA v3 Sık, API benzeri erişim
Oranlar ve çizgiler Oran portalları Cloudflare Turnstile Yüksek frekanslı yenileme
Tarihsel kayıtlar Arşiv siteleri Resim CAPTCHA'sı Veri dışa aktarma istekleri

Bu satırların neredeyse tamamı tek bir kalıba iner: istek hızı. Referans siteleri, oyuncu sayfalarını saniyeler içinde tarayan bir istemciyi insan trafiğinden ayırmak için Turnstile'ı devreye sokar. Bu yüzden tek bir çözme çağrısı yetmez; oturum sürekliliği, gecikme ve çözüm birlikte kurulmalıdır.

Toplanan verinin kişisel veri içerip içermediğini de baştan netleştirin. Oyuncu isimleri ve maç istatistikleri kamuya açık kayıtlardır; kullanıcı yorumları, forum profilleri veya fantezi lig katılımcı listeleri ise KVKK kapsamına girebilir. Teknik olarak erişilebilir olması, saklanabilir olduğu anlamına gelmez.

Turnstile Çözümünü Toplayıcıya Bağlayın (Python)

Aşağıdaki toplayıcı, _is_captcha_page ile doğrulama sayfasını tanır, _solve_turnstile_and_retry ile sitekey'i çıkarıp in.php uç noktasına gönderir ve res.php üzerinden sonucu sorgular. Dönen cf-turnstile-response değeri, isteği başlatan aynı requests.Session nesnesiyle geri gönderilir — token'ı yeni bir oturumla göndermek en sık yapılan hatadır ve 403 ile döner.

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

Koddaki time.sleep(2) satırı süs değildir: kadro genelinde oyuncu sayfalarını tararken istekler arasına konan bu gecikme, portalın hız eşiğini tetikleme olasılığını düşürür ve dolayısıyla çözmeniz gereken CAPTCHA sayısını azaltır. Sorgulama döngüsü 60 tur × 3 saniye ile 180 saniyede sınırlanır; süre dolduğunda TimeoutError fırlatılır, böylece toplu iş sessizce asılı kalmaz.

Sezon Verisini Takım Takım Toplayın (JavaScript)

Tek oyuncu yerine bütün bir sezonu çekiyorsanız, hata izolasyonu asıl mesele haline gelir. Aşağıdaki toplayıcı her takımı ayrı try/catch içinde işler: bir takımın sayfası doğrulamada takılırsa diğerleri etkilenmez, hata mesajı sonuç nesnesine yazılır ve iş devam eder. Takımlar arasındaki 3 saniyelik bekleme de aynı mantığın devamıdır.

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

Toplama Takvimini Maç Saatlerine Göre Kurun

Spor verisinde en büyük kazanç, kodu değil zamanlamayı optimize etmekten gelir. Portalın yükü düştüğünde doğrulama eşikleri de gevşer:

Spor Yoğun veri anı CAPTCHA duyarlılığı Önerilen yaklaşım
Beyzbol Günlük maç kayıtları Orta Maçlar bittikten sonra toplayın
Basketbol Maç geceleri Maç sırasında yüksek Yoğun olmayan saatleri kullanın
Amerikan futbolu Haftalık maçlar Maçlar arasında düşük Haftalık toplu gönderim
Futbol Ligler arasında her gün Orta Lig başına ayrı oturum
Buz hokeyi Her gece Orta Maç sonrası toplama

Türkiye'den çalışan bir ekip için pratik sonuç: Süper Lig ve Avrupa kupası maçları Europe/Istanbul saatiyle çoğunlukla 19:00–23:00 arasında oynanır; doğrulama baskısı da tam o pencerede zirve yapar. Aynı veriyi ertesi sabah çekmek hem daha az CAPTCHA hem daha kararlı yanıt süresi demektir. Kuzey Amerika ligleri (NBA, NHL, MLB) ise Türkiye sabahında zaten "maç sonrası" durumdadır — iki takvim çakışmadığı için tek toplayıcı ikisini de sırayla işler.

Zamanlamayla kazanamadığınız yerde plan seviyesi devreye girer. CaptchaAI thread tabanlı çalışır: fatura çözüm başına değil, eşzamanlı thread başına hesaplanır ve her planda thread başına sınırsız çözüm vardır. Tek ligi günde bir kez tarayan bir iş için BASIC ($15/ay, 5 thread) fazlasıyla yeter; on beş ligi paralel işleyen bir veri sağlayıcı için ADVANCE ($90/ay, 50 thread) daha gerçekçi bir başlangıçtır. TL kurundaki oynaklık düşünüldüğünde, aylık maliyetin USD cinsinden ve hacimden bağımsız sabit kalması bütçe planlamasını belirgin biçimde kolaylaştırır.

Sorun Giderme

Sorun Sebep Düzeltme
Her sayfada Turnstile çıkıyor Oturum çerezi taşınmıyor Tüm istekleri tek Session üzerinden geçirin, qa_session_cookie'yi koruyun
Token gönderiliyor ama 403 dönüyor Token farklı oturumdan gönderildi Çözümü isteği başlatan oturumla gönderin, 60 saniye içinde kullanın
Oyuncu sayfası farklı istatistik gösteriyor Sezon/kariyer sekmesi Sezon parametresini URL'ye ekleyin
Skor sayfası boş dönüyor Maç henüz oynanmadı Sorgulamadan önce fikstürü kontrol edin
50 istekten sonra hız sınırı Portalın günlük eşiği İstekleri saatlere yayın, eşzamanlılığı düşürün
Turnstile sitekey not found Sayfa iframe ile geliyor Ham HTML'i loglayın, data-sitekey yerine widget parametrelerini kontrol edin

Hangi CAPTCHA Türleri Kapsam İçinde?

Spor portallarında karşınıza en çok Cloudflare Turnstile, Cloudflare doğrulama akışı ve reCAPTCHA v2/v3 çıkar; arşiv sitelerinde ek olarak klasik resim CAPTCHA'sı görürsünüz. Bunların tamamı CaptchaAI tarafından desteklenir; GeeTest v3 de aynı kapsamdadır. GeeTest v4 desteği ise çok yakında geliyor, henüz kullanılabilir değil. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor — bir portal bu ikisinden birini kullanıyorsa, o kaynağı toplama planınızın dışında tutmanız gerekir. CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) ise beta aşamasındadır.

Pratik sonuç: toplama listenizi kurarken kaynakları doğrulama türüne göre etiketleyin. Aynı in.php / res.php akışı yalnızca method parametresi değişerek çalıştığı için, desteklenen türleri tek bir toplayıcıyla işleyebilirsiniz; desteklenmeyen bir türü kullanan portalı ise ayrı bir kovaya alıp manuel süreçle veya resmî veri sağlayıcısıyla çözmeniz gerekir.

SSS

Turnstile token'ı ne kadar süre geçerli kalır?

Pratikte kısa ömürlüdür — çözümü aldıktan sonraki ilk 60 saniye içinde hedef isteğe eklemeniz gerekir. Toplayıcınız token'ı alıp kuyruğa koyuyorsa, kuyrukta beklerken süresi dolar ve portal 403 döner.

Aynı anda kaç oyuncu sayfasını paralel tarayabilirim?

Eşzamanlılığınızın tavanı planınızdaki thread sayısıdır: BASIC ($15/ay, 5 thread) ile aynı anda beş çözüm işlemi yürütebilirsiniz. Ancak asıl sınır genellikle portalın kendi hız eşiğidir — thread'i artırmadan önce gecikmeleri ölçün.

Portal desteklenmeyen bir CAPTCHA türü kullanıyorsa ne yaparım?

Önce türü doğrulayın: sayfa kaynağında h-captcha geçiyorsa hCaptcha, Arkose Labs betiği yükleniyorsa FunCaptcha vardır; ikisi de desteklenmez. Bu kaynakları listenizden ayırıp resmî veri sağlayıcısı üzerinden çözün.

Geçmiş sezon verilerini her seferinde yeniden çekmem gerekir mi?

Hayır. Tarihsel istatistikler nadiren değişir; bir kez toplayıp yerelde önbelleğe alın. CAPTCHA yükünün tamamı ilk toplu tarama sırasında oluşur, sonraki çalışmalarda yalnızca güncel sezonu sorgularsınız.

Resmî lig API'leri CAPTCHA çözme ihtiyacını ortadan kaldırır mı?

Kısmen. Bazı ligler resmî API sunar; bunlar genelde pahalıdır, hız sınırlıdır ve referans sitelerdeki gelişmiş metrikleri içermez. API'yi omurga olarak kullanıp eksik metrikleri korumalı kaynaklardan tamamlamak en dengeli kurulumdur.

İlgili Makaleler

Sonraki Adımlar

Toplayıcınızı doğrulama sayfasında durmayacak şekilde kurun: CaptchaAI API anahtarınızı alın ve ilk Turnstile çözümünüzü bugün akışa ekleyin.

Bu makale için yorumlar devre dışı bırakılmıştır.