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) расскажет вам все, что нужно для отладки:

Практическое следствие: вам почти никогда не нужен сторонний пакет прокси. Все, что помещает действительный 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.

Диаграмма потока цепочки proxy middleware Scrapy: запрос паука, пользовательское middleware устанавливает мета прокси, HttpProxyMiddleware добавляет аутентификацию с приоритетом 750, загрузчик выходит через вращающийся шлюз
Вашему middleware нужно только установить meta['proxy']; встроенное на приоритете 750 обрабатывает учетные данные, и повторы снова входят в цепочку на новом 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
Сравнение пользовательского middleware для списка прокси Scrapy и вращающегося резидентного шлюза для ротации прокси
Список своими руками означает проверки состояния, логику исключения и устаревшие IP. Шлюз — это одна конечная точка, которая вращается на стороне сервера — повторы по умолчанию попадают на свежие IP.

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

Сложные цели не отправляют честные 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, а не браузер.

Подключите вращающиеся прокси к вашему проекту Scrapy