Anwendungsfälle

CAPTCHA-Verarbeitung für die Börsendatenerfassung

Wenn Ihre Datenpipeline mitten im Handelstag plötzlich leere Kurse oder HTTP 403 zurückliefert, steckt fast immer eine Cloudflare-Turnstile- oder reCAPTCHA-Abfrage dahinter. Finanzportale schalten diese Abfragen genau dann, wenn es weh tut: bei schnellen Symbolsuchen, beim Herunterladen historischer Kursreihen und bei umfangreichen Screener-Filtern. Die Lösung ist kein Umgehen des Schutzes, sondern ein sauberer Ablauf – Abfrage erkennen, Token über CaptchaAI lösen, Token einfügen, Anfrage in derselben Sitzung wiederholen. Dieser Leitfaden zeigt den kompletten Ablauf in Python und JavaScript.

Wo Finanzportale CAPTCHAs auslösen

CAPTCHAs erscheinen auf Finanzseiten nicht zufällig, sondern an vorhersehbaren Stellen. Wer weiß, welcher Datentyp welche Abfrage auslöst, kann seine Sammler von vornherein robust bauen.

Datentyp Portalbeispiele CAPTCHA-Typ Auslöser
Echtzeit-Kurse Finanzportale Cloudflare Turnstile Schnelle Symbolsuche
Historische Preise Datenanbieter reCAPTCHA v2 Massen-CSV-Downloads
Finanzberichte SEC-Einreichungsseiten Bild-CAPTCHA Wiederholte EDGAR-Abfragen
Screener-Ergebnisse Aktienprüfer Cloudflare Challenge Komplexe Filterabfragen
Analystenbewertungen Forschungsportale reCAPTCHA v3 Mehrere Seitenaufrufe

CaptchaAI deckt genau diese Typen ab: reCAPTCHA v2 und v3, Cloudflare Turnstile und Challenge sowie Bild- und OCR-CAPTCHAs. Der Ablauf ist bei allen gleich – Sie übermitteln sitekey und pageurl an den Endpunkt in.php, fragen das Ergebnis über res.php ab und tragen das zurückgelieferte Token in die Folgeanfrage ein.

Aktiendaten-Collector in Python

Der folgende Collector kapselt den kompletten Zyklus: Er ruft eine Kursseite ab, erkennt eine Turnstile- oder reCAPTCHA-Abfrage, lässt sie über CaptchaAI lösen und wiederholt die Anfrage mit dem Token – alles innerhalb einer persistenten requests.Session, damit gesetzte Cookies erhalten bleiben.

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

Entscheidend ist _is_captcha_page: Der Collector prüft sowohl den Statuscode 403 als auch typische Turnstile-Marker im HTML, bevor er eine Lösung anstößt. So verschwenden Sie kein Guthaben, wenn eine Seite ganz normal ausliefert.

Markt-Screener mit CAPTCHA-Handling (JavaScript)

Screener-Abfragen mit vielen Filterkriterien lösen besonders häufig eine Cloudflare Challenge aus, weil sie serverseitig teuer sind. Die JavaScript-Variante folgt demselben Muster – Abfrage erkennen, Token lösen, POST wiederholen – und eignet sich für Node.js-Worker, die neben einem bestehenden Frontend laufen.

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

Abrufintervalle nach Datentyp

Die meisten CAPTCHA-Probleme entstehen nicht durch den Schutz selbst, sondern durch zu aggressive Abrufraten. Ein Portfolio-Tracker für DAX- und MDAX-Werte braucht Tagesendkurse nur einmal nach Xetra-Schluss – nicht im Minutentakt. Richten Sie Ihre Intervalle nach dem tatsächlichen Bedarf aus, dann fällt die CAPTCHA-Last von selbst.

Datentyp Empfohlenes Intervall CAPTCHA-Häufigkeit
Echtzeit-Kurse 1–5 Minuten Hoch – API verwenden, falls verfügbar
Tagesendpreise Einmal täglich nach Geschäftsschluss Niedrig
Finanzberichte Vierteljährlich Minimal
Screener-Ergebnisse Täglich Mäßig
Analystenbewertungen Wöchentlich Niedrig

Häufige Probleme und Lösungen

Problem Ursache Lösung
Zu viele CAPTCHAs 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 CAPTCHA Der Parser liest eine alte oder unvollständige Ansicht aus Extrahieren Sie Daten 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
HTTP 429 (Rate-Limiting) Zu viele Anfragen pro Zeitfenster Erhöhen Sie das Timeout zwischen Anfragen und rotieren Sie Proxys

FAQ

Welchen CAPTCHA-Typ setzen Finanzportale am häufigsten ein?

Cloudflare Turnstile ist auf Kurs- und Screener-Seiten am häufigsten, gefolgt von reCAPTCHA v2 bei historischen Downloads und reCAPTCHA v3 auf Analystenseiten. CaptchaAI löst alle drei über denselben Ablauf – nur die method und das Token-Feld (cf-turnstile-response bzw. g-recaptcha-response) unterscheiden sich.

Wie halte ich die CAPTCHA-Kosten bei großen Symbollisten niedrig?

Lösen Sie nur dort, wo tatsächlich eine Abfrage erscheint, und halten Sie die Sitzung nach dem ersten Token offen. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung: BASIC (15 $/Monat, 5 Threads) reicht für kleinere Watchlists, ADVANCE (90 $/Monat, 50 Threads) für parallele Portale. Innerhalb eines Plans sind die Lösungen unbegrenzt.

Löst CaptchaAI auch hCaptcha auf Finanzseiten?

Nein. hCaptcha und FunCaptcha werden von CaptchaAI derzeit nicht unterstützt. Abgedeckt sind reCAPTCHA v2/v3, Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild- und Grid-CAPTCHAs; CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta. Prüfen Sie vorab, welchen Schutz Ihre Zielportale einsetzen.

Ist das Erfassen von Börsendaten in Deutschland zulässig?

Das hängt von den Nutzungsbedingungen der Quelle und der Art der Daten ab. Sobald personenbezogene Daten oder IP-Adressen im Spiel sind, greift die DSGVO – prüfen Sie Ihre Rechtsgrundlage und die AGB des Portals, bevor Sie automatisiert erfassen. Für rein öffentliche Kurs- und Kennzahlendaten ist die Lage meist unkritisch, ersetzt aber keine eigene rechtliche Prüfung.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.