Integrationen

Scrapy + CaptchaAI-Integrationshandbuch

Der Crawl läuft durch, das Log meldet 200er-Antworten – und products.json bleibt leer. Statt der Produktliste liefert die Zielseite eine CAPTCHA-Abfrage aus. In Scrapy gehört die Antwort darauf in eine eigene Downloader-Middleware: Sie erkennt die Abfrage, übergibt Sitekey und URL an CaptchaAI und legt das gelöste Token in request.meta ab – die Spider-Logik bleibt unverändert.

Dieser Leitfaden baut die Integration vollständig auf – Solver-Modul, Middleware, settings.py, Spider, Wiederholungslogik – und klärt am Ende die Frage vor dem Produktivbetrieb: wie viele Threads ein Crawl wirklich braucht.

Wann sich eine CAPTCHA-Middleware im Crawl lohnt

Nicht jeder Spider braucht sie. Sinnvoll wird die Integration in drei Situationen:

  • Punktuelle Abfragen: Nur einzelne Seiten eines offenen Katalogs sind geschützt, etwa Detailseiten oder das Formular am Ende der Paginierung.
  • Wiederkehrende Jobs: Ein nächtlicher Crawl auf einem Hetzner- oder netcup-Server soll durchlaufen, auch wenn morgens um drei eine Abfrage dazwischenkommt.
  • Gemischte Typen: Auf derselben Domain begegnen Ihnen reCAPTCHA v2, ein Bild-CAPTCHA im Login und gelegentlich Cloudflare Turnstile.

Bleibt es bei einer einzigen geschützten URL pro Lauf, ist ein direkter API-Aufruf im Spider einfacher.

Voraussetzungen für die Scrapy-Integration

Voraussetzung Details
Python 3.8+
Scrapy 2.5+
requests für die Aufrufe an die CaptchaAI-API
CaptchaAI-API-Schlüssel im Dashboard erstellen
pip install scrapy requests

Solver-Modul: Aufgabe übermitteln, Ergebnis abfragen

Legen Sie captcha_solver.py im Projektstamm an. Das Modul kapselt beide API-Schritte: Die Aufgabe geht per in.php an ocr.captchaai.com, anschließend fragt eine Schleife über res.php den Status ab, bis ein Token vorliegt oder das Timeout greift.

import requests
import time


class CaptchaAISolver:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://ocr.captchaai.com"

    def solve_recaptcha(self, site_key, page_url, timeout=300):
        resp = requests.get(f"{self.base_url}/in.php", params={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": page_url,
        })

        if not resp.text.startswith("OK|"):
            raise Exception(f"Submit failed: {resp.text}")

        task_id = resp.text.split("|")[1]
        deadline = time.time() + timeout

        while time.time() < deadline:
            time.sleep(5)
            result = requests.get(f"{self.base_url}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
            })

            if result.text == "CAPCHA_NOT_READY":
                continue
            if result.text.startswith("OK|"):
                return result.text.split("|", 1)[1]
            raise Exception(f"Solve failed: {result.text}")

        raise TimeoutError(f"Task {task_id} timed out")

    def solve_image(self, image_base64, timeout=120):
        resp = requests.get(f"{self.base_url}/in.php", params={
            "key": self.api_key,
            "method": "base64",
            "body": image_base64,
        })

        if not resp.text.startswith("OK|"):
            raise Exception(f"Submit failed: {resp.text}")

        task_id = resp.text.split("|")[1]
        deadline = time.time() + timeout

        while time.time() < deadline:
            time.sleep(5)
            result = requests.get(f"{self.base_url}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
            })

            if result.text == "CAPCHA_NOT_READY":
                continue
            if result.text.startswith("OK|"):
                return result.text.split("|", 1)[1]
            raise Exception(f"Solve failed: {result.text}")

        raise TimeoutError(f"Task {task_id} timed out")

Zwei Details verdienen Aufmerksamkeit: das Polling-Intervall von fünf Sekunden – reCAPTCHA v2 wird typischerweise in unter 60 Sekunden gelöst, Bild-CAPTCHAs in unter 0,5 Sekunden – und die getrennten Timeouts, damit eine hängende Aufgabe nicht den Crawl blockiert.

Downloader-Middleware für Scrapy schreiben

Die Middleware landet in middlewares.py und greift in process_response ein: Sie durchsucht die HTML-Antwort nach einem data-sitekey-Attribut beziehungsweise nach einem eingebetteten Bild-CAPTCHA und schreibt das Ergebnis nach request.meta.

import base64
import re
from scrapy import signals
from scrapy.http import HtmlResponse
from captcha_solver import CaptchaAISolver


class CaptchaAIMiddleware:
    """Scrapy downloader middleware that detects and solves CAPTCHAs."""

    def __init__(self, api_key):
        self.solver = CaptchaAISolver(api_key)

    @classmethod
    def from_crawler(cls, crawler):
        api_key = crawler.settings.get("CAPTCHAAI_API_KEY")
        if not api_key:
            raise ValueError("CAPTCHAAI_API_KEY setting is required")
        return cls(api_key)

    def process_response(self, request, response, spider):
        # Check for reCAPTCHA on the page
        site_key = self._find_recaptcha_key(response.text)
        if site_key:
            spider.logger.info(f"reCAPTCHA detected on {response.url}")
            token = self.solver.solve_recaptcha(site_key, response.url)
            request.meta["captcha_token"] = token
            spider.logger.info("CAPTCHA solved successfully")

        # Check for image CAPTCHA
        captcha_img = self._find_image_captcha(response)
        if captcha_img:
            spider.logger.info(f"Image CAPTCHA detected on {response.url}")
            text = self.solver.solve_image(captcha_img)
            request.meta["captcha_text"] = text
            spider.logger.info(f"Image CAPTCHA solved: {text}")

        return response

    def _find_recaptcha_key(self, html):
        match = re.search(
            r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', html
        )
        return match.group(1) if match else None

    def _find_image_captcha(self, response):
        img = response.css("img#captcha-image::attr(src)").get()
        if img and img.startswith("data:image"):
            return img.split(",", 1)[1]
        return None

Der Erkennungsteil ist die Stelle, die Sie pro Zielseite anpassen. Das Regex-Muster für data-sitekey deckt die Standard-Einbindung von reCAPTCHA ab; setzt eine Seite das Widget erst per JavaScript, brauchen Sie einen Renderer wie scrapy-playwright oder Splash vor der Middleware.

Middleware in settings.py aktivieren

Registriert wird die Middleware mit Priorität 560 – nach der RetryMiddleware (550) und vor HttpCompressionMiddleware (590); auf dem Rückweg erreicht die Antwort sie damit bereits dekomprimiert. Den API-Schlüssel liest sie aus der Umgebung; er gehört weder ins Repository noch in ein CI-Log.

import os

CAPTCHAAI_API_KEY = os.environ.get("CAPTCHAAI_API_KEY")

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.CaptchaAIMiddleware": 560,
}

In GitLab CI hinterlegen Sie den Wert als maskierte Variable, bei GitHub Actions als Secret. Derselbe Spider läuft damit unverändert lokal, im Container und in der Pipeline.

Spider-Beispiel mit Token-Übergabe

Der Spider bleibt schlank. Findet er ein Token in response.meta, sendet er die Seite erneut ab – diesmal mit dem Feld g-recaptcha-response im Formular. Andernfalls parst er direkt weiter.

import scrapy


class ProductSpider(scrapy.Spider):
    name = "products"
    start_urls = ["https://example.com/products"]

    def parse(self, response):
        # If CAPTCHA was solved, the token is in meta
        token = response.meta.get("captcha_token")
        if token:
            # Resubmit the page with the token
            yield scrapy.FormRequest(
                url=response.url,
                formdata={"g-recaptcha-response": token},
                callback=self.parse_products,
            )
        else:
            yield from self.parse_products(response)

    def parse_products(self, response):
        for product in response.css(".product-item"):
            yield {
                "name": product.css("h2::text").get(),
                "price": product.css(".price::text").get(),
                "url": response.urljoin(
                    product.css("a::attr(href)").get()
                ),
            }

        next_page = response.css("a.next-page::attr(href)").get()
        if next_page:
            yield scrapy.Request(response.urljoin(next_page))

Ein Hinweis zur Token-Lebensdauer: Tokens sind kurzlebig, rund 120 Sekunden. Lösen Sie deshalb unmittelbar vor dem Absenden und sammeln Sie keine Vorräte an – ein Token, das zehn Minuten in einer Warteschlange liegt, ist beim Absenden wertlos.

Wiederholungen bei CAPTCHA-Seiten steuern

Manche Seiten liefern die Abfrage nicht eingebettet, sondern als eigene Interstitial-Seite. Dafür ergänzen Sie eine zweite Middleware, die solche Antworten erkennt und den Request begrenzt oft wiederholt.

class CaptchaRetryMiddleware:
    """Retry requests that return CAPTCHA challenge pages."""

    max_retries = 3

    def process_response(self, request, response, spider):
        if self._is_captcha_page(response):
            retries = request.meta.get("captcha_retries", 0)
            if retries < self.max_retries:
                request.meta["captcha_retries"] = retries + 1
                spider.logger.info(
                    f"CAPTCHA page detected, retry {retries + 1}"
                )
                return request.copy()

        return response

    def _is_captcha_page(self, response):
        indicators = [
            "g-recaptcha",
            "cf-turnstile",
            "captcha-image",
            "Please verify you are human",
        ]
        return any(ind in response.text for ind in indicators)

Drei Versuche sind ein brauchbarer Startwert. Kombinieren Sie sie mit DOWNLOAD_DELAY und AUTOTHROTTLE_ENABLED: Wer nach einer Abfrage sofort mit voller Frequenz weitercrawlt, provoziert die nächste.

Crawl starten und Log prüfen

export CAPTCHAAI_API_KEY="YOUR_API_KEY"
scrapy crawl products -o products.json

Achten Sie im Log auf die Zeilen der Middleware: Jede erkannte Abfrage erzeugt einen Eintrag mit URL, jede Lösung eine Bestätigung. Bleibt die Ausgabedatei leer, obwohl Lösungen protokolliert werden, liegt der Fehler im Selektor – nicht in der Integration.

Threads, Durchsatz und Kosten planen

CaptchaAI rechnet pro gleichzeitigem Thread ab – nicht pro Lösung. Ein Thread ist genau ein CAPTCHA, das gerade bearbeitet wird; ist es fertig, übernimmt derselbe Thread das nächste. Die Lösungen pro Thread sind unbegrenzt, Aufschläge nach CAPTCHA-Typ gibt es nicht. Entscheidend für die Planung ist also nicht die Gesamtzahl der Abfragen pro Nacht, sondern wie viele davon gleichzeitig offen sind.

Szenario Gleichzeitige Abfragen Passender Plan
Einzelner Spider, CONCURRENT_REQUESTS = 8 bis 5 BASIC (15 $/Monat, 5 Threads)
Mehrere Spider auf einem Server 10–15 STANDARD (30 $/Monat, 15 Threads)
Verteilter Crawl über mehrere Worker 40–50 ADVANCE (90 $/Monat, 50 Threads)

Alle Preise in US-Dollar. Ein Praxisbeispiel aus dem DACH-Alltag: Ein Preismonitor für einen Shopware-Shop läuft nachts per GitLab CI auf einem Hetzner-Server, crawlt rund 40.000 Seiten und trifft dabei auf etwa 300 Abfragen. Weil davon nie mehr als eine Handvoll gleichzeitig offen ist, genügt der kleinste Plan.

Welche CAPTCHA-Typen die Middleware abdecken kann

Die Middleware kann nur aufrufen, was der Dienst auch löst. Abgedeckt sind reCAPTCHA v2 (inklusive Invisible und Enterprise), reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3 sowie Bild-, Rasterbild- und BLS-CAPTCHAs. CaptchaFox (Beta), Friendly Captcha (Beta) und Lemin (Beta) kommen aus dem Beta-Programm hinzu.

Nicht im Umfang sind hCaptcha und FunCaptcha; GeeTest v4 ist als „bald verfügbar“ angekündigt. Trifft Ihr Crawl auf einen dieser Typen, sollte die Middleware die URL protokollieren und überspringen, statt Threads dafür zu binden.

Datenschutz und Sorgfaltspflichten im DACH-Raum

Zwei Punkte tauchen in deutschsprachigen Projekten regelmäßig auf. Erstens gelten IP-Adressen nach DSGVO als personenbezogene Daten: Nutzt Ihr Crawl Proxys, Logs oder Zwischenspeicher, gehören Datenfluss und Rechtsgrundlage dokumentiert. Zweitens lohnt der Blick in Nutzungsbedingungen und robots.txt, bevor ein wiederkehrender Job produktiv geht – das automatische Lösen von Abfragen ersetzt keine Erlaubnis.

Fehlerbehebung

Symptom Ursache Abhilfe
ValueError: CAPTCHAAI_API_KEY setting is required Umgebungsvariable fehlt CAPTCHAAI_API_KEY vor dem Crawl setzen
Abfrage wird nicht erkannt abweichende HTML-Struktur Regex und CSS-Selektor in der Middleware anpassen
TimeoutError beim Lösen Netzwerkprobleme oder ausgelastetes Thread-Kontingent Timeout erhöhen, Plan-Threads prüfen
Spider wird trotz Lösung blockiert IP-basierte Sperre Proxy-Rotation ergänzen, DOWNLOAD_DELAY erhöhen
Token wird von der Zielseite abgelehnt Token bereits abgelaufen Lösung näher an den Absende-Zeitpunkt rücken

Häufige Fragen

Wie viele Threads braucht mein Crawl?

So viele, wie im Spitzenmoment gleichzeitig offen sind – meist deutlich weniger als CONCURRENT_REQUESTS. Protokollieren Sie eine Woche lang die parallelen Solver-Aufrufe; für einen einzelnen Spider reicht in der Regel BASIC (15 $/Monat, 5 Threads).

Wie lange ist ein gelöstes Token gültig?

Rund 120 Sekunden. Deshalb löst die Middleware erst beim Antreffen der Abfrage, und der Spider sendet das Formular unmittelbar danach ab. Bei langen Warteschlangen dazwischen läuft das Token ab.

Warum findet die Middleware den Sitekey nicht?

Weil er nicht im ausgelieferten HTML steht. Viele Seiten setzen das Widget erst per JavaScript – dann brauchen Sie scrapy-playwright oder Splash vor der Middleware, damit response.text das gerenderte Markup enthält.

Löst CaptchaAI auch hCaptcha in Scrapy-Projekten?

Nein. hCaptcha und FunCaptcha gehören nicht zum unterstützten Umfang, GeeTest v4 ist lediglich angekündigt. Für reCAPTCHA v2/v3, Turnstile, GeeTest v3 sowie Bild- und Rasterbild-CAPTCHAs funktioniert der Aufbau unverändert.

Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.