
配置思路:先把自动切换链路搭完整
Scrapy 实现自动切换代理 IP,最常见的入口就是下载中间件。通常由 process_request 负责分配代理,process_exception 和 process_response 负责识别异常请求并触发重新分配。
基础配置可以保留,但要先明确两件事。
第一,不要只依赖静态 PROXY_LIST。静态列表适合本地验证或短时任务,但如果网站采集器需要持续运行,代理池没有更新机制时,一旦连续碰到失效节点,访问稳定性会明显下降。
第二,自定义代理中间件和 Scrapy 自带重试机制要协调好。如果中间件和框架都在重复处理失败请求,就容易出现同一请求被多次复制、重试次数失控或日志难以排查的问题。
settings.py 可以按下面的方式整理:
DOWNLOADER_MIDDLEWARES = {
'your_project_name.middlewares.ProxyMiddleware': 543,
}
DOWNLOAD_TIMEOUT = 30
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
PROXY_LIST = [
"http://127.0.0.1:8001",
"http://127.0.0.1:8002",
]
这里真正重要的不是配置项本身,而是它们在链路中的职责:
DOWNLOAD_TIMEOUT决定请求多久未完成才进入超时判断RETRY_TIMES决定单个请求最多允许切换多少次代理RETRY_HTTP_CODES决定哪些响应状态需要重新分配代理PROXY_LIST只是初始代理来源,不应等同于完整的长期代理池方案
中间件实现:不要只随机分配,还要管理失效代理
很多示例代码的问题在于,process_request 只是随机选一个代理,process_exception 再随机换一个。这样只能说明“代理会变”,但不能说明“失效代理会退出轮换”。如果一个代理已经出现连接失败,它仍可能在下一次请求里继续被选中。
更稳妥的做法,是维护一个可用代理池和一个临时失效集合。请求只从可用池中分配,失败后先移出轮换,再由独立的验证或恢复逻辑决定是否重新加入。
import random
from scrapy.exceptions import IgnoreRequest, NotConfigured
class ProxyMiddleware:
def __init__(self, proxy_list):
if not proxy_list:
raise NotConfigured("代理列表为空,请检查配置")
self.all_proxies = set(proxy_list)
self.active_proxies = set(proxy_list)
self.dead_proxies = set()
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist("PROXY_LIST")
return cls(proxy_list)
def process_request(self, request, spider):
if not self.active_proxies:
raise IgnoreRequest("当前无可用代理")
proxy = random.choice(list(self.active_proxies))
request.meta["proxy"] = proxy
request.meta["proxy_retry_times"] = request.meta.get("proxy_retry_times", 0)
def process_response(self, request, response, spider):
if response.status in [403, 429, 500, 502, 503, 504]:
self._mark_dead(request.meta.get("proxy"))
return self._retry_with_new_proxy(request, spider)
return response
def process_exception(self, request, exception, spider):
self._mark_dead(request.meta.get("proxy"))
return self._retry_with_new_proxy(request, spider)
def _mark_dead(self, proxy):
if proxy and proxy in self.active_proxies:
self.active_proxies.remove(proxy)
self.dead_proxies.add(proxy)
def _retry_with_new_proxy(self, request, spider):
retry_times = request.meta.get("proxy_retry_times", 0) + 1
if retry_times > 3:
raise IgnoreRequest("代理重试次数超限")
if not self.active_proxies:
raise IgnoreRequest("无可用代理可切换")
new_request = request.copy()
new_request.dont_filter = True
new_request.meta["proxy_retry_times"] = retry_times
new_request.meta["proxy"] = random.choice(list(self.active_proxies))
return new_request
这段实现比“单纯随机切换”更适合长期运行,主要体现在几个方面:
- 失败代理不会立刻再次参与分配
- 响应异常和连接异常都能触发重新分配
- 单个请求有独立的代理重试计数,避免无限循环
dont_filter=True可以避免 Scrapy 把重试请求当作重复请求直接丢弃
重试、去重和状态判断要配合起来看
Scrapy 自动切换代理 IP 的稳定性,往往不是由“能不能切换”决定,而是由重试链路是否清晰决定。尤其在网站采集器持续运行时,重试、去重和异常识别必须协同工作。
重试次数不要混在多个地方重复计算
如果你已经在中间件里维护了 proxy_retry_times,就要避免再让其他逻辑对同一类失败重复累计。否则你看到的可能是“设置只允许重试 3 次”,实际运行时却远不止 3 次。
去重机制要给代理重试留出口
Scrapy 的去重默认是基于请求特征判断是否重复。如果代理切换后仍然访问同一个 URL,而你没有打开 dont_filter=True,重试请求可能还没真正发出就被过滤掉。这种情况下,表面上像是“代理切换失败”,实际问题在去重逻辑。
状态码不要一刀切处理
403、429、500、超时、TLS 握手失败、DNS 解析异常,这些现象都可能导致请求失败,但含义并不完全一样。把所有失败都直接视为代理彻底失效,会让代理池被过度清理,最后可用资源越来越少。
更稳妥的方式是把失败拆成三类来处理:
- 可直接剔除的硬失败,如明显连接异常
- 先冷却再观察的波动失败,如短时超时或部分状态码
- 不一定属于代理问题的失败,如目标站点响应异常
动态代理池怎么维护,决定网站采集器能不能长期运行
自动切换代理 IP 真正的难点,不在切换动作本身,而在代理池是不是能持续可用。如果只是项目启动时加载一批代理,跑一段时间之后,池子质量通常会自然波动。
常见维护方式大致可以分为三类:
| 方式 | 适用阶段 | 局限 |
|---|---|---|
| 静态代理列表 | 本地测试、短时任务 | 失效后无法自动补充 |
| 启动前批量验证 | 中小规模任务 | 运行过程中质量仍会变化 |
| 动态更新代理池 | 持续运行的网站采集器 | 需要独立维护更新逻辑 |
如果是网站采集器、舆情监测或广告监测这类持续性任务,建议至少做到两点。
第一,代理验证不要阻塞主采集链路。很多项目会在中间件里直接同步验证代理,这会拖慢请求调度,也会让异常判断变得混乱。更好的做法,是把验证放到爬虫启动前、定时任务或独立代理管理模块里执行。
第二,更新和使用要分层。中间件只负责“拿可用代理”和“标记异常代理”,而代理获取、校验、恢复、替换等动作交给独立模块。这样不仅结构更清晰,也更方便后续接入 API 代理源或定时刷新策略。
上线后容易忽略的几个细节
自动切换代理 IP 能跑起来,不代表就能稳定跑。很多问题并不出在代码写法,而是出在运行细节。
请求环境一致性不能忽略
如果同一批采集任务里,请求头、超时设置、重试策略和代理切换规则不一致,返回结果就容易波动。看起来像代理不稳定,实际上是请求环境本身不一致。
例如:
- 有的请求走代理,有的请求没有挂代理
- 有的请求超时设置是 10 秒,有的是 60 秒
- 有的请求失败后会重试,有的直接丢弃
这种情况下,即使代理资源本身没有问题,也很难准确定位到底是目标站点波动、请求参数不一致,还是中间件逻辑导致的差异。
不是所有失败都该马上剔除代理
单次超时、偶发状态码异常,不一定就代表代理彻底不可用。如果缺少分类记录,系统很容易把波动节点直接移出池子,导致可用代理越来越少,后续切换空间反而变小。
比较实用的思路是给代理增加简单的失败计分或冷却时间,而不是只要失败一次就永久移除。
日志必须能看出完整切换链路
如果日志里没有请求 URL、使用代理、重试次数、异常类型和代理状态变化,你很难排查到底是哪个环节出了问题。对网站采集器这类长期任务来说,可观测性和切换逻辑本身同样重要。
面向长期运行时,代理IP支持能力应该怎么评估
当 Scrapy 项目从功能验证进入长期运行阶段,关注点就不再只是“代码能不能切换代理”,而是代理 IP 资源是否能支撑持续调用,请求环境是否稳定,更新机制是否跟得上业务节奏。
对于网站采集器这类任务,常见难点不是某一次请求能否成功,而是连续运行时代理池质量是否波动、切换后访问环境是否还能保持一致、工程接入是否足够顺畅。这个阶段,更值得关注的是代理服务在持续运行场景下的支持能力。
Scrapy 长期调度场景下的接入支持
如果你的目标是把 Scrapy 自动切换代理 IP 用在长期运行的网站采集器里,那么后续评估重点通常会落在三个方面:资源调度是否稳定、请求环境一致性是否可控、工程化调用是否方便接入现有链路。
在这类需求下,青果网络可以作为长期接入方案之一纳入评估。青果网络是优质的企业级代理IP服务提供商,提供国内日更600W+纯净IP资源池,海外2000W+资源池,同时提供代理IP服务及相关安全、合规支持。对于需要持续运行、需要更新代理池并保持调用稳定性的网站采集器来说,这类支持更贴近实际落地要求。
如果你更关注的是 Scrapy 在长期任务中的连续性表现,那么除了中间件代码本身,还要看代理资源在持续调用下是否有利于减少无效切换、降低重复重试成本。青果网络的代理IP业务成功率比行业平均水平高出30%,更适合放在网站采集器持续运行、请求环境一致性和工程化调度这些语境里理解,而不是只看单次请求是否可用。
总结
Scrapy 自动切换代理 IP 的核心,不是随机给请求换一个代理,而是把代理分配、异常识别、重试控制、失效剔除和池子更新做成一条完整链路。对短期任务来说,静态列表足够起步;对网站采集器这类持续运行场景,更关键的是访问稳定性、请求环境一致性和动态维护能力。实际落地时,也可以把青果网络这类更适合长期调度与工程化调用的代理IP支持能力纳入评估。
常见问题解答
Q1:Scrapy 自动切换代理 IP 一定要用下载中间件吗?
A1:大多数情况下是的,下载中间件最适合统一处理代理分配、异常识别和重试链路,维护也更集中。
Q2:为什么已经做了随机切换,网站采集器还是不稳定?
A2:常见原因不是不会切换,而是失效代理没有及时移出轮换、代理池没有更新,或者请求环境本身不一致。
Q3:代理验证适合直接写在中间件里吗?
A3:测试阶段可以临时这样做,但长期运行不建议放在主链路里,最好拆到独立模块或定时任务中。