Scrapy Proxy Middleware: ротация residential-прокси, обработка сбоев и масштабирование

Практическое руководство по интеграции прокси в Scrapy — от собственной middleware с ротацией residential-IP до retry-логики, headless-рендеринга и продакшн-деплоя. Код, паттерны, сравнения.

Scrapy Proxy Middleware: ротация residential-прокси, обработка сбоев и масштабирование

Почему 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.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог