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:
- Jeśli odpowiedź nie ma
data-sitekey, przepuszcza ją dalej. - Jeśli ma, middleware rozwiązuje captcha i zwraca kopię tego samego żądania z tokenem zapisanym w
meta. - Scrapy ponownie pobiera to żądanie. Middleware widzi token w
metai przepuszcza odpowiedź. - Spider odczytuje token z
metai wysyła formularz przezFormRequest.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
- Stosuj się do robots.txt.
ROBOTSTXT_OBEY = Truepowyżej to zapewnia. Zostaw to włączone. - Utrzymuj wysokie opóźnienie. Wyzwanie to sygnał, że strona chce, byś zwolnił. Nie obchodź opóźnienia, żeby skończyć szybciej.
- Przeczytaj regulamin przed crawlowaniem. Niektóre strony zakazują zautomatyzowanego dostępu w całości.
- Obchodź się ostrożnie z danymi osobowymi. Przechowuj tylko pola, których potrzebujesz, i chroń to, co zachowujesz.
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.