Anwendungsfälle

Flugpreisüberwachung trotz CAPTCHA automatisieren

Ein Flugpreis-Monitor scheitert selten am Auslesen der Preise – er scheitert an der Abfrage davor. Sobald Ihr Scheduler eine Tarifseite abruft, antwortet das Buchungsportal häufig nicht mit Preisen, sondern mit einer reCAPTCHA-v2-Abfrage, einem Cloudflare-Interstitial oder einer Turnstile-Prüfung. Genau an dieser Stelle bricht die Pipeline ab, obwohl das Parsen der Tarifdaten trivial wäre. CaptchaAI übernimmt den Lösungsschritt: Sie senden Sitekey und Page-URL an die API, erhalten ein gültiges Token zurück und setzen die ursprüngliche Anfrage damit fort.

Reise- und Airline-Portale schützen ihre Preissuche bewusst gegen automatische Abrufe, weil dynamische Tarife eine wertvolle Ressource sind. Wer viele Strecken über mehrere Fluggesellschaften beobachtet, trifft dabei auf drei wiederkehrende Hürden: reCAPTCHA-Abfragen, Cloudflare-Interstitials und Rate-Limiting. Dieser Leitfaden zeigt einen vollständigen Python-Workflow, der Preisseiten nach Zeitplan abruft, ein CAPTCHA zuverlässig erkennt, es über CaptchaAI löst und anschließend die Tarifdaten ausliest. Alle Preise sind in US-Dollar angegeben.

So läuft der Überwachungs-Workflow ab

Der Ablauf ist immer derselbe: geplanter Abruf, Prüfung auf ein CAPTCHA, bei Bedarf Lösung über CaptchaAI, dann Auswertung und Alarm bei Preisänderungen.

Schedule check → Request fare page → CAPTCHA detected?
                                         ↓ Yes
                                    Solve via CaptchaAI → Inject token → Retry request
                                         ↓ No
                                    Parse fare data → Store → Alert on price change

Das Token ist der Kern: reCAPTCHA v2 liefert ein g-recaptcha-response, Cloudflare Turnstile ein cf-turnstile-response. Beide fügen Sie in die Folgeanfrage ein, die dann die eigentliche Preisseite zurückgibt.

Voraussetzungen

Anforderung Einzelheiten
CaptchaAI API-Schlüssel captchaai.com
Python 3.8+ Mit requests
Proxy Residential-Proxy für Reiseseiten
pip install requests

Reiseportale bewerten Rechenzentrums-IPs deutlich strenger als Privatanschlüsse. Ein Residential-Proxy senkt die Zahl der ausgelösten CAPTCHAs spürbar und ist bei ernsthaftem Monitoring keine Option, sondern Voraussetzung.

Solver-Helfer für reCAPTCHA v2 und Turnstile

Die beiden folgenden Funktionen kapseln das übliche Muster: Aufgabe an in.php übermitteln, dann res.php abfragen, bis das Token bereitliegt. reCAPTCHA v2 erwartet den Sitekey unter googlekey und die Methode userrecaptcha; Turnstile nutzt sitekey und die Methode turnstile. Für vollständige Cloudflare-Challenge-Seiten – nicht nur die schlanke Turnstile-Variante – setzen Sie method=cloudflare_challenge; Details dazu in der Anleitung zur Cloudflare Challenge.

import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_recaptcha_v2(sitekey, pageurl):
    """Solve reCAPTCHA v2 and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

    if submit.get("status") != 1:
        raise RuntimeError(f"Submit error: {submit.get('request')}")

    task_id = submit["request"]
    time.sleep(20)

    for _ in range(30):
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1
        }).json()
        if result.get("status") == 1:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")


def solve_turnstile(sitekey, pageurl):
    """Solve Cloudflare Turnstile and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "turnstile",
        "sitekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

    if submit.get("status") != 1:
        raise RuntimeError(f"Submit error: {submit.get('request')}")

    task_id = submit["request"]
    time.sleep(10)

    for _ in range(30):
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1
        }).json()
        if result.get("status") == 1:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")

Ein Token ist nur rund 120 Sekunden gültig. Lösen Sie es deshalb erst unmittelbar vor der Folgeanfrage und nicht auf Vorrat – ein zu früh gelöstes Token läuft ab, bevor Sie es absenden.

Der Fare-Monitor: Preise abrufen und CAPTCHAs lösen

Die Klasse FareMonitor hält eine Session mit optionalem Proxy, erkennt anhand des HTML, welcher CAPTCHA-Typ vorliegt, extrahiert den passenden Sitekey und wiederholt die Anfrage mit dem gelösten Token. Das Auslesen der Preise arbeitet mit einer Kette von Selektoren, sodass ein geändertes Markup nicht sofort die ganze Pipeline stoppt.

import json
from datetime import datetime


class FareMonitor:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {
                "http": f"http://{proxy}",
                "https": f"http://{proxy}"
            }
        self.fare_history = {}

    def check_fare(self, route):
        """Check fare for a route, solving CAPTCHAs if needed."""
        url = route["url"]

        response = self.session.get(url)

        # Detect CAPTCHA in response
        if self._has_recaptcha(response.text):
            sitekey = self._extract_sitekey(response.text)
            token = solve_recaptcha_v2(sitekey, url)
            response = self.session.post(url, data={
                "g-recaptcha-response": token,
                **route.get("params", {})
            })

        elif self._has_turnstile(response.text):
            sitekey = self._extract_turnstile_key(response.text)
            token = solve_turnstile(sitekey, url)
            response = self.session.post(url, data={
                "cf-turnstile-response": token,
                **route.get("params", {})
            })

        return self._parse_fare(response.text, route)

    def _has_recaptcha(self, html):
        return "g-recaptcha" in html or "recaptcha/api" in html

    def _has_turnstile(self, html):
        return "cf-turnstile" in html or "turnstile" in html

    def _extract_sitekey(self, html):
        # Extract data-sitekey from reCAPTCHA div
        if 'data-sitekey="' in html:
            start = html.index('data-sitekey="') + 14
            end = html.index('"', start)
            return html[start:end]
        return None

    def _extract_turnstile_key(self, html):
        if 'data-sitekey="' in html:
            idx = html.index("cf-turnstile")
            start = html.index('data-sitekey="', idx) + 14
            end = html.index('"', start)
            return html[start:end]
        return None

    def _parse_fare(self, html, route):
        """Parse fare data from the response with resilient selector fallbacks."""
        from bs4 import BeautifulSoup

        soup = BeautifulSoup(html, "html.parser")
        price_text = ""
        for selector in [
            ".fare-price",
            "[data-testid='fare-price']",
            ".price-amount",
            ".total-price",
        ]:
            element = soup.select_one(selector)
            if element:
                price_text = element.get_text(strip=True)
                break

        currency_match = re.search(r"(USD|EUR|GBP|\$|€|£)", price_text)
        return {
            "route": route["name"],
            "timestamp": datetime.now().isoformat(),
            "price_text": price_text,
            "currency": currency_match.group(1) if currency_match else "",
            "available": bool(price_text),
            "raw_length": len(html)
        }

    def monitor_routes(self, routes):
        """Check all routes and report price changes."""
        results = []
        for route in routes:
            try:
                fare = self.check_fare(route)
                results.append(fare)
                print(f"[OK] {route['name']}: checked")
            except Exception as e:
                print(f"[ERROR] {route['name']}: {e}")
        return results


# Usage
routes = [
    {
        "name": "NYC-LAX",
        "url": "https://example-airline.com/search?from=JFK&to=LAX&date=2025-08-15",
        "params": {"adults": 1}
    },
    {
        "name": "SFO-ORD",
        "url": "https://example-airline.com/search?from=SFO&to=ORD&date=2025-08-20",
        "params": {"adults": 1}
    }
]

monitor = FareMonitor(proxy="user:pass@proxy.example.com:8080")
results = monitor.monitor_routes(routes)

for r in results:
    print(json.dumps(r, indent=2))

Wichtig ist, dass die Sitekey-Extraktion zum tatsächlichen CAPTCHA auf der Seite passt. _extract_turnstile_key sucht das data-sitekey gezielt im Umfeld des cf-turnstile-Elements, damit auf Seiten mit mehreren Widgets nicht der falsche Schlüssel gegriffen wird.

Den Monitor nach Zeitplan ausführen

Für den Dauerbetrieb rufen Sie das Skript per cron oder Taskplaner in festen Intervallen auf. Auf einem kleinen VPS – etwa bei Hetzner, IONOS oder netcup – genügt ein einzelner Cronjob:

# Check fares every 6 hours
0 */6 * * * cd /path/to/project && python fare_monitor.py >> /var/log/fares.log 2>&1

Welche Prüf-Kadenz ist sinnvoll?

Die Frequenz ist der wichtigste Kostenhebel: Jeder Abruf, der auf ein CAPTCHA trifft, verbraucht eine Lösung und belastet Ihre Proxys. Orientieren Sie die Taktung deshalb am geschäftlichen Wert der Strecke, nicht am technisch Machbaren.

Streckentyp Sinnvolle Kadenz Warum
Hochpreisige oder stark schwankende Verbindungen Alle paar Stunden Preisänderungen sind geschäftlich relevanter
Normale Freizeitstrecken 1–2 Mal täglich Weniger Druck auf Zielseiten und Proxys
Große Routenlisten über viele Airlines Gestaffelte Batch-Läufe Gleichmäßigeres Lastprofil und weniger CAPTCHAs
Sehr aggressive Preisalarme Nur mit Vorsicht Häufige Abrufe erhöhen Kosten und Blockrisiken stark

Häufige Probleme und ihre Lösung

Problem Ursache Lösung
Häufige CAPTCHAs Zu viele Anfragen von derselben IP Rotierende Residential-Proxys verwenden
Veraltete Preise Zwischengespeicherte Seiten Cache-Busting-Header setzen oder Anfrageparameter zufällig anordnen
IP blockiert Rate-Limiting Verzögerungen zwischen den Prüfungen erhöhen und Proxys wechseln
CAPTCHA-Lösung schlägt fehl Falsch extrahierter Sitekey Prüfen, ob der Sitekey zum CAPTCHA auf der Seite passt

Kosten: Abrechnung pro Thread, nicht pro Lösung

CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro einzelner Lösung – jeder Plan enthält unbegrenzt viele Lösungen pro Thread. Ein Thread ist eine gerade in Bearbeitung befindliche CAPTCHA-Abfrage; sobald sie fertig ist, nimmt derselbe Thread die nächste auf. Für einen gestaffelten Monitor mit wenigen parallelen Läufen reicht meist BASIC (15 $/Monat, 5 Threads). Wer viele Strecken über zahlreiche Airlines gleichzeitig prüft, fährt mit ADVANCE (90 $/Monat, 50 Threads) oder PREMIUM (170 $/Monat, 100 Threads) planbarer als mit einer Abrechnung pro Einzellösung. Da die Kadenz die Parallelität bestimmt, ist die Wahl des Tarifs vor allem eine Frage, wie viele Prüfungen gleichzeitig laufen sollen.

Rechtlicher Hinweis für DACH-Betreiber

Automatisiertes Sammeln von Preisdaten bewegt sich in Deutschland, Österreich und der Schweiz in einem regulierten Rahmen. Sobald Sie Proxys einsetzen, verarbeiten Sie IP-Adressen, die nach DSGVO als personenbezogene Daten gelten – prüfen Sie Ihre Rechtsgrundlage und Datenflüsse. Beachten Sie außerdem die AGB und die robots.txt der Zielportale und beschränken Sie die Frequenz auf ein Maß, das die Infrastruktur der Gegenseite nicht belastet. Diese Sorgfalt ist im DACH-Raum kein Beiwerk, sondern gehört zur Grundausstattung eines seriösen Monitorings.

Häufige Fragen

Warum bekomme ich beim Flugpreis-Scraping so viele CAPTCHAs?

Meist liegt es an der IP-Reputation und der Abruffrequenz. Rechenzentrums-IPs lösen auf Reiseportalen fast sofort eine Abfrage aus; kombiniert mit engen Intervallen sammeln sich reCAPTCHA- und Turnstile-Prüfungen schnell an. Rotierende Residential-Proxys und größere Abstände zwischen den Läufen senken die Rate deutlich.

Läuft das Token ab, bevor ich es absende?

Das kann passieren. Sowohl g-recaptcha-response als auch cf-turnstile-response sind nur etwa 120 Sekunden gültig. Lösen Sie das CAPTCHA daher unmittelbar vor der Folgeanfrage und nicht vorab auf Vorrat, sonst weist das Portal ein bereits abgelaufenes Token zurück.

Welche CaptchaAI-Methode brauche ich für reCAPTCHA v2 und Turnstile?

Für reCAPTCHA v2 verwenden Sie method=userrecaptcha mit dem Sitekey im Feld googlekey, für Cloudflare Turnstile method=turnstile mit sitekey. Bei einer vollständigen Cloudflare-Challenge-Seite nutzen Sie stattdessen method=cloudflare_challenge und werten das zurückgegebene cf_clearance-Cookie aus.

Wie viele Threads brauche ich für eine große Routenliste?

Das hängt nicht von der Zahl der Strecken ab, sondern davon, wie viele Prüfungen gleichzeitig laufen. Prüfen Sie 50 Routen streng nacheinander, genügen wenige Threads; laufen viele Airlines parallel, skaliert die Parallelität mit dem Tarif – etwa 50 Threads bei ADVANCE (90 $/Monat). Da jeder Thread unbegrenzt viele Lösungen erlaubt, begrenzt nur die Gleichzeitigkeit den Durchsatz.

Ist das Monitoring von Flugpreisen rechtlich zulässig?

Öffentliche Preisdaten zu beobachten ist grundsätzlich möglich, doch der Rahmen zählt. Halten Sie AGB und robots.txt der Portale ein, drosseln Sie die Frequenz und prüfen Sie bei Proxy-Einsatz Ihre DSGVO-Grundlage, da IP-Adressen personenbezogen sind. Klären Sie im Zweifel die rechtliche Zulässigkeit für Ihren konkreten Anwendungsfall.


Betreiben Sie CaptchaAI in einem realistischen Monitoring-Rhythmus, der Preisrelevanz, Proxy-Kosten und CAPTCHA-Druck gegeneinander abwägt.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.