Middleware для капчі в Scrapy: розв'язання капчі у вашому павуку (2026)
Scrapy побудована навколо запитів і колбеків, тож капча не вкладається в неї охайно. Правильний спосіб її обробити - 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. Колбек перетворює токен на повторний запит, і 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 запитує повтор одразу, як тільки приходить токен, а павук відправляє форму в тому самому ланцюжку колбеків. Тримайте ланцюжок коротким. Не додавайте повільних завантажень або довгої обробки між токеном і відправкою форми. Більше про це вікно див. у терміні дії токена капчі.
Помилки
Невдале розв'язання викликає RuntimeError у потоці. Scrapy зафіксує це як невдалий запит. Невдалі розв'язання не тарифікуються, тож повторити дешево. Для політики повторів з обмеженнями використовуйте вбудований RetryMiddleware Scrapy і обгорніть _solve у короткий цикл. Наш посібник про обробку помилок розв'язувача капчі перелічує шаблони, які витримують продакшен.
Скрапінг відповідально
- Дотримуйтеся robots.txt.
ROBOTSTXT_OBEY = Trueвище це забезпечує. Тримайте увімкненим. - Тримайте затримку високою. Перевірка - ознака того, що сайт хоче, щоб ви сповільнилися. Не обходьте затримку, щоб закінчити швидше.
- Прочитайте умови використання перед краулінгом. Деякі сайти забороняють автоматизований доступ повністю.
- Обережно поводьтеся з персональними даними. Зберігайте лише потрібні поля і захищайте те, що зберігаєте.
Про правовий бік див. чи законний веб-скрапінг.
Ціни
Scarnox бере плату за кожне успішне розв'язання, а невдалі розв'язання безкоштовні. Актуальні тарифи дивіться на сторінці тарифів. Почніть з безкоштовного пробного кредиту, картка не потрібна.