# 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行）可以让你了解调试所需的一切：

- 启动时，它调用`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`配置。
- 中间件在编码前会对凭证进行解码，因此在密码中使用`@`等特殊字符时需要进行URL编码——它们会正确传递。
- 如果重试更改了代理，中间件会丢弃过时的认证头，而不是将其泄露给新代理——这是现代Scrapy中的凭证卫生修复，也是一个不使用旧版本的好理由。

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

## 每个请求的代理与meta

最轻量的集成是直接在爬虫中设置`meta`——当只有某些请求需要代理，或不同目标需要不同出口国家时很有用：

```python
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@residentialpremium.quantumproxies.io:8080"},
        )
```

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

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

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

```python
# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

    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 请求经过下载器中间件链：spider 产出请求，优先级为 350 的自定义中间件把 meta'proxy' 设为网关，优先级为 750 的内置 HttpProxyMiddleware 把凭据转换为 Proxy-Authorization 请求头，下载器经由轮换网关发出请求；重试时会获得新 IP。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag1.png)
*你的中间件只需设置meta['proxy']；内置的在优先级750处理凭证，重试在新IP上重新进入链。*

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

如果你确实运行自己的代理列表——静态ISP地址或混合池——经典模式（由scrapy-proxies包推广，推荐`RETRY_TIMES = 10`，因为免费列表代理失败率很高）是：每个请求随机选择，收到封禁信号时驱逐，重新排队请求：

```python
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封禁避免清单](https://quantumproxies.io/blog/avoid-ip-ban-web-scraping)中。

## 决定结果的Scrapy设置

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

```python
# 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中？我们的[速率限制指南](https://quantumproxies.io/blog/fix-429-too-many-requests-scraping)深入探讨并发预算。

![两种 Scrapy 轮换配置并列：一种是自建代理列表，随着被封 IP 被剔除、请求重新排队而越来越短；另一种是轮换网关，只有一个端点，在服务器端完成轮换。卡片说明：列表意味着要做健康检查和剔除，20 个静态 IP 可能几分钟就耗光，而网关会让每次重试都使用新 IP。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag2.png)
*DIY列表意味着健康检查、驱逐逻辑和过时IP。网关是一个在服务器端轮换的端点——重试默认落在新IP上。*

## 超越状态码的封禁检测

复杂的目标不会发送诚实的403。他们会提供包含CAPTCHA页面、空产品网格或剥离模板的200。健壮的爬虫验证内容，而不仅仅是状态——检查真实页面上唯一存在的元素，缺失时重新排队。如果页面即使通过良好代理返回结构上为空，内容可能是在客户端渲染的；请参阅[为什么爬虫会得到空页面](https://quantumproxies.io/blog/scraper-returns-empty-page-javascript)。当一个域无论IP如何都能击败纯HTTP时，将该域交给一个[Scraper API](https://quantumproxies.io/scraper-api)，它渲染JavaScript并返回干净的HTML或markdown——Scrapy像处理任何其他响应一样处理它，你保持你的管道。

```python
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请求的成本](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost)——大多数Scrapy项目需要更好的IP，而不是浏览器。

[将旋转代理接入你的Scrapy项目](https://quantumproxies.io/rotating-proxies)
