# Scrapy Proxy Middleware: Ротация прокси без блокировок

Scrapy поддерживает прокси с версии 0.8, но документация никогда не объясняет, как вращать, повторять и избегать блокировок в масштабах. Вот анатомия middleware, код и настройки, которые определяют, завершится ли ваш обход.

Scrapy поддерживает proxy middleware с версии 0.8, так что вопрос 'поддерживает ли Scrapy прокси' был решен пятнадцать лет назад. То, что документация никогда не собирает воедино, — это картина в производстве: как встроенное proxy middleware Scrapy действительно обрабатывает учетные данные, когда устанавливать прокси для каждого запроса, а когда глобально, и как вращать и восстанавливаться, когда цель начинает блокировать выходы в середине обхода. Это руководство охватывает всю цепочку — встроенное `HttpProxyMiddleware`, мета на каждый запрос, пользовательское middleware для ротации с обработкой блокировок и настройки, которые определяют, завершится ли обход на 100 тыс. страниц или умрет в 3 часа ночи.

## Как работает встроенное proxy middleware Scrapy

`scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware` включено по умолчанию с приоритетом 750 в `DOWNLOADER_MIDDLEWARES_BASE`. Чтение его исходного кода (около 100 строк в Scrapy 2.17) расскажет вам все, что нужно для отладки:

- При запуске он вызывает `urllib.request.getproxies()`, так что переменные окружения `http_proxy` / `https_proxy` настраивают паука без кода. `no_proxy` учитывается только для схем http и https.
- Для каждого запроса `request.meta['proxy']` всегда имеет приоритет над окружением. Установите его в `None`, чтобы принудительно установить прямое соединение для этого запроса.
- Учетные данные, встроенные в URL прокси (`http://user:pass@host:port`), удаляются, кодируются в Base64 и отправляются в виде заголовка `Proxy-Authorization: Basic`. Кодировка по умолчанию — latin-1, и ее можно настроить через `HTTPPROXY_AUTH_ENCODING`.
- Middleware декодирует учетные данные перед их кодированием, поэтому URL-кодируйте специальные символы, такие как `@` в паролях — они проходят правильно.
- Если повторный запрос меняет прокси, middleware сбрасывает устаревший заголовок аутентификации, чтобы не утекать его новому прокси — это исправление гигиены учетных данных в современном Scrapy и хорошая причина не использовать древние версии.

Практическое следствие: вам почти никогда не нужен сторонний пакет прокси. Все, что помещает действительный URL прокси в `request.meta['proxy']` до приоритета 750, получает аутентификацию и обработку заголовков бесплатно.

## Прокси на каждый запрос с помощью мета

Самая легкая интеграция — установка `meta` непосредственно в пауке — полезно, когда только некоторые запросы нуждаются в прокси или разные цели требуют разных стран выхода:

```python
import scrapy

class PricesSpider(scrapy.Spider):
    name = "prices"

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/product/1",
            meta={"proxy": "http://USER:PASS@residentialpremium.quantumproxies.io:8080"},
        )
```

Мета на каждый запрос особенно полезна, когда выбор прокси сам по себе зависит от данных: направляйте немецкие страницы продуктов через имя пользователя, ориентированное на Германию, отправляйте загрузки изображений через дешевые выходы из дата-центров, а HTML — через резидентные, или повышайте упорный URL до более устойчивой сессии при второй попытке. Поскольку мета сохраняется при повторных запросах и перенаправлениях, решение, которое вы принимаете при создании запроса, следует за ним на протяжении всего цикла загрузки. Установка мета на тысячи запросов вручную не масштабируется, однако — для этого и нужны downloader middlewares.

## Самая простая производственная настройка: вращающийся шлюз

С [вращающимися прокси](https://quantumproxies.io/rotating-proxies) за одним шлюзовым конечным узлом ротация происходит на стороне сервера: каждый запрос через один и тот же URL выходит с другого IP из пула более 90 миллионов резидентных IP, охватывающего более 200 стран. Ваше middleware сокращается до трех строк, и нет списка для проверки состояния, удаления или обновления:

```python
# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

    def process_request(self, request, spider):
        request.meta["proxy"] = self.PROXY

# settings.py
DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.RotatingGatewayMiddleware": 350,
}
```

Приоритет 350 важен: ваше middleware должно работать до встроенного на 750, чтобы `HttpProxyMiddleware` все еще мог преобразовать учетные данные в заголовок аутентификации. Эта пара — ротация через шлюз плюс стандартная механика повторов Scrapy — обеспечивает наибольшую надежность на строку кода, потому что каждый повтор автоматически проходит через новый выходной IP.

![Запрос Scrapy проходит цепочку downloader middleware: паук выдает его, ваше middleware с приоритетом 350 задает meta'proxy' на шлюз, встроенное HttpProxyMiddleware с приоритетом 750 превращает учетные данные в заголовок Proxy-Authorization, а загрузчик выходит через ротационный шлюз; повторные попытки получают новый IP.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag1.png)
*Вашему middleware нужно только установить meta['proxy']; встроенное на приоритете 750 обрабатывает учетные данные, и повторы снова входят в цепочку на новом IP.*

## Пользовательское middleware для ротации прокси с обработкой блокировок

Если вы действительно используете свой собственный список прокси — статические адреса ISP или смешанный пул — классический шаблон (популяризированный пакетом scrapy-proxies, который рекомендовал `RETRY_TIMES = 10`, потому что бесплатные списки прокси часто не работают) таков: выбирайте случайным образом для каждого запроса, исключайте при сигналах блокировки, повторно ставьте запрос в очередь:

```python
import random

class ProxyListMiddleware:
    BAN_CODES = {403, 429}

    def __init__(self, proxies):
        self.proxies = list(proxies)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("PROXY_LIST"))

    def process_request(self, request, spider):
        if self.proxies and "proxy" not in request.meta:
            request.meta["proxy"] = random.choice(self.proxies)

    def process_response(self, request, response, spider):
        if response.status in self.BAN_CODES:
            bad = request.meta.get("proxy")
            if bad in self.proxies and len(self.proxies) > 1:
                self.proxies.remove(bad)
                spider.logger.warning("Evicted %s (%d left)", bad, len(self.proxies))
            return request.replace(dont_filter=True)  # re-queue on a new proxy
        return response
```

Обратите внимание на все, что это middleware должно делать, что шлюз делает бесплатно: отслеживать состояние пула, исключать сгоревшие IP, повторно ставить запросы в очередь. Оно также сокращается под нагрузкой — пул из 20 статических IP может испариться за считанные минуты на агрессивной цели. Более широкая стратегия обхода блокировок (темп, заголовки, дисциплина сессий) находится в нашем [контрольном списке по избежанию блокировки IP](https://quantumproxies.io/blog/avoid-ip-ban-web-scraping).

## Настройки Scrapy, которые определяют результат

Middleware размещает прокси; настройки определяют, как ведет себя обход. Эти наиболее важны:

```python
# settings.py — a sane baseline for proxied crawls
RETRY_TIMES = 5
RETRY_HTTP_CODES = [429, 403, 500, 502, 503, 504, 408]

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
```

- **RETRY_HTTP_CODES** — добавьте 429 и 403, чтобы мягкие блокировки вызывали повтор (и, с ротацией, новый IP) вместо неудачного элемента.
- **RETRY_TIMES** — 3-5 подходит для качественного пула; необходимость в 10 — признак того, что сам пул нездоров.
- **CONCURRENT_REQUESTS_PER_DOMAIN** — реальный регулятор вежливости. Ротация распределяет нагрузку по IP, но цель все равно видит общий объем.
- **DOWNLOAD_TIMEOUT** — стандартные 180 секунд позволяют одному медленному выходу занимать слот параллельности в течение трех минут. 30 вполне достаточно.
- **AUTOTHROTTLE** — адаптирует темп к наблюдаемой задержке; хорошо сочетается с ротацией на целях с ограничением скорости. Все еще тонете в 429? Наш [гид по ограничению скорости](https://quantumproxies.io/blog/fix-429-too-many-requests-scraping) охватывает бюджеты параллельности в деталях.

![Две схемы ротации в Scrapy бок о бок: самодельный список прокси, который сокращается по мере исключения забаненных IP и повторной постановки запросов в очередь, и ротационный шлюз, то есть один эндпоинт с ротацией на стороне сервера. Карточка: список означает проверки состояния и исключение, 20 статических IP могут испариться за минуты, а шлюз отправляет каждую повторную попытку с нового IP.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag2.png)
*Список своими руками означает проверки состояния, логику исключения и устаревшие IP. Шлюз — это одна конечная точка, которая вращается на стороне сервера — повторы по умолчанию попадают на свежие IP.*

## Обнаружение блокировок за пределами кодов состояния

Сложные цели не отправляют честные 403. Они выдают 200, содержащие страницу CAPTCHA, пустую сетку продуктов или очищенный шаблон. Надежные пауки проверяют контент, а не только статус — ищите элемент, который существует только на реальной странице, и повторно ставьте в очередь, когда он отсутствует. Если страницы возвращаются структурно пустыми даже через хорошие прокси, контент, вероятно, рендерится на стороне клиента; см. [почему скреперы получают пустые страницы](https://quantumproxies.io/blog/scraper-returns-empty-page-javascript). И когда один домен побеждает простой HTTP независимо от IP, передайте этот домен [Scraper API](https://quantumproxies.io/scraper-api), который рендерит JavaScript и возвращает чистый HTML или markdown — Scrapy потребляет его, как любой другой ответ, и вы сохраняете свой конвейер.

```python
def parse(self, response):
    if not response.css("div.product-grid"):
        # 200 OK but the real content is missing: soft ban or JS wall
        yield response.request.replace(dont_filter=True)
        return
    for product in response.css("div.product-grid article"):
        yield {"name": product.css("h2::text").get()}
```

## Часто задаваемые вопросы

### Поддерживает ли Scrapy прокси из коробки?

Да. `HttpProxyMiddleware` поставляется включенным с приоритетом 750: он читает переменные окружения `http_proxy` / `https_proxy` и учитывает `request.meta['proxy']` для каждого запроса, включая учетные данные `user:pass@`, которые он преобразует в заголовок Proxy-Authorization. Вам нужно только написать код, чтобы решить, какой прокси получает каждый запрос.

### Как установить прокси для одного запроса в Scrapy?

Передайте его в мета запроса: `scrapy.Request(url, meta={'proxy': 'http://user:pass@host:port'})`. Мета всегда имеет приоритет над прокси на уровне окружения, а установка значения в `None` заставляет этот один запрос идти напрямую — полезно для смешивания проксированного и непроксированного трафика в одном пауке.

### Как вращать прокси в Scrapy?

Либо напишите downloader middleware, которое выбирает прокси для каждого запроса из списка и исключает заблокированные, либо направьте каждый запрос на вращающийся шлюзовый конечный узел, который назначает свежий выходной IP на стороне сервера. Подход с шлюзом требует трехстрочного middleware, не требует проверок состояния и превращает каждый повтор Scrapy в ротацию бесплатно.

### Почему мой Scrapy прокси возвращает 407?

Прокси отклонил аутентификацию. Проверьте, что учетные данные находятся внутри URL прокси в мета, что специальные символы URL-кодированы и что ваш IP сервера находится в белом списке, если ваш план использует аутентификацию по IP. Если учетные данные содержат не-ASCII символы, установите `HTTPPROXY_AUTH_ENCODING` в соответствии с вашим провайдером.

Шаблон, который нужно запомнить: установите `meta['proxy']` рано, позвольте встроенному middleware на 750 выполнить аутентификацию, добавьте 429 и 403 в ваши коды повторов и предпочитайте ротацию на стороне сервера вместо ухода за списком. Если части вашего обхода нуждаются в JavaScript, взвесьте [стоимость безголовых браузеров по сравнению с HTTP-запросами](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost) перед тем, как использовать один — большинству проектов Scrapy нужны лучшие IP, а не браузер.

[Подключите вращающиеся прокси к вашему проекту Scrapy](https://quantumproxies.io/rotating-proxies)
