用于网页抓取的轮换代理:Python requests、Scrapy 中间件与排名跟踪
一份在 Python 和 Scrapy 中轮换代理的实用指南——代理池选择、粘性会话与按请求轮换的对比、可用的中间件代码、封禁检测,以及为什么排名跟踪器更需要位置一致性,而不是原始 IP 数量。
如果你从单个 IP 抓取几百个页面,最终总会遇到速率限制、CAPTCHA 或软封禁。轮换是标准解决方案——但糟糕的轮换比不轮换更糟:它会消耗代理预算、让调试更难,还可能比一个稳定、礼貌的爬虫更像机器人。
本指南涵盖轮换的工作原理、如何在 Python 中用 requests 实现,以及如何在 Scrapy 中通过下载器中间件实现,还有这些相同技术如何应用于 SERP 排名跟踪。
在轮换之前,检查轮换是否是问题所在
轮换会隐藏 IP 层面的封禁。它不能修复:
- 模式问题。 再多的 IP 也救不了一个用默认 user agent 每秒请求同一 URL 五十次的爬虫。
- 会话问题。 如果你的流程需要保持登录状态,会话中途更换 IP 看起来就像账号被盗。
- 地理问题。 如果你需要的内容有地区限制,你需要的是正确的国家/地区,而不是更多 IP。
先诊断:记录每个 IP 的状态码、响应正文和响应时间。如果每个 IP 都得到 200,而某个特定页面返回封禁,那么你的问题在于请求行为,而不是 IP 信誉。
选择适合任务的代理类型
| 类型 | 典型优势 | 注意事项 |
|---|---|---|
| 数据中心 | 快速、高并发、广泛可用 | 容易被识别;会被严格站点封禁 |
| 住宅 | 与真实用户流量融合 | 较慢;有带宽限制;会话处理很重要 |
| 移动 | 信任度高、最难被封禁 | 昂贵;不稳定;并发会话较少 |
| ISP / 静态住宅 | IP 稳定、一致 | 池子较小;轮换较少 |
对于宽松站点上的结构化数据,数据中心代理通常就足够了。对于搜索结果、市场和旅游网站,通常需要住宅或移动代理。评估供应商时应看轮换粒度、会话控制、地理覆盖范围以及如何处理封禁——而不是看宣传的 IP 数量。
轮换策略
按请求轮换
每个请求都从代理池选择的 IP 发出。许多供应商将其实现为一个轮换网关主机名加端口,或者用户名中的会话令牌。最适合无状态的页面抓取。
粘性会话
在定义的时间窗口内或直到你释放之前,复用同一个 IP。用于多步流程:搜索、打开列表、读取详情页。在大多数商业代理池中,粘性由代理用户名中的会话标识符控制,新的标识符会产生新的 IP。
地理粘性
对于排名跟踪和价格检查,你通常希望同一组查询使用相同的城市或国家,因为结果会因位置而异。应在地理区域内轮换,而不是跨地理区域轮换。
并发限流
轮换和速率限制是互补的,而不是替代关系。一个只有五个并发请求、但遵守 Retry-After 的爬虫,会胜过一个用两百个线程把每个 IP 打到被封禁的爬虫。
Python:使用 requests 轮换代理
如果需要 SOCKS 支持,请安装它。requests 库底层使用 PySocks:
pip install "requests[socks]"
然后在代理池中轮换:
import itertools
import requests
POOL = [
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
'socks5h://user:[email protected]:1080',
]
proxy_cycle = itertools.cycle(POOL)
def fetch(url, timeout=20):
proxy = next(proxy_cycle)
proxies = {'http': proxy, 'https': proxy}
response = requests.get(url, proxies=proxies, timeout=timeout)
response.raise_for_status()
return response.text
在生产环境中,有几点值得补充:
- 在
429或403时用不同的代理重试,而不是同一个。 - 使用
requests.Session()复用连接,但每个 IP 创建一个会话,而不是整个抓取过程共用一个会话。 - 记录哪个代理产生了哪个结果,这样你可以丢弃坏 IP,而不是重新抽到它们。
- 当你希望主机名由代理而不是本地解析时,使用
socks5h://(注意那个h)。
Scrapy:轮换代理下载器中间件
Scrapy 已经通过内置的 HTTP 代理中间件处理代理认证,所以你只需在请求发送前设置 request.meta['proxy']。一个简单的轮换中间件如下:
import random
class RotatingProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
proxies = crawler.settings.getlist('ROTATING_PROXIES')
if not proxies:
raise ValueError('ROTATING_PROXIES is empty')
return cls(proxies)
def process_request(self, request, spider):
request.meta['proxy'] = random.choice(self.proxies)
在你的 settings 模块中注册它:
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RotatingProxyMiddleware': 350,
}
ROTATING_PROXIES = [
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
]
AUTOTHROTTLE_ENABLED = True
RETRY_HTTP_CODES = [403, 429, 500, 502, 503, 504]
两点说明:
- 简单的
random.choice会不断复用已被封禁的 IP。像scrapy-rotating-proxies这样的社区项目在同一思路上增加了封禁检测和按代理统计,当请求规模超过少量请求后,这个依赖是值得的。 - Scrapy 的重试中间件通常会使用同一个代理重试。如果封禁是你的问题,请让重试路径重新执行代理选择。
将相同方法应用于排名跟踪
排名跟踪器有一个通用爬虫没有的代理要求:位置一致性。一个关键词先从一个城市检查,再从另一个城市检查,会产生不同的 SERP 和无意义的排名变动图。
排名跟踪的实用规则:
- 为每个跟踪的关键词选择一个位置,并在多次运行中保持稳定。
- 定期刷新 IP 以免被标记,但保持在同一个城市或地区内。
- 跟踪移动端结果时使用移动代理,因为桌面端和移动端 SERP 差异足够大,会产生影响。
- 积极缓存,并按计划检查,而不是持续检查。
也值得直说:抓取搜索引擎通常违反其服务条款。如果你需要可靠的排名数据,官方 API 和获得许可的排名跟踪数据源是风险更低的路径。代理适用于 API 确实不存在的情况。
如何判断轮换是否有效
在整个抓取过程中跟踪这些指标,而不是按请求跟踪:
- 每个代理的成功率,即 2xx 响应数除以尝试次数。
- 每个代理的封禁率,并记录封禁类型:
403、CAPTCHA、空正文、同意页面。 - 每个代理的响应时间中位数。一个虽然能用但每页要花三十秒的代理是净损失。
- 实际使用的唯一 IP 数与你配置的代理池大小之比。
如果所有 IP 的成功率都差不多,那么你的问题不是 IP 信誉,再多的轮换也无济于事。
要点总结
只有在修复了请求速率、请求头和会话逻辑之后,才进行轮换。无状态抓取使用按请求轮换,任何多步操作使用粘性会话。在 Python 中,requests 需要 SOCKS 扩展,并且每个会话需要一个代理;而在 Scrapy 中,一个分配 request.meta['proxy'] 的下载器中间件就能完成大部分工作,剩下的由封禁跟踪包处理。对于排名跟踪,把地理位置视为需求,把轮换视为维护。