# Scrapy 用于轮换住宅代理的代理中间件

> Scrapy 轮换代理中间件：每请求轮换并保持会话的住宅 IP，支持 AutoThrottle 和重试，集成 scrapy-playwright。代码经过语法检查。

- URL: https://crawlrelay.com/zh/docs/scrapy/
- 最后更新: 2026-10-13
- Publisher: CrawlRelay (https://crawlrelay.com)

一个 30 行的下载器中间件，能按请求选择国家和会话，还有让爬虫礼貌运行的设置：robots.txt，按域并发，AutoThrottle 和重试。

**快速回答 — Scrapy 如何使用轮换代理？**

写个小的下载中间件，在 Scrapy 的 `HttpProxyMiddleware` 前设置 `request.meta["proxy"]` 到 `http://USERNAME:PASSWORD@gw.crawlrelay.com:7777`；Scrapy 会帮你发送认证。用 **CrawlRelay** 住宅代理时，国家和固定会话写在用户名里，一个中间件搞定。流量费US$0.50/GB起，支持加密货币支付，无需 KYC。

## Scrapy 配置速览

| 属性 | 值 |
| --- | --- |
| 代理位置 | 中间件设置的 `request.meta["proxy"]` |
| 中间件顺序 | 你的中间件优先级低于内置的 `HttpProxyMiddleware`（750） |
| 认证凭据 | 在代理 URL 里；Scrapy 转成 `Proxy-Authorization` 头部 |
| SOCKS5 | 默认 HTTP/1.1 处理器不支持；Scrapy 2.17+ 通过可选的 HttpxDownloadHandler 支持 SOCKS，HTTP 端7777最简单 |
| 轮换 | 每新连接用新的出口；正常请求里唯一会话 ID 会带来不同 IP |
| 礼貌设置 | `ROBOTSTXT_OBEY`、`CONCURRENT_REQUESTS_PER_DOMAIN`、`AUTOTHROTTLE_ENABLED` |
| 重试 | 默认 `RETRY_HTTP_CODES` 会处理 429 和 5xx；默认不读 Retry-After |

## 如何编写代理中间件？

把类放在 `middlewares.py`。它会从设置和三个可选 `meta` 键（`proxy_country`、`proxy_session`、`proxy_ttl`）构造用户名。无 `proxy_session` 的请求会轮换；带 `proxy_session="new"` 的请求会用新会话通常有新出口 IP；固定会话 ID 的请求共享出口。中间件每次执行（包括重试）都会重新计算代理，不会修改其他代码设置的代理。缺少 `CRAWLRELAY_USER` 时，爬虫启动就停止，不会从你自己的 IP 运行。

*middlewares.py*

```python
import uuid
from urllib.parse import quote


class CrawlRelayProxyMiddleware:
    """Sets request.meta["proxy"]; Scrapy's HttpProxyMiddleware then sends the credentials."""

    def __init__(self, user, password, country):
        self.user, self.password, self.country = user, password, country

    @classmethod
    def from_crawler(cls, crawler):
        s = crawler.settings
        if not s.get("CRAWLRELAY_USER"):
            # Fail loudly: NotConfigured would only disable the middleware and crawl from your own IP
            raise ValueError("CRAWLRELAY_USER is not set: refusing to crawl without the proxy")
        return cls(s.get("CRAWLRELAY_USER"), s.get("CRAWLRELAY_PASSWORD", ""), s.get("CRAWLRELAY_COUNTRY", "us"))

    def process_request(self, request, spider=None):
        if "proxy" in request.meta and not request.meta.get("_crawlrelay"):
            return None  # a proxy set by other code: leave it alone
        parts = [self.user, "country", request.meta.get("proxy_country", self.country)]
        session = request.meta.get("proxy_session")
        if session == "new":  # a fresh session for this request (and for each of its retries)
            session = uuid.uuid4().hex[:12]
        if session:
            parts += ["session", session, "ttl", str(request.meta.get("proxy_ttl", 10))]
        username = "-".join(parts)
        request.meta["proxy"] = "http://%s:%s@gw.crawlrelay.com:7777" % (
            quote(username, safe=""), quote(self.password, safe=""))
        request.meta["_crawlrelay"] = True
        return None
```

## 哪些 Scrapy 设置保持代理爬虫礼貌且高速？

代理帮你把请求分散到多个 IP，但目标还是看到你的总速率。这些设置能让每个域名请求保持稳定，不响应变慢时 AutoThrottle 会自动降速。保留 `ROBOTSTXT_OBEY = True`，新 Scrapy 项目默认设置了。

*settings.py（节选）*

```python
import os

BOT_NAME = "myproject"
ROBOTSTXT_OBEY = True
USER_AGENT = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"

CRAWLRELAY_USER = os.environ.get("CRAWLRELAY_USER")  # e.g. the username from your dashboard
CRAWLRELAY_PASSWORD = os.environ.get("CRAWLRELAY_PASSWORD")
CRAWLRELAY_COUNTRY = "us"

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.CrawlRelayProxyMiddleware": 350,  # runs before HttpProxyMiddleware (750)
}

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 4
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 30.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0

RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 522, 524, 408]
DOWNLOAD_TIMEOUT = 45
```

## 我如何保持分页使用同一个IP，同时为商品页进行IP轮换？

在依赖上一页的流程请求中传递固定的`proxy_session`，而独立页面则不传递。下面的爬虫运行于books.toscrape.com，这是一个专为爬取练习发布的沙箱网站。

*spiders/catalog.py*

```python
import scrapy


class CatalogSpider(scrapy.Spider):
    name = "catalog"
    start_urls = ["https://books.toscrape.com/"]

    def parse(self, response):
        for href in response.css("article.product_pod h3 a::attr(href)").getall():
            # Product pages are independent: rotating exits
            yield response.follow(href, callback=self.parse_item)
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            # The listing flow keeps one sticky exit for 30 minutes
            yield response.follow(next_page, callback=self.parse,
                                  meta={"proxy_session": "listing1", "proxy_ttl": 30})

    def parse_item(self, response):
        yield {
            "title": response.css("div.product_main h1::text").get(),
            "price": response.css("p.price_color::text").get(),
            "url": response.url,
        }
```

## Scrapy 会重用连接吗？这会停止IP轮换吗？

会的，Scrapy 会保持连接活跃并在每个代理和目标主机间重用它们，因此连续请求会共享同一个出口。对于大多数爬取来说没问题：Scrapy每个站点最多开启`CONCURRENT_REQUESTS_PER_DOMAIN`条连接，每个新连接通常会分配不同的住宅代理IP。

当请求必须通过新IP发出，比如因失败出口导致的[502](https://crawlrelay.com/zh/docs/errors/502-bad-gateway/)重试时，使用`response.request.replace(meta={**response.meta, "proxy_session": "new"}, dont_filter=True)`重新发起请求。中间件会生成新的会话用户名，不同用户名意味着不同的连接池，Scrapy会打开新连接，网关通常会分配不同的出口。

## 如何在Scrapy中尊重429响应的Retry-After？

Scrapy内置的`RetryMiddleware`会重试429但不会等待`Retry-After`的延时。AutoThrottle 会随着时间自动减慢该域名的请求速度，通常已足够。对于发送较长`Retry-After`值的API，需要增加一个小中间件读取此头部并安排延时重试，或降低该域名的`CONCURRENT_REQUESTS_PER_DOMAIN`。429页](https://crawlrelay.com/zh/docs/errors/429-too-many-requests/)解释了该头部的含义。

## 如何在scrapy-playwright中使用代理？

对于需要JavaScript的页面，scrapy-playwright通过设置传递Playwright的启动参数。凭证需作为独立字段提供：Chromium会忽略服务器URL内写入的凭证。如果渲染跨页的未登录流程，建议每个固定会话用一个context。

*scrapy-playwright的settings.py*

```python
import os

DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

PLAYWRIGHT_LAUNCH_OPTIONS = {
    "proxy": {
        "server": "http://gw.crawlrelay.com:7777",
        "username": os.environ.get("CRAWLRELAY_USER", "USERNAME") + "-country-us",
        "password": os.environ.get("CRAWLRELAY_PASSWORD", "PASSWORD"),
    },
}
```

## Scrapy与代理：常见问题

### 我需要scrapy-rotating-proxies或代理列表吗？

不需要。这些包轮换单个代理地址列表并禁用失败代理。住宅网关则在单一地址后面完成轮换：每个新连接通常分配不同住宅代理IP。只需一个设置`request.meta["proxy"]`的中间件即可满足需求。

### 为什么Scrapy显示我的代理出现TunnelError？

`TunnelError: Could not open CONNECT tunnel`表示代理拒绝或未能完成对HTTPS站点的CONNECT请求。消息中的状态码表明原因：407是凭证问题，502或504是出口或目标问题。参考407](https://crawlrelay.com/zh/docs/errors/407-proxy-authentication-required/)和502](https://crawlrelay.com/zh/docs/errors/502-bad-gateway/)相关页。

### 每个域名建议使用多少并发请求？

从每域2到4个请求开始，让AutoThrottle自动调整。仅当429、403和5xx响应比例保持平稳时再提升，并观察网站响应时间。大型网站承受更多请求，小型网站可能需要请求间隔及单连接。

### 应该关闭ROBOTSTXT_OBEY以获取更多数据吗？

不应该。robots.txt声明了网站允许自动客户端访问的路径，RFC 9309于2022年标准化了该格式。忽视robots.txt会增加法律和道德风险，且违反我们接受使用政策](https://crawlrelay.com/zh/acceptable-use/)，当用于访问网站已排除的内容时。

### 一个Scrapy爬取会消耗多少流量？

Scrapy仅下载HTML，页面通常压缩后几十KB。查看运行结束时Scrapy打印的统计数据中的`downloader/response_bytes`值，将其除以响应数。带宽指南](https://crawlrelay.com/zh/guides/bandwidth-calculator/)可帮助你计算费用。

**将住宅代理IP接入你的Scrapy项目** 测试使用US$5.94加10 GB，生产使用US$0.50/GB来自50 GB。 [查看价格](https://crawlrelay.com/zh/pricing/) · [并发指南](https://crawlrelay.com/zh/docs/concurrency/)

---

CrawlRelay: CrawlRelay 是为开发者设计的住宅代理和 4G/5G 移动代理轮换服务，适用于爬虫和 AI 数据管道：按需付费且流量永不过期，最低流量从 US$0.50/GB 起，支持加密货币支付，无需上传身份证明。无需 KYC —— 无需身份证、自拍和任何证件。支持加密货币支付。
