01这场爬取是为了什么——搜索索引、LLM 训练数据,还是归档?它决定了规模与新鲜度。
从种子开始抓取页面,并存储原始内容供下游使用——由消费端定义什么叫「完成」。
免费完整指南
设计网页爬虫。请覆盖URL frontier design: priority queues for scheduling and re-crawl ordering, Politeness: robots.txt compliance and per-domain rate limiting to avoid hammering one host, JavaScript rendering...
面试节奏保持精简,让页面能把注意力花在真正的设计决策上。
不要只是陈述需求,要主动问出来。每张卡把设计约束和一句你可以在画架构前说出口的澄清问句配成一组。
01这场爬取是为了什么——搜索索引、LLM 训练数据,还是归档?它决定了规模与新鲜度。
从种子开始抓取页面,并存储原始内容供下游使用——由消费端定义什么叫「完成」。
02爬取如何成长——抽取出的链接会喂回来吗?
每一个抓取到的页面都会被解析出链接,经过过滤后再喂回 frontier——这场爬取靠种子自我维持。
03礼貌性:是硬性需求还是有更好?
这是一项硬性需求:遵守 robots.txt(含 crawl-delay),通过 user-agent 诚实表明身份,并且永远不让任何单一主机过载。
04运营者可以在爬取途中注入种子与优先 URL 吗?
可以:frontier 接受运营者以较高优先级入列,排在被发现的链接之前——一场爬取是被操控的,而不是射后不理。
05我们到底要存储什么——原始字节、解析后的文字,还是两者都要?
两者都要:原始内容放在 blob 存储,加上抽取出的文字与元数据——下游的重新处理绝不该需要重新抓取整个网络。
06robots.txt 必须多新?
数小时内保持新鲜:绝不能拿超过几小时旧的 robots.txt 规则去爬一台主机——依据一份过时的允许行事是一种合规风险。
范围外搜索排名与索引(爬虫产出语料库,而不是索引) · 动态页面的 JavaScript 渲染 · 为了新鲜度的持续重爬(先做一次完整爬取)
01多少页面,多快?
5 天内爬完 100 亿页——这一句话就决定了机队规模与队列吞吐量。
02什么保护我们所爬的网站?
无论积压多少,都遵守每台主机的 crawl-delay:为单一网站排队的一百万个 URL 必须缓慢排出,绝不能成为洪流。
03同一页面常常住在许多 URL 上——镜像、追踪参数、www 变体。语料库应该每个页面只留一份吗?
绝不把同一个 URL 抓两次,而经由不同 URL 到达的同一页面,在语料库里只能落地一次。
04如果一台机器在爬取途中宕机,它正在处理的 URL 可以丢吗,还是每个 URL 最终都必须有交代?
什么都不会被无声丢失:每个 URL 最终要么被抓取、要么被明确记录为失败——一台机器宕机永远不能让工作消失。
05无限的 URL 空间怎么办?
深度上限、每域名的页面预算,以及 URL 模式过滤器,让日历页面与广告农场不会吃掉整场爬取。
真实面试探得比一份整齐清单深得多。这些范围问句,区分出真正拷问问题的人和只是背诵的人。
把每个估算都当成一种压力,用来合理化一个组件:缓存、队列、分片、副本、worker pool 或退路。
所需抓取速率
100 亿页 ÷ 5 天10,000,000,000 ÷ 432,000 秒 ≈ 持续 23K pages/s
这是一个机队,而不是一台服务器——而且 frontier 每秒必须派出 23K 个抓取 URL,并遵守礼貌抓取——绝不对同一台主机猛打。
机队规模
每页平均抓取延迟约 2 秒,所以每条连接约 0.5 pages/s · 每台机器约 4K 条有效并发连接23K ÷ (4,000 × 0.5) ≈ 持续 12 台机器——为重试与慢速主机预留约 2 倍
二十几台抓取器就能赶上期限;瓶颈是礼貌性,而不是运算力。
URL-seen 内存
100 亿个 URL 放进一个 1% 假阳性的 Bloom filter(每个约 10 bits)10B × 10 b ≈ 12 GB——相对于精确字符串的约 400 GB
Bloom 给出的「肯定是新的」才是重点:一次假阳性会跳过一个 URL,而假阴性永远不会发生——所以没有任何东西会被爬两次。
语料库的存储
100 亿页 × 每页约 100 KB 的已存储 HTML——常被引用的数 MB 页面重量大多是这个爬虫从不抓取的图片与脚本≈ 1 PB 原始数据
带压缩的 blob 存储;元数据(哈希、URL)留在一个独立的存储中保持可查询。
DNS 压力
23K fetches/s,每一次都需要解析没有缓存:23K lookups/s · 有每抓取器的 DNS 缓存:每个新主机约 1 次
机队内的 DNS 缓存是必备的——公共解析器会把这场爬取限速到死。
决策示例
连续五天每秒两万三千页——但真正的硬约束在相反的方向:任何单一网站都绝不能感受到超过涓涓细流的量。
我会把 frontier 分成两个阶段:前端队列按优先级排序,后端队列严格地每主机一条,并为每台主机配一个遵守 crawl-delay 的 token bucket。抓取器租用 URL(绝不删除),成功时 ack,并让租约超时来恢复宕机。入列前,URL 先过一次 Bloom filter 的 seen 检查;抓取后,用内容哈希抓出不同 URL 下的同一页面。robots 规则按域名带 TTL 缓存,而 DNS 在机队内部有自己的缓存。
我不会做的事:用一个全局的优先级队列——某个大站一旦倒进一百万个 URL,抓取器就会猛打那台主机,爬取就变成一场 DDoS。我也不会用一张精确的数据库表去追踪看过的 URL:那是 400 GB 的字符串加上每次入列一次查询,而一个 12 GB 的 Bloom filter 免费就能回答「肯定是新的」——而且它罕见的假阳性只不过跳过一个 URL,那是安全的方向。
如果语料库需要的是持续的新鲜度而不是一次爬取,frontier 就会多出一个重爬调度器:页面按变更频率与重要性重新入列,而 seen 过滤器切换成一种支持老化淘汰的结构。
先看一张完整的图,再把每条路径各自画成一张图 —— 写入路径与读取路径承载不同流量,合理化不同的组件。
完整全貌
路径 1
只有当一个 URL 的主机有 token 时它才会被发出;宕机时租约回到队列,重试会退避,而反复失败会落进一个 dead-letter 队列。
路径 2
每一页都产出链接;过滤器丢掉陷阱与垃圾,Bloom filter 丢掉所有已经看过的,而存活下来的重新进入 frontier。
在优化之前,先让契约可被检视:端点、实体、所有权、重试与状态。
QUEUEfrontier.enqueue(url, depth)
响应已接受 · 已丢弃(seen/filtered/超出预算)
内部契约,不是 REST——爬虫没有公开 API。入列会先通过 URL-seen 过滤器与模式过滤器。
QUEUEfrontier.lease(fetcher_id) → CrawlTask
响应带 lease TTL 的任务 · 成功时 ack(task) · nack → 以退避重试
每主机的后端队列强制礼貌性:只有当那台主机的 token bucket 有 token 时,抓取器才会收到一个 URL。
GEThttps://{host}/robots.txt (external)
响应规则以 TTL 缓存在 DomainState 中
唯一的对外契约:遵守 Disallow 与 crawl-delay,并送出诚实的 User-Agent。
核心实体
CrawlTaskurl · domain · depth · status: queued/leased/done/failed · retries
工作的单位;在抓取器处理它时是被租用(而非删除)的,所以宕机能自我修复。
DomainStatedomain (PK) · robots_rules · crawl_delay · last_fetch_at · pages_crawled
robots 规则按域名带 TTL 缓存;token bucket 就住在这里。
Pageurl · content_hash (SHA-256) · fetched_at · blob_ref
content_hash 驱动第二层去重——不同 URL 下的相同内容只存储一次。
在面试最后三分之一挑一条路线。每条路线给你主题、它该回答的面试官问题,以及要避免的失败模式。
frontier 为一个不起眼的网站握着一百万个 URL。是什么保证它永远不会被猛打?
那些 URL 全进入这个主机专属的一个队列。一个跟站点 crawl-delay 挂钩的 token bucket 一次放一个出来,fetcher 只能取到 bucket 允许的量。加上一千台机器,那个主机看到的还是同样的细水长流——是队列在强制它,不是靠自觉。
在抓取器内部做速率限制——礼貌性必须是结构性的,放在每主机队列里,否则一个横向扩展的机队就会把它破坏掉。
一百亿个 URL——你如何在内存中对每次入列回答「以前看过吗?」,而在这里一次 Bloom 假阳性的代价是什么?
每次入队之前查一下 Bloom filter。100 亿个 URL、每个约 10 bit,大约 12 GB 就装得下,而存精确字符串要 400 GB。它绝不会给假阴性,所以不会有东西被爬两次。偶尔一次假阳性顶多跳过一个 URL——这是安全的那个方向。
把假阳性当成错误——跳过一个 URL 是设计好的代价;爬两次才是失败。
镜像、追踪参数,以及 www/非 www 都提供一模一样的内容。第二层去重放在哪一层,用什么 key?
第二层放在抓取之后,用内容哈希当 key。对页面正文做哈希,再到一个 seen-hashes 存储里查。命中就保留一份规范副本,把另一个 URL 记成别名。归一化负责处理跟踪参数和 www 变体;只有哈希才抓得住来自不同主机的相同页面。
只靠 URL 规范化——只有内容哈希才抓得到跨主机的真正重复。
一个网站永无止境地产生有效的「下个月」链接。什么为爬取设界,你又如何侦测这个陷阱?
三道限制叠在一起:深度上限、每个域名的页面预算,以及给机器生成的形态(比如不断递增的日期)打标的 URL 模式过滤。真正起侦测作用的是预算。当一个小域名烧掉成千上万个几乎一样的页面时,它就会被限速或切断——这样任何单个陷阱都吃不掉整个爬取。
只靠深度——每域名的预算与 URL 模式启发法必须撑住它。
一台机器在抓取途中宕机。它进行中的工作会怎样,恢复的代价是什么?
什么都不会丢,因为每个 URL 是被租用的,不是被删掉的。当那台挂掉的机器不再 ack,它那 4,000 个租约就超时,URL 回到队列给其他 fetcher。恢复的代价只有租约超时的那段延迟,加上重新抓取当时在途的那些——有界而且便宜。
在发出时就把 URL 从队列删掉——应该用带超时的租约,完成时再 ack。
把 网页爬虫 大声讲一遍,让 AI 为你的说明评分。