Укажите учётные данные в URL прокси и передайте его в meta["proxy"]:
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 прокси вместе с учётными данными:
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)DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.RotateProxyMiddleware": 740}В лаборатории запрос с редиректом прошёл через этот middleware дважды, попал на два прокси и авторизовался на обоих.
Не работает вариант, в котором URL задан без учётных данных, а заголовок выставляется вручную:
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в нижнем регистре применялись к URLhttp://, аHTTPS_PROXY— к URLhttps://. Когда была задана только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для URLhttp://иTunnelErrorс кодом 407 дляhttps://.
Чтобы игнорировать окружение и сохранить работу meta["proxy"], замените middleware подклассом, который отбрасывает прочитанное:
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
class ExplicitProxyMiddleware(HttpProxyMiddleware):
def __init__(self, auth_encoding="latin-1"):
super().__init__(auth_encoding)
self.proxies = {}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
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
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, разрешите этот статус для запроса:
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'
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 и помечает его как экспериментальный и пока не рекомендованный для продакшена.
python -m pip install "scrapy[httpx]"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 — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- Scrapy documentation: HttpProxyMiddleware and HTTPPROXY_ENABLED
- Scrapy documentation: Request.meta special keys (proxy, dont_retry, handle_httpstatus_list)
- Scrapy documentation: HttpErrorMiddleware and HTTPERROR_ALLOWED_CODES
- Scrapy documentation: RetryMiddleware
- Scrapy documentation: Spider.start()
- Scrapy documentation: HttpxDownloadHandler
- Scrapy release notes: 2.16.0, start_requests() removed
- Scrapy release notes: 2.6.2, Proxy-Authorization security fix
- Scrapy release notes: 2.7.1, explicit Proxy-Authorization header