礼貌爬虫仍能扩展:速率限制指南

猛烈攻击网站会被封禁;一次只抓取一个请求则毫无进展。可扩展的中间方法是根据每个域名自适应调整速度,分散到干净的IP上——对网站礼貌,对你快速。

现在几乎一半的互联网流量是自动化的,网站对此心知肚明。速率限制是他们的第一道也是最基本的防线——一个速度限制,决定你能获取多少以及多快。诱惑在于将其视为一个需要用蛮力突破的障碍,但这会导致封禁。相反的错误是一次小心翼翼地抓取一个请求,这在规模上毫无进展。可扩展的答案是礼貌爬虫:以一个合法但繁重的用户的方式调整每个目标的速度,将负载分散到干净的IP上,并让网站自己的响应调整你的速度。这就是如何保持快速而不成为导致所有人被封禁的流量。

知道你触碰了哪个限制

服务器根据一个标识符——通常是你的IP,有时是API密钥或账户——在一个时间窗口内计算你的请求,并在你超过阈值时采取行动。三种常见的模型表现不同:固定窗口按日历分钟计算请求;令牌桶每分钟给你固定数量的令牌(比如100个),每个请求消耗一个,当桶空时强制等待;滑动窗口在任何时刻计算过去滚动的60秒内的请求。实际结果是,突发比稳定流更危险——一秒内的十个请求可能触发一个限制,而一分钟内分散的一百个请求则不会。

限制也有不同的形式。软限制是温和的:服务器减慢你的速度,或者返回429 请求过多,并带有Retry-After头,告诉你确切的等待时间(Cloudflare的错误1015就是这样)。有些网站容忍小的突发——允许每分钟100个,但只在120个时限制,而另一些则在每分钟15个时就开始限制,并在30个时才完全封锁。硬限制是严格的上限:一个API允许每小时1000个请求,一旦超过就完全锁定你。反复触碰软限制会导致升级:429变为403,然后是几分钟到几小时的临时封禁,每次触发时窗口都会增长,最终是永久性地将你的IP或整个子网列入黑名单。

读取响应,不要猜测

对爬虫的最大升级是响应服务器告诉你的,而不是以固定速率猛攻。学习这些术语:429意味着减速并遵守Retry-After403意味着此身份被标记,因此要旋转而不是重试;503通常是一个挑战或临时拒绝;而返回验证码页面的200是软封锁,而不是成功。对每一个都要区别对待。错误的操作——在403时重试同一个被封的IP,或忽略Retry-After并在429时继续猛攻——会将软警告变为永久封禁。我们关于修复429的深入探讨详细介绍了响应处理。

import time, requests

def polite_get(session, url, max_tries=4):
    for attempt in range(max_tries):
        r = session.get(url, timeout=20)
        if r.status_code == 200 and "captcha" not in r.text.lower():
            return r
        if r.status_code == 429:                       # obey the server
            wait = int(r.headers.get("Retry-After", 2 ** attempt))
            time.sleep(wait)
            continue
        if r.status_code in (403, 503):                # this exit is burned
            rotate_ip(session)                         # fresh IP, then retry
            time.sleep(2 ** attempt)                    # exponential backoff
            continue
        return r
    return None
自适应节奏循环图,发送请求,读取状态码,旋转IP并退避,然后调整速率
让网站自己的响应代码驱动你的速度:在200时加速,429时遵守Retry-After,403时旋转。

为每个域名分配请求预算

一个触及多个网站的爬虫不应对所有网站应用一个全局速率。一个小博客和一个坚固的市场所能承受的负载截然不同,因此给每个域名分配自己的预算。每个主机的令牌桶是一个清晰的模式:为每个域名分配一个保守的速率,随着时间的推移补充,并让对不同主机的请求并行运行,而对同一主机的请求保持在其限制内。在新的目标上慢慢开始,让响应代码告诉你是否可以加速。

import time
from collections import defaultdict

class DomainLimiter:
    def __init__(self, per_min=30):
        self.gap = 60.0 / per_min          # min seconds between hits per host
        self.last = defaultdict(float)
    def wait(self, host):
        now = time.time()
        delay = self.gap - (now - self.last[host])
        if delay > 0:
            time.sleep(delay)
        self.last[host] = time.time()

# 30 req/min to any single host; different hosts proceed independently
limiter = DomainLimiter(per_min=30)
limiter.wait("example.com")

分散负载以保持每个IP的礼貌

这是将“礼貌”与“扩展”调和的举措:礼貌是以每个IP为单位衡量的,但你的总吞吐量是所有IP的总和。如果一个目标每分钟允许每个地址30个请求,一个IP将你限制在30个——但十个干净的IP,每个执行30个,可以让你每分钟达到300个,同时每个出口都保持礼貌。一个旋转住宅网关会自动完成这一操作,每次请求分配一个新的IP,覆盖9000万+个地址,因此没有单个出口看起来具有攻击性。这不是一个猛烈攻击的技巧;这是分配真实负载,以便没有一个服务器承受可疑的峰值。IP旋转的基本原理在IP旋转是什么以及为什么重要中。

分散负载到干净的旋转IP上

减少请求,更多缓存,选择合适的时间

最礼貌的请求是你从未发送的请求。三个习惯可以在不损失数据的情况下减少负载。首先,积极缓存并使用条件请求——发送If-Modified-SinceIf-None-Match,这样未更改的页面会返回一个小的304而不是完整的主体,从而节省服务器和你的带宽。其次,故意预算并发:一个信号量限制每个域名的在途请求,防止意外突发。第三,将重负载任务安排在目标的非高峰时段,当你的流量占他们的比例较小时,不太可能触发阈值。结合这些,可以将一个任务所需的请求减半——这是我们更广泛的反封禁清单背后的纪律。

还有一件值得明确说的事情:检查robots.txt并遵守网站声明的爬行期望。礼貌不仅仅是自我保护——成为一个好公民可以让开放的网络对每个人都可抓取。我们关于robots.txt 实践的指南涵盖了它的约束和不约束。

从429软限制到403,再到临时封禁和永久IP或子网黑名单的升级梯图
反复触碰软限制会使其变硬:429变为403,然后是不断增长的临时封禁,最终是永久黑名单。

常见问题解答

如何在爬虫时避免速率限制?

为每个域名设定自己的请求预算,在429时遵守Retry-After,指数退避,并将负载分散到一个干净IP的旋转池中,以免任何单个地址显得具有攻击性。添加缓存和条件请求以减少总体请求,并将重负载任务安排在目标的非高峰时段。

HTTP 429是什么意思,我该如何处理?

429 请求过多是一个软速率限制——服务器要求你减速,而不是封禁你。读取Retry-After头,并在重试前等待确切的时间;如果缺失,则指数退避。永远不要忽视它并继续猛攻,因为反复的429会升级为403,然后是定时或永久封禁。

每分钟多少请求是安全的?

没有通用的数字——完全取决于目标。一个小网站可能只容忍每分钟几个请求;一个大网站则可能更多。保守地开始(比如每分钟每个IP 20-30个),观察429,并根据响应进行调整。通过增加IP来扩大总吞吐量,而不是提高单个IP的速率。

旋转代理是否被视为不礼貌?

如果用于分配真实负载,则不是。旋转保持每个单独的IP在礼貌的速率内,同时你的总吞吐量增加——服务器从未看到任何一个地址的可疑峰值。只有当你用它来超过网站合理处理的总量时,才会变得不礼貌;要调整整体,而不仅仅是每个IP的速率。

礼貌和可扩展性并不是对立的。读取信号,遵守Retry-After,为每个域名分配预算,缓存你能缓存的,并将其余的分散到干净的旋转IP上。你会比鲁莽的爬虫更快——因为你是那个从不被封禁的人。

通过旋转住宅代理礼貌地扩展