看几乎任何长期运行爬虫的抓取日志,都会出现同样的模式。绝大多数请求返回的页面与上一份副本完全相同。每一次抓取都消耗了带宽或额度,占用了一个并发槽位,并给别人的服务器带来负载,却什么也没得到。
这不是抓取器的漏洞,而是一个调度决策,通常是默认设置:以相同周期重新访问所有内容,或者更频繁地重新访问重要内容。这两种做法都显得合理,但都会浪费大部分预算。本文要讨论的是如何有意识地做出这个决策,为每一次访问按其可能带来的收益定价。
度量正确的单位
团队通常追踪的是每次抓取页面的成本。真正重要的数字是每次检测到变化的成本。
一个每天抓取一百万页、检测到一万次变化的爬虫,每次有用观察花费了一百次抓取。在不损失变化检测的前提下将抓取次数减半,就能将数据集成本减半。在不多发现变化的前提下将抓取次数翻倍,就是白白花了两倍的钱。把”未发现变化的抓取”按站点和页面类型放到仪表盘上,钱花在哪里就变得一目了然。
了解一次抓取的真实成本
一次访问的成本取决于你的计费方式,而两种常见模式会奖励不同的优化方向。
| 计费模式 | 你付费的对象 | 什么能让一次访问更便宜 |
|---|---|---|
| 住宅代理带宽 | 通过网关的字节数 | 更小的响应:不渲染、不加载图片、压缩传输 |
| 抓取 API 额度 | 成功的响应 | 更少的抓取次数;每次响应的大小影响不大 |
在 Shifter 的住宅网关上,每一个通过的字节都会计入,包括请求头,即使请求失败,只要在出错前传输了字节也会计入。缩小每次抓取的体积能直接带来收益,相关技巧参见削减代理带宽成本。而在 Web Scraping API 上,一次成功的请求消耗一个额度,无论是否启用了 JavaScript 渲染;失败的请求和目标错误不消耗额度。在这种模式下,渲染过的页面和普通页面成本相同,唯一能影响账单的杠杆就是你请求了多少次成功的抓取。
无论哪种情况,调度器都是你手里最大的杠杆,因为最便宜的抓取就是你决定不去做的那一次。
每个页面的变化频率是多少?
按预期价值调度,需要估算每个页面变化的频率。爬虫研究中的标准工作假设是,变化以某个平均速率大致随机地发生在每个页面上,据此可以得出自上次访问以来页面已发生变化的概率:
P(changed) = 1 - e^(-rate × time since last visit)
你可以从自己的历史数据中估算这个速率。每次访问都会告诉你页面自上一次访问以来是否发生了变化。将观察到的变化次数除以观察时长,按页面统计,并向同类页面的平均值做平滑处理,这样一个只访问过三次的页面就不会得到极端的估计值。
有两点需要注意。第一,一次访问只能告诉你页面发生了变化,而无法告诉你变化了多少次,因此变化速度快于你访问速度的页面看起来会比实际更慢。第二,要根据你关心的字段来定义”变化”。如果一个页面的时间戳、广告位或会话令牌每次加载都不同,那它看起来一直在变化,但这没有任何意义。应该对提取出的记录做哈希,而不是对 HTML 做哈希。
一个反直觉的结论:不要追逐变化最快的页面
显而易见的策略是按照页面变化频率的比例来访问它们。但这是错的,而这个证明已有二十多年历史。
在《Effective Page Refresh Policies for Web Crawlers》(ACM Transactions on Database Systems, 2003)一文中,Junghoo Cho 和 Hector Garcia-Molina 比较了在固定访问预算下保持本地副本新鲜度的分配策略。他们的发现,用他们自己的话说是:“均匀策略在任何情况下都总是比比例策略更有效。“他们的最优策略更进一步,他们直接总结道:“为了提高新鲜度,我们应该惩罚那些变化过于频繁的元素。”
一旦说明白,这个直觉就很简单。一个每十五分钟变化一次的页面,几乎在你抓取它之后马上又变旧了。访问它只能买来几分钟的新鲜度。而一个大约每天变化一次的页面,如果每天访问一次,那么在每次访问后的大部分时间里都是正确的。在预算有限的情况下,后者是远好得多的买卖。
你可以给它算个数。在相同的变化模型下,一次访问买到的新鲜度等于页面当前处于过期状态的概率,乘以此后能保持正确的预期时长。对于一个大约能每天重新访问一次的爬虫:
| 页面变化频率 | 每次访问买到的新鲜度(天) |
|---|---|
| 大约每15分钟一次 | 0.010 |
| 大约每小时一次 | 0.042 |
| 大约每6小时一次 | 0.241 |
| 大约每天一次 | 0.400 |
| 大约每周一次 | 0.124 |
| 大约每月一次 | 0.032 |
| 大约每年一次 | 0.003 |
最好的买卖是那些变化速度大致与你能负担的访问速度相匹配的页面。变化速度远快于此的页面,无论以什么可承受的速率访问,都几乎无法保持新鲜;变化速度远慢于此的页面,则几乎总是已经是新鲜的。
有一个重要的例外。上述结论讨论的是新鲜度:你的副本与实时页面匹配的频率。但有些任务的目标是捕捉事件:每一次价格变动、每一次缺货、每一次编辑。如果每次变化都单独重要,那么快速变化的页面需要更多而不是更少的访问,正确的答案甚至可能是完全换一个数据源,比如 API、订阅源,或是能在不完全抓取的情况下显示变化的列表页。在调优之前,先确定你要解决的是哪个问题。
按每单位成本的预期价值为每次访问打分
把这些要素组合起来,每个候选访问会得到一个优先级:页面的重要程度,乘以一次访问能买到的新鲜度,再除以这次访问的成本。
import math
def freshness_gain(rate, days_since_visit, interval_days):
"""Expected fresh days bought by visiting now, under a Poisson change model."""
p_stale = 1 - math.exp(-rate * days_since_visit)
fresh_after = (1 - math.exp(-rate * interval_days)) / rate if rate > 0 else interval_days
return p_stale * fresh_after
def priority(page, today, interval_days=1.0):
gain = freshness_gain(page.change_rate, today - page.last_fetched, interval_days)
return page.value * gain / page.cost
调度器随后依据优先级队列工作:每个周期,把预算花在得分最高的访问上,然后停止。其中三个输入值得仔细对待。
- 价值是一个业务判断,而非技术判断。一个畅销的产品、一个重要的竞争对手、一个客户经常运行的查询。保持粗粒度即可;通常三到四个层级就够了。
- 成本应该是这次访问的真实成本:该页面类型的字节数、额度消耗,以及是否需要渲染。在按带宽计费的方案上,一个需要无头浏览器的页面成本可能是普通抓取的许多倍。
- 变化速率来自你的历史数据,每次访问后都会更新。
在昂贵的抓取之前使用廉价的信号
很多时候,你可以用远低于抓取本身成本的方式了解页面是否发生了变化。
- 条件请求。 在网站支持
ETag或Last-Modified的情况下,304 Not Modified响应只需消耗一小部分字节。按主机追踪这些校验器是否可靠;有些网站发送了它们却又不遵守。 - 列表页作为变化检测器。 一个分类页或搜索页往往能显示数十个商品的价格和库存情况。抓取该列表页、进行比较,然后只抓取摘要发生变化的商品。这正是大多数实时价格订阅和库存监控能保持低成本的原因。
- 网站地图和订阅源。 如果它们携带可信的修改日期,就能在不访问其他任何内容的情况下告诉你发生了什么变化。
- 结构化端点。 页面背后的 JSON 响应通常比页面本身更小、更稳定。
为调度器看不到的东西预留预算
一个只针对已知页面做优化的调度器会逐渐变得”失明”。要在每个周期中预留一部分预算,用于以下三件事:
- 发现。 新的 URL 没有历史记录,永远无法在评分上超过已建立记录的页面。要为它们单独分配预算。
- 重新估算。 一个被评为几个月来都不变化的页面,仍然应该偶尔被访问,因为页面的行为会发生改变。没有这一步,一个错误的估计永远得不到纠正。
- 验证。 一个不论评分高低、按小规模随机抽样抓取的样本,能告诉你模型的假设是否依然成立。
让成本和健康状况反向影响调度计划
调度计划是一个方案,而不是保证。当某个网站开始限流时,调度器应该及时得知并重新排序,而不是继续排队等待抓取阶段已经无法完成的访问。这条从抓取器回传到前沿队列的反馈路径,在分布式爬虫中的背压与流量控制一文中有所讨论,网站的整体状况则在构建目标健康分数一文中有所涉及。健康分数的下降也应该提高访问该站点的有效成本,这正是一个具有成本意识的调度器所需要的信号。
结论
均匀分配的抓取预算,或者按页面繁忙程度成比例分配的抓取预算,大多都花在确认什么都没发生上。应该根据自己的历史数据估算每个页面的变化频率,按每次访问能买到的收益和需要付出的成本为其定价,并把预算优先分配给最好的买卖。这些最好的买卖往往是那些变化速度大致与你能负担的追踪速度相匹配的页面,而不是变化最快的那些。
这样做的好处不仅是账单变小。一个抓取次数更少、抓取更谨慎的爬虫,对它所依赖的站点来说也更轻量。
来源与参考文献
- Junghoo Cho 和 Hector Garcia-Molina,Effective Page Refresh Policies for Web Crawlers,ACM Transactions on Database Systems,Vol. 28, No. 4,2003 年 12 月。
- Shifter,住宅代理带宽与计费。哪些内容算作流量。
- Shifter,Web Scraping API 错误与限制。额度消耗、失败行为与重试。