Scarnox · Blog

Middleware captcha dla Scrapy: rozwiązuj captcha w spiderze (2026)

Scrapy jest zbudowane wokół żądań i callbacków, więc captcha nie pasuje tu idealnie. Czysty sposób obsługi to middleware downloadera: widzi każdą odpowiedź, zauważa wyzwanie, pobiera token, a spider wysyła formularz z tym tokenem. Ten przewodnik buduje takie middleware dla stron PopularCaptcha, które Scarnox dziś rozwiązuje w trybie proxyless.

Zacznij z darmowym kredytem próbnym, bez karty i przetestuj kod na stronie testowej, zanim skierujesz go gdziekolwiek indziej.

Jak działa middleware

Middleware uruchamia się przy każdej odpowiedzi:

  1. Jeśli odpowiedź nie ma data-sitekey, przepuszcza ją dalej.
  2. Jeśli ma, middleware rozwiązuje captcha i zwraca kopię tego samego żądania z tokenem zapisanym w meta.
  3. Scrapy ponownie pobiera to żądanie. Middleware widzi token w meta i przepuszcza odpowiedź.
  4. Spider odczytuje token z meta i wysyła formularz przez FormRequest.from_response.

Wywołanie solvera działa w wątku przez deferToThread, więc nie blokuje reactora Scrapy. Inne żądania posuwają się naprzód, gdy captcha jest rozwiązywane.

Ustawienia projektu

Klucz trzymaj w zmiennej środowiskowej, a nie w kodzie:

# 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

Opóźnienie pobierania i pojedyncza współbieżność na domenę są tu celowe. Utrzymują niskie obciążenie strony i zmniejszają liczbę wyzwań, które widzisz.

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,  # działa w wątku roboczym, nie w reactorze
        )
        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):
        # Już rozwiązane albo wysyłka formularza zbudowana przez nas: przepuść dalej.
        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:
            # Zwrócenie Request planuje go ponownie. dont_filter omija filtr duplikatów.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

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

Scrapy obsługuje zwracanie Deferred z process_response. Callback zamienia token w ponowione żądanie, a Scrapy planuje je jak każde inne.

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 wypełnia własne pola formularza; my dokładamy token.
            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(),
            }

Selektory są przykładowe. Zastąp je prawdziwymi z witryny, którą scrapujesz.

Flaga skip_captcha powstrzymuje middleware przed ponownym rozwiązywaniem wysyłki formularza. Bez niej każde wysłanie kosztowałoby kolejne rozwiązanie.

Uruchomienie

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

Obserwuj log. Linia taka jak PopularCaptcha on ..., solving oznacza, że znaleziono wyzwanie i zażądano tokena. Jeśli widzisz tę linię na każdej stronie, zwolnij. Częste wyzwania to znak, że strona zauważyła crawl.

Czas tokena

Token PopularCaptcha wygasa po około dwóch minutach. Middleware żąda ponowienia zaraz po nadejściu tokena, a spider wysyła formularz w tym samym łańcuchu callbacków. Utrzymuj ten łańcuch krótkim. Nie dodawaj wolnych pobrań ani długiego przetwarzania między tokenem a wysłaniem formularza. Więcej o tym oknie czasowym znajdziesz w wygasanie tokena captcha.

Błędy

Nieudane rozwiązanie zgłasza RuntimeError w wątku. Scrapy zaloguje to jako nieudane żądanie. Nieudane rozwiązania nie są naliczane, więc ponawianie jest tanie. Jeśli potrzebujesz polityki ponowień z limitami, użyj wbudowanego w Scrapy RetryMiddleware i opakuj _solve w krótką pętlę. Nasz przewodnik obsługa błędów solvera captcha wymienia wzorce, które sprawdzają się w produkcji.

Scrapuj odpowiedzialnie

Tło prawne znajdziesz w czy web scraping jest legalny.

Cennik

Scarnox pobiera opłatę za udane rozwiązanie, a nieudane są bezpłatne. Stawki znajdziesz na stronie cennika. Zacznij z darmowym kredytem próbnym, bez karty.

Dalsze kroki