Scarnox · Blog

Scrapy-Captcha-Middleware: Captchas in deinem Spider lösen (2026)

Scrapy ist um Anfragen und Callbacks herum gebaut, daher passt ein Captcha nicht ohne Weiteres hinein. Der saubere Weg ist eine Downloader-Middleware: Sie sieht jede Antwort, erkennt eine Herausforderung, holt ein Token, und der Spider sendet das Formular mit diesem Token ab. Dieser Leitfaden baut diese Middleware für PopularCaptcha-Seiten, die Scarnox heute proxyless löst.

Starte mit kostenlosem Testguthaben, ohne Karte und probiere den Code auf einer Testseite, bevor du ihn auf andere Ziele richtest.

Wie die Middleware funktioniert

Die Middleware läuft bei jeder Antwort:

  1. Hat die Antwort kein data-sitekey, reicht sie die Antwort unverändert weiter.
  2. Ist es vorhanden, löst die Middleware das Captcha und gibt eine Kopie derselben Anfrage zurück, mit dem Token in meta.
  3. Scrapy ruft diese Anfrage erneut ab. Die Middleware sieht das Token in meta und lässt die Antwort durch.
  4. Der Spider liest das Token aus meta und sendet das Formular mit FormRequest.from_response ab.

Der Lösungsaufruf läuft über deferToThread in einem Thread, sodass er den Reaktor von Scrapy nicht blockiert. Andere Anfragen laufen weiter, während das Captcha gelöst wird.

Projekteinstellungen

Lege deinen Schlüssel in eine Umgebungsvariable, nicht in den Code:

# settings.py
import os

SCARNOX_API_KEY = os.environ["SCARNOX_API_KEY"]
SCARNOX_TIMEOUT = 185

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.PopularCaptchaMiddleware": 543,
}

ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3
CONCURRENT_REQUESTS_PER_DOMAIN = 1

Die Download-Verzögerung und die einzelne Parallelität pro Domain sind bewusst gesetzt. Sie halten die Last der Seite niedrig und verringern die Anzahl der Captchas, denen du begegnest.

Die Middleware

# myproject/middlewares.py
import requests
from scrapy import Request
from scrapy.http import Response
from twisted.internet.threads import deferToThread

API_URL = "https://api.scarnox.com/api/tasks/create"


class PopularCaptchaMiddleware:
    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            api_key=crawler.settings.get("SCARNOX_API_KEY"),
            timeout=crawler.settings.getint("SCARNOX_TIMEOUT", 185),
        )

    def _solve(self, sitekey: str, page_url: str) -> str:
        r = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={
                "captcha_type": "PopularCaptchaTaskProxyless",
                "site_key": sitekey,
                "site_url": page_url,
            },
            timeout=self.timeout,  # läuft in einem Worker-Thread, nicht im Reaktor
        )
        data = r.json()
        if data.get("errorId") != 0:
            raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
        return data["solution"]["token"]

    def process_response(self, request: Request, response: Response, spider):
        # Bereits gelöst oder ein selbst gebauter Formular-Submit: durchreichen.
        if request.meta.get("captcha_token") or request.meta.get("skip_captcha"):
            return response

        sitekey = response.css("[data-sitekey]::attr(data-sitekey)").get()
        if not sitekey:
            return response

        spider.logger.info("PopularCaptcha on %s, solving", response.url)

        def retry_with_token(token: str) -> Request:
            # Eine zurückgegebene Request plant sie neu ein. dont_filter umgeht den Duplikatfilter.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

        return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)

Scrapy unterstützt es, aus process_response ein Deferred zurückzugeben. Der Callback macht aus dem Token eine erneut ausgeführte Anfrage, und Scrapy plant sie wie jede andere ein.

Der Spider

# myproject/spiders/search.py
import scrapy
from scrapy.http import FormRequest


class SearchSpider(scrapy.Spider):
    name = "search"
    start_urls = ["https://search.example.com/search?q=web+scraping"]

    def parse(self, response):
        token = response.meta.get("captcha_token")
        if token and response.css("[data-sitekey]"):
            # Scrapy füllt die eigenen Felder des Formulars aus; wir fügen das Token hinzu.
            yield FormRequest.from_response(
                response,
                formdata={
                    "h-captcha-response": token,
                    "g-recaptcha-response": token,
                },
                callback=self.parse_results,
                meta={"skip_captcha": True},
                dont_filter=True,
            )
            return

        yield from self.parse_results(response)

    def parse_results(self, response):
        for item in response.css("div.result"):
            yield {
                "title": item.css("h3::text").get(),
                "url": item.css("a::attr(href)").get(),
            }

Die Selektoren sind Platzhalter. Ersetze sie durch die echten der Seite, die du scrapst.

Das Flag skip_captcha verhindert, dass die Middleware den Formular-Submit erneut löst. Ohne dieses Flag würde jeder Submit eine weitere Lösung kosten.

Ausführen

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json

Behalte das Log im Blick. Eine Zeile wie PopularCaptcha on ..., solving bedeutet, dass eine Herausforderung gefunden und ein Token angefordert wurde. Siehst du diese Zeile bei jeder Seite, werde langsamer. Häufige Herausforderungen sind ein Zeichen dafür, dass die Seite deinen Crawl bemerkt hat.

Timing des Tokens

Ein PopularCaptcha-Token läuft in etwa zwei Minuten ab. Die Middleware stellt die Wiederholung sofort ein, sobald das Token eintrifft, und der Spider sendet in derselben Kette von Callbacks ab. Halte die Kette kurz. Füge zwischen Token und Formular-Submit keine langsamen Downloads oder lange Verarbeitung ein. Mehr zu diesem Zeitfenster findest du unter Ablauf von Captcha-Tokens.

Fehler

Eine fehlgeschlagene Lösung löst im Thread einen RuntimeError aus. Scrapy protokolliert das als fehlgeschlagene Anfrage. Fehlgeschlagene Lösungen werden nicht berechnet, deshalb ist eine Wiederholung günstig. Für eine Wiederholungsstrategie mit Grenzen nutze den eingebauten RetryMiddleware von Scrapy und umschließe _solve mit einer kurzen Schleife. Unser Leitfaden zur Fehlerbehandlung bei Captcha-Lösungen listet die Muster auf, die im Produktivbetrieb halten.

Verantwortungsvoll scrapen

Zum rechtlichen Hintergrund siehe Ist Web-Scraping legal?.

Preise

Scarnox rechnet pro erfolgreicher Lösung ab, und fehlgeschlagene Lösungen sind kostenlos. Die Raten findest du auf der Preisseite. Starte mit kostenlosem Testguthaben, ohne Karte.

Nächste Schritte