跳到正文

网络爬虫系统设计:URL Frontier、礼貌策略与去重

设计一个既能扩展又守礼貌的网络爬虫:URL Frontier、按主机划分的队列、robots.txt 规则、URL 与内容去重、爬虫陷阱以及容量估算。

· 创始人兼开发者

发布于

一个标为 URL Frontier 的框架中有优先级队列和按主机划分的队列。抓取器取出 URL,使用 DNS 缓存和 robots.txt 缓存,从互联网下载页面并存入对象存储库。解析器提取链接,检查已见 URL 过滤器和指纹索引,再把链接送回 Frontier。

爬虫看起来很简单:下载一个页面,提取它的链接,再下载这些链接,如此循环。写成一个循环,对一千个页面是可行的。到了十亿个页面,它就会以颇具启发性的方式崩溃:用数百个并行请求猛轰小网站,用五十个不同的 URL 下载同一个页面,迷失在无穷无尽的日历里,并把大部分时间花在等待 DNS 上。

本文设计一个既能扩展又守礼貌的爬虫:容量估算、带优先级队列和按主机队列的 URL Frontier、由 RFC 9309 标准化的 robots.txt、URL 与内容去重、爬虫陷阱以及它们周围的组件,并配有一张展示整个循环的图。

需求与估算

目标:每月为搜索或分析索引抓取约十亿个页面,更频繁地刷新重要页面,并且绝不让任何网站过载。

指标 假设或计算 结果
每秒页面数 1,000,000,000 / (30 × 86,400 s) 每秒约 386 个
带宽 386 × 假设每页 100 KB 约 39 MB/s,约 310 Mbit/s
原始存储 10 亿 × 100 KB 压缩前每月约 100 TB
发现的 URL 每页几十个链接 远多于你能抓取的 URL

最重要的是最后一行。发现的速度远远超过抓取的速度,所以爬虫始终在选择不抓取什么。优先级排序是核心功能,而不是优化。

抓取循环

  1. 从 Frontier 中取出下一个 URL。
  2. 从缓存中检查其主机的 robots.txt。
  3. 通过本地 DNS 缓存解析主机名。
  4. 在遵守该主机礼貌延迟的前提下抓取页面。
  5. 存储原始响应。
  6. 解析页面,提取链接并对其进行规范化。
  7. 丢弃已经见过的链接;把其余链接连同优先级加入 Frontier。
  8. 为内容计算指纹,并在后续处理中跳过近似重复的页面。

第一步之后的每一步,都是朴素的爬虫浪费精力或造成伤害的地方。

URL Frontier

Frontier 是爬虫的心脏:一个必须同时回答两个不同问题的队列。哪个 URL 最重要?现在可以连接哪个主机?

经典答案是 Mercator 的设计,见于《Introduction to Information Retrieval》的 URL Frontier 一章:

  • 前端队列负责优先级。 优先级分配器根据页面变化频率、链接热度或所在网站的重要性等信号,把每个新 URL 分配到 F 个队列之一。
  • 后端队列负责礼貌。 每个后端队列只保存一个主机的 URL。一个堆为每个主机记录下一次可以连接它的最早时间。
  • 抓取线程取出堆顶的主机,从该主机的后端队列中抓取一个 URL,再以新的最早时间把主机放回堆中。后端队列清空时,会从前端队列中补充,并偏向更高的优先级。

Mercator 的设计者建议后端队列的数量约为抓取线程的三倍,这样线程就很少需要等待需要礼让的主机。

在大规模下,按主机对 Frontier 分区:对主机名做哈希来选择 Frontier 分片,这样同一主机的所有 URL 都在同一个地方,礼貌策略可以在本地执行,无需协调。

礼貌策略与 robots.txt

有礼貌的爬虫对每个主机一次只发一个请求,并在请求之间等待。同一章中有一条常用的经验法则:等待时间约为上一次从该主机抓取所用时间的十倍,这样慢的服务器会自动获得更多喘息空间。遇到 429 和 503 响应时进一步退避,并遵守任何 Retry-After 头。

robots.txt 在 2022 年随 RFC 9309 成为标准。爬虫必须正确执行的规则如下:

情况 爬虫必须怎么做
成功获取 robots.txt 遵守针对你的 user agent 的规则,没有时退回 * 组
状态码 400–499 视为没有 robots.txt:允许抓取
状态码 500–599 或网络错误 假定完全禁止
长时间无法访问,例如 30 天 可以视为不可用,或继续使用缓存的副本
重定向 至少跟随五次连续重定向
缓存 除非文件无法访问,否则缓存的副本使用不得超过 24 小时
文件大小 至少解析前 500 KiB

4xx 与 5xx 之间的不对称常常让人意外:文件不存在意味着“请便”,而服务器错误意味着“别进来”,因为爬虫无法知道规则本来会怎么说。Crawl-delay 不属于 RFC 9309,各个爬虫是否遵守它也不尽相同。请用一个带有联系方式 URL 的描述性 user agent 来标识你的爬虫。

URL 去重

同一个页面藏在许多 URL 背后:HTTP://Example.com:80/a/./b?utm_source=x#top 和 http://example.com/a/b 是同一个页面。在检查一个 URL 是否见过之前,先进行规范化:

  • 按照 RFC 3986 第 6 节,把协议和主机转为小写,去掉默认端口,解析 . 和 .. 路径段,并规范化百分号编码。
  • 去掉片段,它根本不会发送到服务器。
  • 谨慎地移除 utm_* 这类已知的跟踪参数;其他参数可能会改变内容。
  • 抓取后遵守 rel="canonical" 提示。

然后检查已见 URL 集合。面对数十亿个 URL,布隆过滤器是常见选择。按十亿个 URL、1% 误判率来设计,大约需要每个 URL 9.6 位,总计约 1.2 GB,使用七个哈希函数。误判意味着跳过一个其实没见过的 URL,对大多数抓取来说,1% 是可以接受的代价;对于站点地图条目这类绝不能漏掉的 URL,要另外维护一个精确的存储。

内容去重

镜像站、打印友好版本和会话参数,会在不同 URL 下产生完全相同或几乎相同的页面。完全重复很好处理:对规范化后的内容做哈希并比较即可。近似重复则需要一种指纹:页面变化很小时,指纹的变化也很小。simhash 是标准技术:一种 64 位指纹,相似文档之间只有少数几位不同。Google 的研究人员在 2007 年报告说,对于一个包含八十亿页面的库,使用以三个不同位为阈值的 64 位 simhash 指纹,是检测近似重复页面的实用设置。

爬虫陷阱

有些网站会生成无限的 URL 空间,而且往往是无意的:永远有“下个月”链接的日历、每个 URL 里都带着的会话 ID、包含所有筛选组合的分面搜索,以及不断加长路径的相对链接。防御措施:

  • 限制 URL 长度和路径深度。
  • 按主机的重要性,限制每个抓取周期内每个主机的页面数。
  • 检测 /a/b/a/b/a/b 这样重复出现的路径段。
  • 对内容指纹总是与已有页面匹配的 URL 降低优先级。

站点地图则从相反的方向提供帮助:它们列出网站希望被抓取的 URL,每个站点地图文件最多 50,000 个 URL 或 50 MB(未压缩),更多时使用站点地图索引文件。

循环之外:DNS、存储与重新抓取

  • DNS 是一个隐藏的瓶颈。每秒数百次抓取时,在抓取器附近部署一个共享的缓存解析器,可以避免把每次查询都发往上游。
  • 存储。 把原始响应以大型批量文件的形式写入对象存储,并通过数据流把解析后的文本和链接送往索引。
  • 重新抓取。 页面的变化速度各不相同。记录每个页面在以往访问中变化的频率,并据此安排下一次抓取;使用 If-Modified-Since 或 ETag 的条件请求,在内容没有变化时能节省带宽。
  • 大量依赖 JavaScript 的页面需要无头浏览器,每页的成本要高得多。只对确实需要的主机进行渲染。

网络爬虫图

一个标为 URL Frontier 的框架中有优先级队列和按主机划分的队列。抓取器取出 URL,使用 DNS 缓存和 robots.txt 缓存,从互联网下载页面并存入对象存储库。解析器提取链接,检查已见 URL 过滤器和指纹索引,再把链接送回 Frontier。

13 个组件 · 11 个连接

在 Archboard 中打开
图表内容
  • URL frontier
  • Priority queues (F)
  • Per-host queues (B)
  • Fetchers
  • Caching DNS resolver
  • robots.txt cache, 24 h max
  • Websites
  • Raw pages
  • Parser + link extractor
  • Seen URLs (Bloom filter)
  • Content fingerprints (simhash)
  • pages for indexing
  • refill by priority
  • next host that is due
  • resolve
  • allowed?
  • GET, polite
  • raw response
  • parse
  • new links
  • unseen URLs
  • simhash
  • text + links
一个抓取循环。URL Frontier 按优先级安排工作,每次只交出一个主机;抓取器检查 robots.txt 和 DNS 缓存并存储页面,解析器把新链接经过已见 URL 过滤器送回去,同时丢弃近似重复的页面。

这张图的读法如下:

  • 框架就是 URL Frontier:优先级队列为按主机划分的队列供给 URL,后者负责执行礼貌策略。
  • 抓取器每次对每个主机只取一个 URL,查询 robots.txt 缓存和 DNS 缓存,然后从互联网下载。
  • 原始页面进入对象存储库。解析器提取链接和文本。
  • 新链接通过已见 URL 过滤器后回到 Frontier,从而闭合循环。内容指纹会标记出近似重复的页面。
  • 解析后的页面经由右侧的数据流送往索引。
  • Markdown 块列出了容量估算和礼貌规则。

绘制带循环的流水线

  • 让循环清晰可见。 爬虫是一个环;把它画成一条直线式的流水线,会掩盖导致陷阱和重复问题的反馈。
  • 标明礼貌策略在哪里执行。 把按主机划分的队列画进图里,而不是放在脚注中。
  • 把缓存标注为缓存。 robots.txt 缓存和 DNS 缓存会改变施加在外部世界上的负载;标注它们的过期时间。
  • 在连线上写上数字:流入的每秒页面数,流出的每页链接数。

通用方法请参阅系统设计图示例。对每个主机的速率控制与限流器系统设计用的是同样的思路,抓取之后的处理则与数据管道架构图类似。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 添加一个名为 URL frontier 的基础节点包中的 框架,并在其中放两个数据节点包中的 队列 节点:优先级队列和按主机划分的队列。
  3. 为抓取器添加一个云节点包中的 计算实例 节点,添加网络节点包中的一个 DNS 节点和一个 互联网 节点,再为 robots.txt 缓存添加一个数据节点包中的 Redis 节点。
  4. 为原始页面添加一个数据节点包中的 对象存储库,并为解析器再添加一个 计算实例 节点。
  5. 为已见 URL 过滤器添加第二个 Redis 节点,为内容指纹添加一个云节点包中的 托管数据库,并为送往索引的页面添加一个数据节点包中的 Kafka 节点。
  6. 用带标签的连线画出循环,并添加一个基础节点包中的 Markdown 块,写上这些数字。

这个示例共用 13 个图表节点,在 Free 套餐的限制之内。

常见问题

什么是 URL Frontier? 它是决定爬虫下一步抓取什么的数据结构。它把按重要性为 URL 排序的优先级队列,与按主机划分的队列及调度结合起来,防止爬虫过于频繁地连接任何一个主机。

爬虫如何保持礼貌? 每个主机同一时间只保持一个连接,请求之间的延迟随服务器响应时间增长,遇到 429 和 503 响应时退避,并遵守 robots.txt。

如果 robots.txt 返回错误会怎样? 根据 RFC 9309,4xx 状态码意味着爬虫可以抓取该网站,而 5xx 状态码或网络错误意味着在能够读取该文件之前,爬虫必须假定一切都被禁止。

爬虫如何避免重复下载同一个页面? 它们对 URL 进行规范化,并对照已见 URL 集合(通常是布隆过滤器)进行检查;同时用精确哈希和 simhash 为内容计算指纹,找出藏在不同 URL 下的同一页面。

十亿个 URL 的布隆过滤器需要多大? 误判率为 1% 时约 1.2 GB,每个 URL 大约用 9.6 位,并使用七个哈希函数。更低的错误率需要每个条目使用更多的位。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。