Scrapy代理中间件:旋转代理,避免封禁

Scrapy自0.8版本起就支持代理,但文档从未告诉你如何在大规模爬取中进行代理轮换、重试和避免封禁。这里是中间件的结构、代码和决定爬取是否完成的设置。

Scrapy自0.8版本起就提供了代理中间件,因此“Scrapy是否支持代理”这个问题在十五年前就已解决。文档从未完整呈现生产环境的情况:内置的Scrapy代理中间件如何实际处理凭证,何时为每个请求设置代理而非全局设置,以及当目标开始在爬取中途封禁出口时如何轮换和恢复。此指南涵盖整个链条——内置的HttpProxyMiddleware、每个请求的meta、带有封禁处理的自定义轮换中间件,以及决定10万页爬取是完成还是凌晨3点中断的设置。

内置Scrapy代理中间件的工作原理

scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware默认在DOWNLOADER_MIDDLEWARES_BASE中以优先级750启用。阅读其源码(在Scrapy 2.17中约100行)可以让你了解调试所需的一切:

实际结果:你几乎不需要第三方代理包。任何在优先级750之前将有效代理URL放入request.meta['proxy']的操作都能免费获得认证和头处理。

每个请求的代理与meta

最轻量的集成是直接在爬虫中设置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"},
        )

当代理选择本身是数据驱动时,每个请求的meta就显得尤为出色:通过德国定向用户名路由德国产品页面,通过廉价数据中心出口发送图像下载,而HTML则通过住宅出口,或者在第二次尝试时将顽固的URL升级到更持久的会话。因为meta在重试和重定向时仍然有效,你在构建请求时做出的决定会贯穿整个下载周期。手动为数千个请求设置meta并不具备可扩展性——这就是下载中间件的用途。

最简单的生产设置:旋转网关

在一个旋转代理后面使用单一网关端点,轮换在服务器端进行:每个通过相同URL的请求从90M+住宅池中200+国家的不同IP出口。你的中间件缩减到三行,无需健康检查、修剪或刷新列表:

# 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很重要:你的中间件必须在内置的750之前运行,以便HttpProxyMiddleware仍然可以将凭证转换为认证头。这种配对——网关轮换加上Scrapy的标准重试机制——是每行代码最高的可靠性,因为每次重试都会自动通过新的出口IP。

Scrapy代理中间件链的流程图:爬虫请求,自定义中间件设置meta代理,HttpProxyMiddleware在优先级750添加认证,下载器通过旋转网关出口
你的中间件只需设置meta['proxy'];内置的在优先级750处理凭证,重试在新IP上重新进入链。

带有封禁处理的自定义旋转代理中间件

如果你确实运行自己的代理列表——静态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

注意这个中间件必须做的一切,而网关可以免费完成:跟踪池健康,驱逐被封禁的IP,重新排队请求。在攻击目标上,20个静态IP的池可以在几分钟内消失。更广泛的反封禁策略(节奏、头、会话纪律)在我们的IP封禁避免清单中。

决定结果的Scrapy设置

中间件放置代理;设置决定爬取行为。这些最为重要:

# 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
DIY Scrapy代理列表中间件与旋转住宅网关的代理轮换比较
DIY列表意味着健康检查、驱逐逻辑和过时IP。网关是一个在服务器端轮换的端点——重试默认落在新IP上。

超越状态码的封禁检测

复杂的目标不会发送诚实的403。他们会提供包含CAPTCHA页面、空产品网格或剥离模板的200。健壮的爬虫验证内容,而不仅仅是状态——检查真实页面上唯一存在的元素,缺失时重新排队。如果页面即使通过良好代理返回结构上为空,内容可能是在客户端渲染的;请参阅为什么爬虫会得到空页面。当一个域无论IP如何都能击败纯HTTP时,将该域交给一个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中为单个请求设置代理?

在请求的meta中传递它:scrapy.Request(url, meta={'proxy': 'http://user:pass@host:port'})。Meta总是覆盖环境级别的代理,并将值设置为None强制该请求直接连接——在一个爬虫中混合代理和非代理流量时很有用。

如何在Scrapy中轮换代理?

要么编写一个下载器中间件,从列表中为每个请求选择一个代理并驱逐被封禁的代理,要么将每个请求指向一个旋转网关端点,该端点在服务器端分配一个新的出口IP。网关方法只需三行中间件,无需健康检查,并将每个Scrapy重试自动转化为轮换。

为什么我的Scrapy代理返回407?

代理拒绝了认证。检查凭证是否在meta中的代理URL内,特殊字符是否经过URL编码,以及如果你的计划使用IP认证,服务器IP是否被列入白名单。如果凭证包含非ASCII字符,请设置HTTPPROXY_AUTH_ENCODING以匹配你的提供商。

要记住的模式:尽早设置meta['proxy'],让内置中间件在750进行认证,添加429和403到你的重试代码,并优先选择服务器端轮换而不是列表管理。如果你的爬取部分需要JavaScript,在使用之前权衡无头浏览器与HTTP请求的成本——大多数Scrapy项目需要更好的IP,而不是浏览器。

将旋转代理接入你的Scrapy项目