Middleware для Scrapy, который решает капчу в пауке (2026)
Scrapy построен вокруг запросов и callback-функций, поэтому капча в нем не вписывается напрямую. Чистый способ с ней справиться — downloader middleware: он видит каждый ответ, замечает проверку, получает токен, а паук отправляет форму с этим токеном. Это руководство строит такой middleware для страниц PopularCaptcha, которые Scarnox решает сегодня без прокси.
Начните с бесплатного пробного баланса, карта не нужна и протестируйте код на тестовой странице, прежде чем запускать его на чем-либо еще.
Как работает middleware
Middleware выполняется на каждом ответе:
- Если в ответе нет
data-sitekey, ответ пропускается дальше. - Если он есть, middleware решает капчу и возвращает копию того же запроса с токеном, сохраненным в
meta. - Scrapy выполняет этот запрос снова. Middleware видит токен в
metaи пропускает ответ. - Паук читает токен из
metaи отправляет форму черезFormRequest.from_response.
Вызов решения выполняется в потоке через deferToThread, поэтому он не блокирует reactor Scrapy. Другие запросы продолжают идти, пока капча решается.
Настройки проекта
Ключ храните в переменной окружения, а не в коде:
# 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
Задержка загрузки и одна параллельная задача на домен здесь намеренно. Они снижают нагрузку на сайт и уменьшают число проверок, которые вы видите.
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, # работает в рабочем потоке, а не в reactor
)
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):
# Уже решено или это отправка формы, которую мы собрали сами: пропускаем.
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:
# Возврат Request переставляет его в очередь. dont_filter отключает фильтр дубликатов.
return request.replace(
dont_filter=True,
meta={**request.meta, "captcha_token": token},
)
return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)
Scrapy поддерживает возврат Deferred из process_response. Callback превращает токен в повторный запрос, и Scrapy ставит его в очередь, как любой другой.
Паук
# 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 заполняет поля формы сама, мы добавляем токен.
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(),
}
Селекторы — заглушки. Замените их настоящими с сайта, который парсите.
Флаг skip_captcha не дает middleware решать отправку формы повторно. Без него каждая отправка стоила бы еще одно решение.
Запуск
export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json
Следите за логом. Строка вида PopularCaptcha on ..., solving означает, что проверка найдена и токен запрошен. Если эта строка появляется на каждой странице, замедлитесь. Частые проверки — признак того, что сайт заметил краулинг.
Тайминг токена
Токен PopularCaptcha истекает примерно через две минуты. Middleware запрашивает повтор, как только токен приходит, а паук отправляет форму в той же цепочке callback. Держите цепочку короткой. Не добавляйте медленных загрузок или долгой обработки между токеном и отправкой формы. Подробнее об этом окне — в сроке действия токена капчи.
Ошибки
Неудачное решение вызывает RuntimeError внутри потока. Scrapy запишет это в лог как неудачный запрос. Неудачные решения не тарифицируются, поэтому повторы дешевы. Для политики повторов с ограничениями используйте встроенный RetryMiddleware Scrapy и оберните _solve в короткий цикл. Наше руководство по обработке ошибок решателя капчи перечисляет проверенные в продакшене шаблоны.
Ответственный парсинг
- Соблюдайте robots.txt.
ROBOTSTXT_OBEY = Trueвыше делает это. Держите его включенным. - Держите задержку высокой. Проверка — признак того, что сайт хочет, чтобы вы замедлились. Не обходите задержку, чтобы закончить быстрее.
- Прочитайте условия использования перед краулингом. Некоторые сайты прямо запрещают автоматический доступ.
- Аккуратно обращайтесь с персональными данными. Храните только нужные поля и защищайте то, что сохраняете.
Юридический фон — в законности парсинга сайтов.
Цены
Scarnox списывает оплату за каждое успешное решение, а неудачные решения бесплатны. Тарифы — на странице цен. Начните с бесплатного пробного баланса, карта не нужна.