Skip to content
文章 / 2 分钟阅读

用于网页抓取的轮换代理: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'] 的下载器中间件就能完成大部分工作,剩下的由封禁跟踪包处理。对于排名跟踪,把地理位置视为需求,把轮换视为维护。