Почему Scrapy-пауки без прокси обречены
Вы написали элегантный паук, протестировали на десятке страниц — всё летает. Запускаете на 100K URL, и через пять минут получаете 403, CAPTCHA, пустые ответы. Anti-bot системы Banjo, PerimeterX, Cloudflare работают именно так: первый запрос с вашего IP прошёл, сотый — уже блок. Без Scrapy residential proxies и грамотной ротации любой продакшн-паук упирается в rate-limit целевого сайта.
В этом руководстве мы разберём Scrapy proxy middleware от архитектуры до деплоя: напишем собственную middleware с ротацией, добавим retry-логику, подключим headless-рендеринг и настроим мониторинг. Всё — с рабочим кодом и реальными конфигурациями ProxyHat.
Модель downloader middleware в Scrapy
Scrapy обрабатывает каждый запрос через цепочку downloader middleware — классов с методами process_request, process_response и process_exception. Порядок важен: чем меньше число в DOWNLOAD_MIDDLEWARES, тем раньше middleware выполняется на входящем запросе.
Стандартная HttpProxyMiddleware (порядок 560) читает прокси из мета-поля proxy и устанавливает его для HTTP-клиента. Это значит, что любая middleware с меньшим номером может подставить прокси до того, как запрос уйдёт в сеть.
Ключевые хуки для прокси-интеграции
process_request(request, spider)— подмена IP перед отправкой.process_response(request, response, spider)— анализ ответа, детект банов.process_exception(request, exception, spider)— обработка таймаутов, connection-ошибок.
Именно в этих трёх методах мы построим всю логику ротации и обработки сбоев.
Пишем ProxyHatRotatorMiddleware — собственную middleware с ротацией residential-прокси
Роллинг-свой middleware даёт полный контроль: вы решаете, когда менять IP, как выбирать гео и как обрабатывать sticky-сессии. Ниже — полнофункциональный класс, который ротирует residential-прокси ProxyHat с поддержкой гео-таргетинга и sticky-сессий.
import random
import string
from scrapy import signals
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
class ProxyHatRotatorMiddleware(HttpProxyMiddleware):
"""
Scrapy proxy middleware с ротацией residential-прокси через ProxyHat.
Поддерживает:
- per-request ротацию (каждый запрос = новый IP)
- sticky-сессии (один IP на N секунд)
- гео-таргетинг (страна, город)
"""
def __init__(self, username, password, gateway, port,
rotate_mode='per_request', sticky_duration=600,
country=None, city=None):
self.username = username
self.password = password
self.gateway = gateway
self.port = port
self.rotate_mode = rotate_mode
self.sticky_duration = sticky_duration
self.country = country
self.city = city
self._session_id = None
self._session_created_at = 0
@classmethod
def from_crawler(cls, crawler):
s = crawler.settings
return cls(
username=s.get('PROXYHAT_USERNAME'),
password=s.get('PROXYHAT_PASSWORD'),
gateway=s.get('PROXYHAT_GATEWAY', 'gate.proxyhat.com'),
port=s.getint('PROXYHAT_PORT', 8080),
rotate_mode=s.get('PROXYHAT_ROTATE_MODE', 'per_request'),
sticky_duration=s.getint('PROXYHAT_STICKY_DURATION', 600),
country=s.get('PROXYHAT_COUNTRY'),
city=s.get('PROXYHAT_CITY'),
)
def _build_proxy_url(self, request):
"""Конструирует URL прокси с учётом гео и сессии."""
user_parts = [self.username]
# Гео-таргетинг из настроек или из request.meta
country = request.meta.get('proxy_country', self.country)
city = request.meta.get('proxy_city', self.city)
if country:
user_parts.append(f'country-{country}')
if city:
user_parts.append(f'city-{city}')
# Режим ротации
if self.rotate_mode == 'sticky':
import time
now = int(time.time())
if (self._session_id is None or
now - self._session_created_at > self.sticky_duration):
self._session_id = ''.join(
random.choices(string.ascii_lowercase + string.digits, k=8)
)
self._session_created_at = now
user_parts.append(f'session-{self._session_id}')
# per_request — не добавляем session, ProxyHat выдаст новый IP
user = '-'.join(user_parts)
return f'http://{user}:{self.password}@{self.gateway}:{self.port}'
def process_request(self, request, spider):
# Не подставляем прокси для локальных URL
if request.meta.get('dont_proxy'):
return
proxy_url = self._build_proxy_url(request)
request.meta['proxy'] = proxy_url
# Запрещаем HttpProxyMiddleware дублировать заголовки
request.meta['_proxy_parsed'] = True
Подключение в settings.py:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatRotatorMiddleware': 540,
# Отключаем стандартную HttpProxyMiddleware — наша наследует её функционал
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}
PROXYHAT_USERNAME = 'your_user'
PROXYHAT_PASSWORD = 'your_pass'
PROXYHAT_ROTATE_MODE = 'per_request' # или 'sticky'
PROXYHAT_COUNTRY = 'US' # опционально
Теперь каждый запрос паука автоматически получает residential-IP. В per_request-режиме ProxyHat выдаёт новый IP на каждый запрос; в sticky-режиме IP держится PROXYHAT_STICKY_DURATION секунд. Вы также можете переопределить страну и город для конкретного запроса:
yield scrapy.Request(
url='https://example.de/prices',
meta={'proxy_country': 'DE', 'proxy_city': 'berlin'}
)
scrapy-rotating-proxies vs собственная middleware
Пакет scrapy-rotating-proxies — популярное community-решение. Оно управляет пулом прокси, помечает «мёртвые» IP и балансирует нагрузку. Но для residential-прокси с API-ротацией (как у ProxyHat) этот подход избыточен и даже вреден.
| Критерий | scrapy-rotating-proxies | ProxyHatRotatorMiddleware |
|---|---|---|
| Управление пулом IP | Ручной список в настройках | API ProxyHat — бесконечный пул |
| Детект банов | По HTTP-статусам из коробки | Кастомная логика в process_response |
| Гео-таргетинг | Нет | Страна/город через username-флаги |
| Sticky-сессии | Нет | Встроены через session-ID |
| Зависимости | Дополнительный пакет + поддержка | Только Scrapy + ProxyHat |
| Масштабирование | Ограничено размером списка | Не ограничено — residential-пул |
Вывод: если вы используете residential-прокси с ротацией на стороне провайдера — пишите свою middleware. Она короче, понятнее и лучше интегрируется с гео-таргетингом.
Retry-логика с прокси-ротацией: обрабатываем failures per-request
Стандартный RetryMiddleware Scrapy (порядок 500) повторяет запрос при ошибках — но на том же IP. Для заблокированного запроса это бессмысленно: второй попытке нужен другой IP. Решение — кастомная middleware, которая сбрасывает sticky-сессию перед retry.
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
class ProxyHatRetryMiddleware(RetryMiddleware):
"""
Retry-логика, которая при повторной попытке сбрасывает прокси-сессию,
чтобы следующий запрос пошёл через новый IP.
"""
def __init__(self, settings):
super().__init__(settings)
self.ban_codes = set(
settings.getlist('PROXYHAT_BAN_STATUS_CODES', [403, 429])
)
def process_response(self, request, response, spider):
# Если статус — признак бана, форсируем ротацию IP
if response.status in self.ban_codes:
spider.logger.debug(
f'Ban detected ({response.status}) for {request.url}, '
f'rotating proxy on retry'
)
# Сбрасываем sticky-сессию, чтобы следующий запрос
# получил новый IP
request.meta.pop('_proxy_session', None)
request.meta['proxy_country'] = request.meta.get('proxy_country')
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return super().process_response(request, response, spider)
def process_exception(self, request, exception, spider):
# Таймауты и connection-ошибки — тоже повод сменить IP
spider.logger.debug(
f'Proxy exception {type(exception).__name__} for {request.url}'
)
request.meta.pop('_proxy_session', None)
return super().process_exception(request, exception, spider)
Настройки:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatRetryMiddleware': 550,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
}
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 403, 429]
PROXYHAT_BAN_STATUS_CODES = [403, 429]
Теперь при 403/429 Scrapy делает retry, а ProxyHatRotatorMiddleware выдаёт новый IP. Это закрывает 90% сценариев блокировок.
JS-тяжёлые сайты: scrapy-splash и scrapy-playwright с прокси
Многие сайты рендерят контент через JavaScript. Scrapy по умолчанию получает только исходный HTML. Два популярных решения — scrapy-splash и scrapy-playwright.
scrapy-splash + прокси
Splash передаёт прокси через аргументы Lua-скрипта. ProxyHat подключается так:
import scrapy
class JSSpider(scrapy.Spider):
name = 'js_spider'
def start_requests(self):
urls = ['https://example.com/js-heavy-page']
for url in urls:
yield scrapy.Request(
url,
self.parse,
meta={
'splash': {
'args': {
'html': 1,
'proxy': 'http://user-country-US:pass@gate.proxyhat.com:8080',
'wait': 3,
},
'endpoint': 'render.html',
},
}
)
scrapy-playwright + прокси
Playwright позволяет задать прокси на уровне браузера:
PLAYWRIGHT_LAUNCH_OPTIONS = {
'proxy': {
'server': 'http://gate.proxyhat.com:8080',
'username': 'user-country-US',
'password': 'your_pass',
}
}
Или per-request через мета:
yield scrapy.Request(
url='https://example.com',
meta={
'playwright': True,
'playwright_include_page': False,
'proxy_country': 'US', # наша middleware подставит нужный IP
}
)
Важно: при использовании Playwright наша ProxyHatRotatorMiddleware будет работать, потому что Playwright тоже читает request.meta['proxy']. Но если вы задаёте прокси в PLAYWRIGHT_LAUNCH_OPTIONS, middleware-подстановка игнорируется — выберите один подход.
Деплой: Scrapyd, ScrapeOps или Docker + cron
Вариант 1 — Scrapyd
Scrapyd — стандартный сервис для запуска Scrapy-пауков. Деплой через scrapyd-deploy, запуск через API. Проблема: нет встроенного мониторинга, нет горизонтального масштабирования. Подходит для 1–3 пауков с предсказуемой нагрузкой.
Вариант 2 — ScrapeOps
ScrapeOps — managed-платформа с дашбордом, алертами и автоскейлингом. Удобно для команд без DevOps. Но вы привязаны к платформе и её тарифам.
Вариант 3 — Docker + cron (наш выбор)
Простой, воспроизводимый и масштабируемый вариант. Docker-образ с пауком, cron для расписания, docker-compose для оркестрации.
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["scrapy", "crawl", "my_spider"]
# docker-compose.yml
version: '3.8'
services:
spider:
build: .
environment:
- PROXYHAT_USERNAME=${PROXYHAT_USERNAME}
- PROXYHAT_PASSWORD=${PROXYHAT_PASSWORD}
deploy:
replicas: 3 # 3 параллельных инстанса
restart_policy:
condition: on-failure
Запуск по расписанию через cron на хост-машине:
# Каждый день в 06:00 UTC
0 6 * * * cd /opt/myproject && docker compose up --build --remove-orphans
Для масштабирования увеличивайте replicas и CONCURRENT_REQUESTS. ProxyHat residential-пул не ограничен — каждый инстанс получит уникальные IP.
Мониторинг: per-IP success rate и детект банов
В продакшн-скрейпинге критически знать: какой процент запросов проходит успешно и когда сайт начинает блокировать. Добавим статистику в нашу middleware.
from scrapy import signals
from collections import defaultdict
class ProxyHatStatsMiddleware:
"""
Собирает статистику по успехам/провалам и детектит баны.
"""
def __init__(self):
self.total = defaultdict(int)
self.banned = defaultdict(int)
self.ban_threshold = 0.3 # 30% банов = тревога
@classmethod
def from_crawler(cls, crawler):
o = cls()
crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
return o
def process_response(self, request, response, spider):
domain = response.url.split('/')[2]
self.total[domain] += 1
if response.status in (403, 429, 503):
self.banned[domain] += 1
ratio = self.banned[domain] / self.total[domain]
if ratio > self.ban_threshold:
spider.logger.warning(
f'Ban ratio for {domain}: {ratio:.1%} — '
f'consider slower rate or different geo'
)
return response
def spider_closed(self, spider, reason):
spider.logger.info('=== Proxy success rates ===')
for domain in self.total:
success = 1 - (self.banned[domain] / self.total[domain])
spider.logger.info(
f'{domain}: {success:.1%} success '
f'({self.total[domain]} requests)'
)
Подключение:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyHatStatsMiddleware': 900,
}
При закрытии паука вы получите лог вида:
INFO: example.com: 94.2% success (5230 requests)
WARNING: shop.example.de: 68.1% success — consider slower rate or different geo
Для продакшн-мониторинга отправляйте эти метрики в Prometheus/Grafana через scrapy-stats-prometheus или в Datadog через statsd.
Ключевые выводы
- Пишите свою middleware — для residential-прокси с API-ротацией это проще и мощнее, чем scrapy-rotating-proxies.
- Ротация IP при retry — критически важна: повторный запрос на том же IP не решит проблему бана.
- Гео-таргетинг через username — ProxyHat позволяет указать страну и город без отдельного пула IP.
- Sticky-сессии — для сайтов с авторизацией используйте
session-*флаги, чтобы IP не менялся внутри логической сессии.- Мониторьте success rate — если доля 403/429 превышает 20–30%, снижайте
CONCURRENT_REQUESTSили меняйте гео.- Docker + cron — простой и воспроизводимый деплой без vendor lock-in.
Готовы начать? Зарегистрируйтесь на ProxyHat — residential, mobile и datacenter прокси с ротацией из коробки, гео-таргетинг по 190+ странам и мгновенная замена IP при бане.
Читайте также: Руководство по residential-прокси для Scrapy и кейсы веб-скрейпинга с ProxyHat.






