Use Cases

Borsa verisi toplarken CAPTCHA yönetimi: Turnstile ve reCAPTCHA

Borsa verisi toplayan bir işte CAPTCHA'yı hata olarak değil, planlanmış bir adım olarak ele alın: isteği gönderin, doğrulama sayfasını tanıyın, sitekey değerini çıkarın, token'ı alın ve aynı oturumda kaldığınız yerden devam edin. Kapanış sonrası fiyat çekimini yarım saat geciktiren şey genellikle portalın sert koruması değil, bu adımın koda gömülmemiş olmasıdır. Aşağıda finansal portallarda en sık karşılaşılan doğrulama türlerini, çalışan bir Python toplayıcısını, JavaScript tarafında eleme sorgularını ve toplama ritmini bozmadan çözümü yerleştirmenin yolunu bulacaksınız.

Finansal portallarda CAPTCHA nerede tetiklenir?

Veri türü Portal örnekleri CAPTCHA türü Tetikleyici
Anlık fiyat verisi Finans portalları Cloudflare Turnstile Hızlı sembol aramaları
Geçmiş fiyatlar Veri sağlayıcılar reCAPTCHA v2 Toplu CSV indirmeleri
Mali tablolar SEC dosyalama siteleri Resim CAPTCHA'sı Tekrarlanan EDGAR sorguları
Eleme sonuçları Hisse eleme araçları Cloudflare doğrulama akışı Karmaşık filtre sorguları
Analist derecelendirmeleri Araştırma portalları reCAPTCHA v3 Çoklu sayfa görüntülemeleri

Pratikte yük tek bir türden gelmez: aynı toplayıcı gün içinde Turnstile'ı, akşam toplu indirmede reCAPTCHA v2'yi, arşiv sorgularında da resim CAPTCHA'sını görür. Bu beş satırın tamamı CaptchaAI'nin genel kullanıma açık türleri arasındadır; reCAPTCHA v2/v3, Cloudflare Turnstile, Cloudflare doğrulama akışı, resim/OCR ve GeeTest v3 destekleniyor, hCaptcha ve FunCaptcha ise desteklenmiyor. Bu yüzden toplayıcınızı tek bir doğrulama türüne göre değil, sayfayı okuyup türü ayırt eden bir algılama katmanına göre kurun.

Veri türüne göre toplama ritmi

Veri türü Önerilen aralık CAPTCHA sıklığı
Anlık fiyat verisi 1–5 dakika Yüksek — varsa resmî API'yi kullanın
Gün sonu fiyatları Kapanıştan sonra günde bir kez Düşük
Mali tablolar Üç ayda bir Asgari
Eleme sonuçları Günlük Orta
Analist derecelendirmeleri Haftalık Düşük

Ritmi zaman dilimine göre kurgulayın. Europe/Istanbul saatiyle çalışan bir ekip için Borsa İstanbul seansı ile ABD piyasalarının açılışı aynı güne sığar; anlık fiyat toplama penceresi bu iki blokta yoğunlaşır, ağır toplu indirmeler ise kapanış sonrasına bırakılır. Toplu işleri gece çalıştırdığınızda hem doğrulama sıklığı düşer hem de eş zamanlı thread ihtiyacınız azalır. Zamanlayıcıda tarih ve saat değerlerini tr-TR yerel ayarıyla değil ISO biçiminde saklayın; yerel biçimlendirmeyi yalnızca raporlama katmanında uygulayın.

Python ile borsa verisi toplayıcı: anlık fiyat, geçmiş veri ve sembol taraması

Aşağıdaki sınıf tek bir oturumu korur, yanıt bir doğrulama sayfası gibi görünüyorsa sitekey değerini çıkarır, görevi CaptchaAI'ye gönderir ve dönen token'ı kullanarak aynı URL'ye devam eder. Turnstile ile reCAPTCHA ayrımını data-sitekey deseninden yapar: 0x ile başlayan değer Turnstile yoluna, diğeri userrecaptcha yoluna gider.

import requests
import time
import re
from datetime import datetime, timedelta

class StockDataCollector:
    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_quote(self, portal_url, symbol):
        """Get current stock quote, solving CAPTCHAs if needed."""
        url = f"{portal_url}/quote/{symbol}"
        response = self.session.get(url)

        if self._is_captcha_page(response):
            response = self._solve_and_retry(response, url)

        return self._parse_quote(response.text, symbol)

    def get_historical(self, portal_url, symbol, days=365):
        """Download historical price data."""
        url = f"{portal_url}/history/{symbol}"
        params = {
            "period": f"{days}d",
            "interval": "1d"
        }
        response = self.session.get(url, params=params)

        if self._is_captcha_page(response):
            response = self._solve_and_retry(response, url)

        return self._parse_historical(response.text)

    def scan_symbols(self, portal_url, symbols, delay=2):
        """Collect quotes for multiple symbols."""
        results = {}

        for symbol in symbols:
            try:
                results[symbol] = self.get_quote(portal_url, symbol)
                time.sleep(delay)
            except Exception as e:
                results[symbol] = {"error": str(e)}

        return results

    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_and_retry(self, response, url):
        match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
        if not match:
            # Fall back to reCAPTCHA detection
            match = re.search(r'data-sitekey="([^"]+)"', response.text)
            if match:
                return self._solve_recaptcha_and_retry(match.group(1), url)
            raise ValueError("No CAPTCHA sitekey 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("CAPTCHA solve timed out")

    def _solve_recaptcha_and_retry(self, site_key, url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "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={
                    "g-recaptcha-response": data["request"]
                })

        raise TimeoutError("reCAPTCHA solve timed out")

    def _parse_quote(self, html, symbol):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        return {
            "symbol": symbol,
            "price": text_or_none(soup.select_one("[data-field='regularMarketPrice'], .price")),
            "change": text_or_none(soup.select_one("[data-field='regularMarketChange'], .change")),
            "volume": text_or_none(soup.select_one("[data-field='regularMarketVolume'], .volume")),
            "market_cap": text_or_none(soup.select_one("[data-field='marketCap'], .market-cap")),
            "timestamp": datetime.now().isoformat()
        }

    def _parse_historical(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        rows = []

        for row in soup.select("table tr")[1:]:  # Skip header
            cells = [td.text.strip() for td in row.select("td")]
            if len(cells) >= 6:
                rows.append({
                    "date": cells[0],
                    "open": cells[1],
                    "high": cells[2],
                    "low": cells[3],
                    "close": cells[4],
                    "volume": cells[5]
                })

        return rows


# Usage
collector = StockDataCollector("YOUR_API_KEY")

# Single quote
quote = collector.get_quote("https://finance.example.com", "AAPL")
print(f"AAPL: ${quote['price']} ({quote['change']})")

# Scan multiple symbols
portfolio = collector.scan_symbols(
    "https://finance.example.com",
    ["AAPL", "GOOGL", "MSFT", "AMZN", "TSLA"]
)

Üç noktaya dikkat edin. Birincisi, sorgulama döngüsü 3 saniyelik aralıklarla en fazla 60 tur döner; bu, Turnstile için gerekenden çok daha geniş bir tavandır, çünkü Turnstile tipik olarak 10 saniyenin altında çözülür. İkincisi, self.session korunmadığında her istek yeni bir oturum gibi görünür ve doğrulama her seferinde yeniden tetiklenir. Üçüncüsü, scan_symbols içindeki delay değeri sembol başına bekleme süresidir; portal 429 döndürmeye başlarsa önce bu değeri büyütün, eş zamanlı istek sayısını değil.

JavaScript ile eleme (screener) sorguları

Eleme sorguları, filtre kombinasyonu her seferinde değiştiği için en sık doğrulama tetikleyen istek türüdür. Node.js tarafında aynı akışı fetch ile kurabilirsiniz: yanıtı okuyun, cf-turnstile izini arayın, token'ı alın ve filtreleri POST olarak yeniden gönderin.

class MarketScreener {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  async screenStocks(portalUrl, filters) {
    const params = new URLSearchParams(filters);
    const response = await fetch(`${portalUrl}/screener?${params}`);
    const html = await response.text();

    if (html.includes('cf-turnstile') || response.status === 403) {
      return this.solveAndScreen(portalUrl, filters, html);
    }

    return this.parseScreenerResults(html);
  }

  async solveAndScreen(portalUrl, filters, 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: portalUrl,
        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(`${portalUrl}/screener`, {
          method: 'POST',
          body: new URLSearchParams({
            ...filters,
            'cf-turnstile-response': data.request
          })
        });
        return this.parseScreenerResults(await response.text());
      }
    }
    throw new Error('Turnstile solve timed out');
  }

  parseScreenerResults(html) {
    const rows = [];
    const tableMatch = html.match(/<table[^>]*>[\s\S]*?<\/table>/i);
    if (!tableMatch) return rows;

    const rowMatches = tableMatch[0].matchAll(/<tr[^>]*>([\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 >= 4) {
        rows.push({
          symbol: cells[0],
          price: cells[1],
          change: cells[2],
          volume: cells[3]
        });
      }
    }
    return rows;
  }
}

// Usage
const screener = new MarketScreener('YOUR_API_KEY');
const results = await screener.screenStocks('https://finance.example.com', {
  sector: 'technology',
  marketCap: 'large',
  peRatio: '<25'
});

Sonuç ayrıştırmayı doğrudan doğrulama katmanına bağlamayın. Portallar tablo yapısını sık değiştirir; ayrıştırıcıyı ayrı bir fonksiyonda tutmak, çözüm akışını bozmadan seçicileri güncellemenizi sağlar. Turnstile token'ı tek kullanımlıktır ve kısa ömürlüdür, bu yüzden token'ı alır almaz gönderin, arada başka iş yapmayın.

Türkiye'deki ekipler için pratik notlar

Fiyatlandırma tarafında CaptchaAI thread tabanlıdır: fatura çözüm başına değil, eş zamanlı thread sayısına göre hesaplanır ve her planda thread başına sınırsız çözüm vardır. TL'nin oynaklığı nedeniyle aylık giderin USD cinsinden sabit kalması, müşteri projesi üreten serbest çalışan geliştiriciler için somut bir avantajdır; fiyatlar yalnızca USD olarak geçerlidir.

Boyutlandırma pratikte şudur: tek portalı gün sonunda tarayan bir iş için BASIC ($15/ay, 5 thread) genellikle yeterlidir, çünkü beş doğrulama aynı anda uçar ve biri bittiğinde thread hemen bir sonrakini alır. Altı-yedi portalı gün içi anlık fiyat toplamayla birlikte sürüyorsanız ADVANCE ($90/ay, 50 thread) daha rahat bir tavan verir. Önce bir günlük gerçek istek sayınızı ölçün, sonra plan seçin.

Uyum tarafında tek bir hatırlatma yeterli: topladığınız veri fiyat ve bilanço gibi kurumsal veriyle sınırlıyken KVKK kapsamı dışındadır, ancak forum yorumu, analist profili veya kullanıcı adı gibi kişisel veri içeren alanlara uzandığında KVKK yükümlülükleri devreye girer. Toplama kapsamını baştan yazılı tutun ve yetkili olduğunuz kaynaklarla sınırlayın.

Sık karşılaşılan sorunlar

Sorun Sebep Düzeltme
Her istekte Turnstile Her seferinde yeni oturum Çerezleri istekler arasında kalıcı tutun
Geçmiş veriler eksik Sayfalama doğrulamanın arkasında Sayfa başına çözün, sayfalanmış bağlantıları takip edin
Anlık fiyat verisi eski Önbelleğe alınmış yanıt sunuldu Önbellek bozan sorgu parametresi ekleyin
429 oran sınırı Çok fazla istek İstekler arasındaki gecikmeyi artırın, toplu işi gece penceresine taşıyın
Token reddedildi Token gönderilmeden önce süresi doldu Token'ı alır almaz gönderin; sorgulama ile gönderim arasına iş koymayın

SSS

Turnstile çözümü ne kadar sürer ve bunu zamanlamaya nasıl yansıtırım?

Cloudflare Turnstile tipik olarak 10 saniyenin altında çözülür. Zamanlayıcıda sembol başına 10–15 saniyelik bir pay bırakmak, gün sonu toplu işinin penceresini aşmasını önler.

Borsa verisi toplamak için hangi CaptchaAI planı yeterli?

Eş zamanlı kaç doğrulama beklediğinize bakın. Tek portal ve günlük toplama için BASIC ($15/ay, 5 thread), çok portallı gün içi toplama için ADVANCE ($90/ay, 50 thread) makul bir başlangıçtır; thread dolduğunda bir sonraki görev sıraya girer.

CaptchaAI hCaptcha'yı da çözüyor mu?

Hayır. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor; GeeTest v4 ise "çok yakında" etiketiyle bekliyor. Bir portal bu türlerden birini kullanıyorsa toplama planınızı resmî veri kaynağı veya lisanslı sağlayıcı üzerinden kurgulayın.

Topladığım finansal veriler KVKK kapsamına girer mi?

Fiyat, hacim ve bilanço verisi kişisel veri değildir. Ancak forum yorumları, analist adları veya kullanıcı profilleri gibi alanları topladığınızda KVKK yükümlülükleri başlar; bu alanları toplama kapsamından çıkarmak en temiz yoldur.

Aynı anda birden fazla portaldan veri toplayabilir miyim?

Evet. Portal başına ayrı oturum ve bağımsız görev gönderimi kullanın; CaptchaAI farklı siteler için eş zamanlı çözümleri thread limitiniz dahilinde yürütür.

İlgili makaleler

Sonraki adımlar

Toplama penceresini doğrulamalara kaptırmayın — ücretsiz API anahtarınızı alın ve ilk portal taramanızı bugün çalıştırın.

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