Scarnox · Blog

Middleware для Scrapy, который решает капчу в пауке (2026)

Scrapy построен вокруг запросов и callback-функций, поэтому капча в нем не вписывается напрямую. Чистый способ с ней справиться — downloader middleware: он видит каждый ответ, замечает проверку, получает токен, а паук отправляет форму с этим токеном. Это руководство строит такой middleware для страниц PopularCaptcha, которые Scarnox решает сегодня без прокси.

Начните с бесплатного пробного баланса, карта не нужна и протестируйте код на тестовой странице, прежде чем запускать его на чем-либо еще.

Как работает middleware

Middleware выполняется на каждом ответе:

  1. Если в ответе нет data-sitekey, ответ пропускается дальше.
  2. Если он есть, middleware решает капчу и возвращает копию того же запроса с токеном, сохраненным в meta.
  3. Scrapy выполняет этот запрос снова. Middleware видит токен в meta и пропускает ответ.
  4. Паук читает токен из 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 в короткий цикл. Наше руководство по обработке ошибок решателя капчи перечисляет проверенные в продакшене шаблоны.

Ответственный парсинг

Юридический фон — в законности парсинга сайтов.

Цены

Scarnox списывает оплату за каждое успешное решение, а неудачные решения бесплатны. Тарифы — на странице цен. Начните с бесплатного пробного баланса, карта не нужна.

Следующие шаги