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 непосредственно в пауке — полезно, когда только некоторые запросы нуждаются в прокси или разные цели требуют разных стран выхода:
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@gate.quantumproxies.io:PORT"},
)
Мета на каждый запрос особенно полезна, когда выбор прокси сам по себе зависит от данных: направляйте немецкие страницы продуктов через имя пользователя, ориентированное на Германию, отправляйте загрузки изображений через дешевые выходы из дата-центров, а HTML — через резидентные, или повышайте упорный URL до более устойчивой сессии при второй попытке. Поскольку мета сохраняется при повторных запросах и перенаправлениях, решение, которое вы принимаете при создании запроса, следует за ним на протяжении всего цикла загрузки. Установка мета на тысячи запросов вручную не масштабируется, однако — для этого и нужны downloader middlewares.
Самая простая производственная настройка: вращающийся шлюз
С вращающимися прокси за одним шлюзовым конечным узлом ротация происходит на стороне сервера: каждый запрос через один и тот же URL выходит с другого IP из пула более 90 миллионов резидентных IP, охватывающего более 200 стран. Ваше middleware сокращается до трех строк, и нет списка для проверки состояния, удаления или обновления:
# middlewares.py
class RotatingGatewayMiddleware:
PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"
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.

Пользовательское middleware для ротации прокси с обработкой блокировок
Если вы действительно используете свой собственный список прокси — статические адреса ISP или смешанный пул — классический шаблон (популяризированный пакетом scrapy-proxies, который рекомендовал RETRY_TIMES = 10, потому что бесплатные списки прокси часто не работают) таков: выбирайте случайным образом для каждого запроса, исключайте при сигналах блокировки, повторно ставьте запрос в очередь:
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.
Настройки Scrapy, которые определяют результат
Middleware размещает прокси; настройки определяют, как ведет себя обход. Эти наиболее важны:
# 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? Наш гид по ограничению скорости охватывает бюджеты параллельности в деталях.

Обнаружение блокировок за пределами кодов состояния
Сложные цели не отправляют честные 403. Они выдают 200, содержащие страницу CAPTCHA, пустую сетку продуктов или очищенный шаблон. Надежные пауки проверяют контент, а не только статус — ищите элемент, который существует только на реальной странице, и повторно ставьте в очередь, когда он отсутствует. Если страницы возвращаются структурно пустыми даже через хорошие прокси, контент, вероятно, рендерится на стороне клиента; см. почему скреперы получают пустые страницы. И когда один домен побеждает простой HTTP независимо от IP, передайте этот домен Scraper API, который рендерит JavaScript и возвращает чистый HTML или markdown — Scrapy потребляет его, как любой другой ответ, и вы сохраняете свой конвейер.
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-запросами перед тем, как использовать один — большинству проектов Scrapy нужны лучшие IP, а не браузер.