Интеграция8 мин чтения

Прокси в Scrapy 2.19: авторизация, ротация, ошибки

Настройка прокси в Scrapy 2.19: учётные данные в meta["proxy"], ротация по запросам, HTTP_PROXY и разбор TunnelError 407, пропущенного ответа 407 и ошибки socks5.

На этой странице

Укажите учётные данные в URL прокси и передайте его в meta["proxy"]:

python
import scrapy

PROXY = "http://user:pass@proxy.example.com:8080"


class ExampleSpider(scrapy.Spider):
    name = "example"

    async def start(self):
        yield scrapy.Request("https://example.com/", meta={"proxy": PROXY})

    def parse(self, response):
        self.logger.info("%s via %s", response.status, response.meta["proxy"])

HttpProxyMiddleware включён по умолчанию (HTTPPROXY_ENABLED = True). Он переносит имя пользователя и пароль из URL в заголовок Proxy-Authorization (документация), поэтому в response.meta["proxy"] остаётся URL без учётных данных.

В нашей лаборатории так прошли авторизацию запросы к https:// и http:// через локальный прокси, который требует Basic-авторизацию. Всё, что описано ниже, записано 7 октября 2026 года на Scrapy 2.19.0, Twisted 26.4.0 и Python 3.14.4.

Три детали, прежде чем перейти к ошибкам:

  • Кодируйте пароль процентами. p@ss:w/rd сработал в виде p%40ss%3Aw%2Frd. Без кодирования запрос завершился ошибкой ValueError: Port could not be cast to integer value as 'w'.
  • Запросы, созданные в callback, не наследуют прокси. Запрос, созданный через response.follow(), ушёл напрямую. При редиректе прокси сохранился. Задавайте meta["proxy"] в каждом запросе или в downloader middleware, как показано ниже.
  • Пишите async def start(). Паук, в котором был только start_requests(), открылся, не отправил ни одного запроса и закрылся с finish_reason: finished без предупреждений. В Scrapy 2.16.0 метод удалён: он «is removed and no longer called» (примечания к версии). Его заменяет Spider.start(). Миграция, включая spider middleware, описана в руководстве Scrapy: start_requests() не вызывается.

Ротация прокси: задавайте meta["proxy"] для каждого запроса

Ротация — это downloader middleware, который выполняется раньше HttpProxyMiddleware (порядок 750) и задаёт URL прокси вместе с учётными данными:

python
from itertools import cycle

PROXIES = cycle([
    "http://user:pass@proxy-a.example.com:8080",
    "http://user:pass@proxy-b.example.com:8080",
])


class RotateProxyMiddleware:
    def process_request(self, request):
        request.meta["proxy"] = next(PROXIES)
python
DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.RotateProxyMiddleware": 740}

В лаборатории запрос с редиректом прошёл через этот middleware дважды, попал на два прокси и авторизовался на обоих.

Не работает вариант, в котором URL задан без учётных данных, а заголовок выставляется вручную:

python
request.meta["proxy"] = next(PROXIES)  # "http://proxy-a.example.com:8080"
request.headers["Proxy-Authorization"] = "Basic dXNlcjpwYXNz"

Первый запрос пришёл на прокси A с заголовком. Целевой сервер ответил 301, и запрос после редиректа снова прошёл через все middleware. Ротация выбрала прокси B и снова выставила заголовок, но прокси B получил запрос без Proxy-Authorization и ответил 407. Повторённый запрос к https:// потерял заголовок точно так же.

HttpProxyMiddleware запоминает, с каким URL прокси был отправлен выставленный вручную заголовок. Если при следующем проходе того же запроса URL прокси другой и без учётных данных, заголовок удаляется. Причина описана в примечаниях к версии 2.6.2: middleware для ротации меняли прокси и оставляли старый заголовок, так что учётные данные одного прокси уходили другому. Начиная с 2.7.1 выставленный вручную заголовок сохраняется только «for as long as that proxy URL remains the same». Учётные данные из URL всегда его заменяют.

Ещё два результата того же кода:

  • На первом проходе заголовок ещё не привязан к прокси. Middleware, который сменил meta["proxy"] с A на B до запуска HttpProxyMiddleware, отправил выставленный вручную заголовок на прокси B.
  • Когда прокси не было вовсе, заголовок был удалён до того, как запрос дошёл до целевого сервера.

Переменные окружения: HTTP_PROXY действует по умолчанию

Scrapy читает переменные прокси при запуске обхода, и включать это настройкой не нужно. С HTTP_PROXY=http://user:pass@127.0.0.1:8888 и без meta["proxy"] запрос в лаборатории прошёл через прокси и авторизовался.

  • HTTP_PROXY и http_proxy в нижнем регистре применялись к URL http://, а HTTPS_PROXY — к URL https://. Когда была задана только HTTP_PROXY, запрос к https:// ушёл напрямую. ALL_PROXY игнорировалась.
  • NO_PROXY=127.0.0.1 отправила запрос напрямую. meta["proxy"] имел приоритет над переменными и не учитывал NO_PROXY, как и сказано в документации.
  • meta={"proxy": None} отправил один запрос напрямую при заданной переменной.
  • HTTPPROXY_ENABLED = False отключил чтение переменных. Вместе с этим отключилась и обработка учётных данных: meta["proxy"] = "http://user:pass@127.0.0.1:8888" после этого вызвал ValueError: invalid hostname: user:pass@127.0.0.1 для URL http:// и TunnelError с кодом 407 для https://.

Чтобы игнорировать окружение и сохранить работу meta["proxy"], замените middleware подклассом, который отбрасывает прочитанное:

python
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware


class ExplicitProxyMiddleware(HttpProxyMiddleware):
    def __init__(self, auth_encoding="latin-1"):
        super().__init__(auth_encoding)
        self.proxies = {}
python
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": None,
    "myproject.middlewares.ExplicitProxyMiddleware": 750,
}

proxies — недокументированный атрибут, поэтому после обновления проверьте это ещё раз. aiohttp ведёт себя наоборот: он игнорирует переменные, пока вы не передадите trust_env=True (Прокси в aiohttp 3.14). Руководство Переменные окружения прокси сравнивает другие клиенты.

TunnelError: Could not open CONNECT tunnel with proxy

code
scrapy.core.downloader.handlers.http11.TunnelError: Could not open CONNECT tunnel with proxy 127.0.0.1:8888 [{'status': 407, 'reason': b'Proxy Authentication Required'}]

Прокси отклонил запрос CONNECT для URL https://. Сообщение было одинаковым и без учётных данных, и с неверным паролем, и самих учётных данных в нём нет, поэтому по нему нельзя понять, какой из двух случаев у вас. Проверьте URL в meta["proxy"] у запроса, который завершился ошибкой, а затем пройдите порядок проверок из руководства Ошибка прокси 407.

  • TunnelError входит в RETRY_EXCEPTIONS. С настройками по умолчанию прокси в лаборатории получил отклонённый CONNECT три раза. В журнале были Retrying <GET https://127.0.0.1:8443/> (failed 1 times), затем (failed 2 times), затем Gave up retrying. С "dont_retry": True в meta прокси получил один запрос.
  • handle_httpstatus_list здесь ничего не меняет. Ответа нет, есть только исключение, поэтому обрабатывайте его в errback.

Ignoring response 407: HTTP status code is not handled or not allowed

code
Ignoring response <407 http://127.0.0.1:8080/>: HTTP status code is not handled or not allowed

Для URL http:// ответ 407 от прокси — обычный ответ, и HttpErrorMiddleware его отбрасывает. Callback не был вызван ни разу, единственным признаком была эта строка уровня INFO, а обход закрылся с finish_reason: finished. Если задан errback, строка в журнал не пишется, а errback получает scrapy.spidermiddlewares.httperror.HttpError: Ignoring non-200 response. Повторять запрос Scrapy не стал.

Чтобы получить ответ в callback, разрешите этот статус для запроса:

python
yield scrapy.Request(url, meta={"proxy": PROXY, "handle_httpstatus_list": [407]})

Атрибут паука handle_httpstatus_list = [407] и настройка HTTPERROR_ALLOWED_CODES = [407] дали тот же результат (HttpErrorMiddleware). Callback после этого получил статус 407 с Proxy-Authenticate: Basic realm="lab". Без них отброшенные ответы учитываются в статистике обхода под ключом httperror/response_ignored_status_count/407.

UnsupportedURLSchemeError: Unsupported scheme: b'socks5'

code
scrapy.exceptions.UnsupportedURLSchemeError: Unsupported scheme: b'socks5'

Стандартный обработчик загрузки Scrapy не поддерживает SOCKS. С meta["proxy"] = "socks5://127.0.0.1:1080" запрос к http:// вызвал эту ошибку один раз, без повторов. socks5h:// вызвал Unsupported scheme: b'socks5h', и URL SOCKS в HTTP_PROXY вызвал ту же ошибку.

Запрос к https:// её не вызвал. Scrapy отправил CONNECT 127.0.0.1:8443 обычным HTTP на порт SOCKS, SOCKS-сервер лаборатории закрыл соединение, и Scrapy трижды ждал истечения таймаута загрузки, прежде чем сообщил DownloadTimeoutError: Getting https://127.0.0.1:8443/ took longer than 15.0 seconds. В лаборатории DOWNLOAD_TIMEOUT был равен 15. По умолчанию он равен 180.

Выходов два:

  • Используйте HTTP-прокси вашего провайдера, если он есть. Тогда применимо всё сказанное выше.
  • Переключитесь на HttpxDownloadHandler. Он входит в Scrapy, а документация указывает для него поддержку SOCKS5 и помечает его как экспериментальный и пока не рекомендованный для продакшена.
sh
python -m pip install "scrapy[httpx]"
python
DOWNLOAD_HANDLERS = {
    "http": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
    "https": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
}

С этим обработчиком socks5:// вернул 200 для обеих схем. Для http://localhost:8080/ SOCKS-сервер записал доменное имя, то есть обработчик передаёт прокси имя хоста. socks5h:// тоже сработал. Статья socks5 и socks5h показывает, что делают с этими двумя схемами другие клиенты. Ошибки у этого обработчика выглядят иначе: отклонённый CONNECT проявился как scrapy.exceptions.DownloadConnectionRefusedError: 407 Proxy Authentication Required.

Повторы: RetryMiddleware повторяет запросы POST

RetryMiddleware повторил POST так же, как повторяет GET. В лаборатории POST прошёл через прокси к серверу, который прочитал запрос и закрыл соединение, не ответив. Scrapy записал Retrying <POST http://127.0.0.1:8081/jobs> (failed 1 times) и отправил запрос снова. Сервер получил одно и то же тело дважды, а callback увидел один ответ 201.

Для запроса, который небезопасно повторять, задайте "dont_retry": True в meta (RetryMiddleware). Тогда сервер получил один POST, а errback получил scrapy.exceptions.DownloadFailedError. Повторы через прокси: один потерянный ответ, две созданные задачи показывает, что меняет ключ идемпотентности.

Каждый повтор — это ещё один запрос через прокси. В статье Сколько ГБ прокси вам нужно измерено, сколько трафика уходит на загрузку одного только HTML. Для помощи с диагностикой внутри агента для написания кода Proxy Toolkit MCP принимает очищенные описания ошибок. Не передавайте в его аргументах учётные данные.

Как это проверялось

На Ubuntu 26.04.1 с Python 3.14.4, Scrapy 2.19.0, Twisted 26.4.0 и httpx2 2.13.1, всё на 127.0.0.1: python -m http.server в роли HTTP- и HTTPS-целей, три прокси, которые требуют Basic-авторизацию и записывают каждый запрос с именами его заголовков, SOCKS5-сервер, который записывает каждое место назначения, и цель, которая теряет ответ на первый POST. Каждый из 62 случаев запускал scrapy runspider в новом интерпретаторе с чистым окружением. Аккаунт прокси не использовался. На Python 3.15.0 pip install scrapy без компилятора C завершается ошибкой, потому что у brotli на 14 октября 2026 года не было колеса cp315; проверенные решения — в статье Python 3.15 для веб-скрапинга.

Архив лаборатории содержит скрипт запуска, случаи, прокси, SOCKS5-сервер, README и записанные результаты.

Не проверялось: macOS и Windows, авторизация прокси по Digest и NTLM, SOCKS-прокси с авторизацией, прокси, до которых нужно подключаться по TLS, HTTP/2 и обработчик загрузки Scrapy на основе aiohttp.

ipvolt — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.

Источники и дополнительное чтение

Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.