每个爬虫都会重试失败的请求,而大多数人选择重试次数的方式都一样:三次,因为感觉上比较合理。但重试并不是免费的。在按千兆字节计费的住宅代理上,每次重试都要重新下载页面;在任何系统上,重试都要耗费时间并给目标带来负载。而且通常每次重试成功的可能性都比上一次更低。
所以总会有一个点,再多尝试一次的成本会超过其价值。本研究展示了如何从你自己的日志中找到这个点,并附有一个实例和可在你的数据上运行的代码。
为什么后续重试的成功率更低
如果失败纯粹是运气不好,那么每次重试的成功概率应该和第一次尝试一样。但实际情况并非如此。一次失败的请求更可能是因为某种持续存在的原因导致失败:页面不存在、该地址被封、网站宕机、解析器永远匹配不上。到达第四次尝试的任务大多是难啃的硬骨头。
这意味着每次尝试的成功率会下降,而每多一次成功所付出的成本也会随之上升。
从日志中测量它
你需要的唯一数据是:对每个任务,记录成功的那次尝试的次数,或者标注所有尝试都失败了。大多数爬虫已经记录了这些信息。由此,第 k 次尝试的成功率就是到达第 k 次尝试并在该次成功的任务所占的比例。
from collections import Counter
def attempt_success_rates(outcomes, max_attempts):
"""Chance that attempt k succeeds, given the job got that far.
outcomes: per job, the attempt number that succeeded, or None if every
attempt failed (with max_attempts tries).
"""
succeeded_at = Counter(o for o in outcomes if o is not None)
reached = len(outcomes)
rates = []
for k in range(1, max_attempts + 1):
rates.append(succeeded_at[k] / reached if reached else 0.0)
reached -= succeeded_at[k]
return rates
def cost_per_success(rates, cap, attempt_cost):
"""Expected cost of one successful job if we stop after `cap` attempts."""
p_reach, spent, success = 1.0, 0.0, 0.0
for rate in rates[:cap]:
spent += p_reach * attempt_cost
success += p_reach * rate
p_reach *= 1 - rate
return spent / success if success else float("inf")
def best_cap(rates, attempt_cost, value_of_success):
"""The last attempt whose expected value still exceeds its cost."""
cap = 0
for k, rate in enumerate(rates, start=1):
if rate * value_of_success > attempt_cost:
cap = k
return cap
三个函数:
attempt_success_rates将你的日志转换为每次尝试的成功率。cost_per_success给出在特定尝试次数后停止时,一次成功任务的平均成本。best_cap找出最后一次预期价值仍高于其成本的尝试。
一个实例
下面是一个包含 10,000 个任务的示例日志,每个任务最多允许六次尝试:
from retry_econ import attempt_success_rates, best_cap, cost_per_success
# Illustrative outcomes for 10,000 jobs, each allowed up to six attempts:
# the attempt that succeeded, or None if all six failed.
outcomes = [1] * 8200 + [2] * 900 + [3] * 260 + [4] * 90 + [5] * 40 + [6] * 20 + [None] * 490
rates = attempt_success_rates(outcomes, max_attempts=6)
print("success rate by attempt:", [round(r, 3) for r in rates])
for cap in range(1, 7):
print(f"cap {cap}: cost per success {cost_per_success(rates, cap, attempt_cost=1.0):.3f}")
print("best cap if a success is worth 5 attempts:", best_cap(rates, attempt_cost=1.0, value_of_success=5.0))
print("best cap if a success is worth 20 attempts:", best_cap(rates, attempt_cost=1.0, value_of_success=20.0))
success rate by attempt: [0.82, 0.5, 0.289, 0.141, 0.073, 0.039]
cap 1: cost per success 1.220
cap 2: cost per success 1.297
cap 3: cost per success 1.357
cap 4: cost per success 1.412
cap 5: cost per success 1.464
cap 6: cost per success 1.514
best cap if a success is worth 5 attempts: 3
best cap if a success is worth 20 attempts: 5
有三点值得注意。
成功率下降得很快。 第一次尝试的成功率为 82%。在到达第二次尝试的任务中,一半成功。到第五次尝试时,成功的比例不到十分之一。
每多一次成功都比上一次更贵。 用一次尝试的成本除以其成功率,可以得到该次尝试产生一次成功的成本:第一次尝试大约相当于 1.2 次尝试的成本,第二次为 2 次,第三次为 3.5 次,第四次为 7 次,第六次则超过 25 次。随着允许的尝试次数增加,平均每次成功的成本上升得较慢,因为每多一次尝试只作用于更少的任务,但边际成本却急剧攀升。
合适的上限取决于一次成功的价值。 如果一条成功的记录价值相当于五次尝试的成本,那就在三次后停止。如果价值相当于二十次,比如某个你必须拿到的页面,那就应该做到五次。没有唯一正确的数字;只有针对每种价值而言正确的数字。
为一次成功赋予价值
一次成功的价值是你必须自行决定的唯一输入。以下是几种设定方式:
- 可替代的记录,比如每天收集的众多价格观测值之一,每条的价值都很低。漏掉一条无妨;较低的上限是合适的。
- 必需的记录,比如完成客户正在等待的数据集所需的页面,价值要高得多。更高的上限是合理的。
- 带宽之外的成本也算在内:时间、对目标的负载,以及反复撞击一个失败页面可能导致其他所有请求被封的风险。
在只对成功响应收费的网页抓取 API 上,重试的金钱成本转移到了服务商身上,但时间和目标负载的影响依然存在。例如,Shifter 的网页抓取 API 不对失败的请求收费。
让重试更便宜
上限只是问题的一半。另一半是让每次重试更有可能成功:
- 重试前先等待,并且每次等待的时间更长。许多失败只是临时的速率限制,过一会儿就会自行解除。
- 当失败提示你该改变某些东西时,就去改变。 某个地址被封,换一个地址可能就能成功;超时可能需要更长的超时时间,而不是原样重发同一个请求。
- 不要重试那些不可能成功的请求。 404 或身份验证错误每次都会失败。
我们关于网页爬虫的重试与退避的指南详细介绍了这些技巧。
在你自己的数据上运行
- 导出一周内所有任务中成功的尝试次数或失败标记。
- 用你当前的最大尝试次数运行
attempt_success_rates。 - 确定一次成功相当于多少次尝试的价值。
- 将上限设为
best_cap返回的值,如果不同任务类型有差异,可分别设定。 - 每月重新检查一次。随着网站和你自身设置的变化,成功率也会变化。
常见问题
爬虫应该重试失败的请求多少次?
这取决于每次尝试的成功率下降得有多快,以及一条成功记录的价值是多少。从你的日志中测量每次尝试的成功率,然后在最后一次预期价值仍高于其成本的尝试处停止。对许多任务来说,这是两到四次尝试。
为什么重试的成功率比第一次尝试更低?
因为失败并非随机的。失败过一次的任务更可能存在某种持续性的问题,比如页面缺失或被封,因此之后的每次尝试都是从更难的情况中抽取出来的。
使用住宅代理时重试会产生费用吗?
会。住宅代理流量是按用量计费的,所以每次重试都要重新下载页面。返回大型错误页面的失败尝试同样会消耗带宽。
我应该重试 404 错误吗?
不应该。不存在的页面在下一次尝试时依然不存在。只重试那些之后可能成功的失败,比如超时、速率限制和临时性的服务器错误。
结论
只要下一次尝试成功的概率乘以成功的价值仍高于尝试的成本,重试就是值得的。你的日志中已经包含了每次尝试的成功率;去测量它,为一次成功赋予价值,然后根据这些数字来设定上限,而不是凭习惯行事。