---
title: 系统设计面试中的Web爬虫：核心考点、分布式架构、礼貌性策略及常见误区深度解析与实践指南
created: 2026-03-12
updated: 2026-03-12
type: summary
tags: ["crawler", "design", "interview", "notes", "telegram"]
external: http://go/ui/posts/2151/
status: inbox
---

设计 Web Crawler（网络爬虫）是系统设计面试中的经典题目。它不仅考察你对分布式系统的理解，还涉及多线程/异步处理、数据一致性、网络协议以及对“礼貌性”（Politeness）的考量。

以下是对 ByteByteGo 这一章节内容的深度总结和面试准备指南：

### 1. 核心考点 (Key Assessment Points)

在面试中，面试官通常希望看到你如何处理以下几个维度：

* **可扩展性 (Scalability)：** 互联网上有数亿个网页，如何通过分布式系统并行爬取？
* **礼貌性 (Politeness)：** 如何避免短时间内给同一个域名发送过多请求导致对方服务器崩溃？
* **鲁棒性 (Robustness)：** 面对死循环（Spidertraps）、恶意内容、大数据量、服务器宕机，系统如何保持稳定？
* **去重 (De-duplication)：** 如何高效地判断一个 URL 或一份内容是否已经处理过（亿级数据的处理）？
* **可扩展性 (Extensibility)：** 系统能否轻松增加新的模块，比如支持视频抓取或 PDF 解析？

---

### 2. 系统核心组件与设计 (Architecture)

一个标准的 Web Crawler 包含以下关键组件：

1. **Seed URLs：** 爬虫开始的起点。
2. **URL Frontier (URL 队列)：** 存储待爬取的 URL，是整个系统的核心调度器。它负责优先级排序和礼貌性控制。
3. **HTML Downloader：** 从互联网下载网页。
4. **DNS Resolver：** 将 URL 转换为 IP 地址。
5. **Content Parser：** 解析网页，验证格式。
6. **Content Seen? (内容去重)：** 通常使用 **Hash 或 SimHash** 算法来比较内容的指纹，避免存储重复网页。
7. **Link Extractor：** 从当前网页提取新的链接。
8. **URL Filter：** 过滤掉黑名单、特定后缀或不需要的 URL。
9. **URL Seen? (URL 去重)：** 使用 **Bloom Filter (布隆过滤器)** 或哈希表存储已爬取的 URL。

---

### 3. 值得注意的地方 (Key Noteworthy Aspects)

* **礼貌性 (Politeness) 策略：**
* 维护一个从域名到 `Download Worker` 的映射。
* 同一个域名的请求应发送到同一个 FIFO 队列，并设置两次下载间的延迟（Delay）。


* **优先级 (Priority)：**
* 根据网页权重（如 PageRank）、更新频率、流量来决定哪些 URL 先爬。


* **新鲜度 (Freshness)：**
* 网页会更新，需要定期重新爬取。策略包括：仅重新爬取重要页面、根据更新频率动态调整爬取周期。


* **分布式抓取：**
* 将 URL 分发到多台下载服务器（Downloader Nodes），利用一致性哈希（Consistent Hashing）或简单的哈希取模来分配负载。



---

### 4. 常见陷阱与误区 (Common Pitfalls)

1. **陷入“蜘蛛陷阱” (Spider Traps)：** 某些网站会生成无限层级的目录（如 `a/b/c/a/b/c...`）。
* *解决方法：* 设置最大 URL 长度，或监控重复的 URL 模式。


2. **性能瓶颈：DNS 查询。** 频繁请求 DNS 会导致同步阻塞。
* *解决方法：* 使用 **DNS 缓存**，或维护本地 DNS 解析器。


3. **内存溢出：** 处理亿级 URL 去重时，传统的 Hash Set 会撑爆内存。
* *解决方法：* 必须提到 **Bloom Filter**。


4. **HTML 解析失败：** 互联网上有很多格式不规范的 HTML。
* *解决方法：* 使用容错性强的库（如 BeautifulSoup），并设置超时。



---

### 5. 面试 Q&A 整理 (Preparation)

**Q: 如何处理海量网页的去重？**
**A:** 使用 **Bloom Filter**。它在空间利用率上极高，虽然有极小的误报率（可能会漏掉极少数没爬过的网页），但在面试中这是一个完美的权衡方案。

**Q: 爬虫如何遵守 robots.txt？**
**A:** 在抓取一个域名之前，先下载其 `robots.txt` 并缓存。下载器在发出请求前必须检查该文件的规则。

**Q: 如果一个下载节点宕机了怎么办？**
**A:** 系统应采用分布式队列（如 Kafka/RabbitMQ）来维护 URL Frontier。如果一个节点崩溃，未完成的任务会自动退回队列或由其他节点接管。

---

### 6. Python 示例代码

在面试中，如果要求写代码，通常会考察**异步并发**或**简单的多线程**。以下是一个使用 `asyncio` 和 `aiohttp` 的异步爬虫框架示例，它模拟了简单的去重和下载逻辑。

```python
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

class SimpleCrawler:
    def __init__(self, seeds, max_concurrency=5):
        self.queue = asyncio.Queue()  # URL Frontier
        self.visited = set()          # URL Seen (简单示例用set，实际应为Bloom Filter)
        self.max_concurrency = max_concurrency
        
        for url in seeds:
            self.queue.put_nowait(url)

    async def fetch(self, session, url):
        """下载网页并提取链接"""
        try:
            # 模拟 Politeness: 可以在这里添加 time.sleep 或按域名限流
            async with session.get(url, timeout=10) as response:
                if response.status == 200:
                    html = await response.text()
                    return html
        except Exception as e:
            print(f"Error fetching {url}: {e}")
        return None

    def extract_links(self, html, base_url):
        """解析并提取链接 (Content Parser & Link Extractor)"""
        soup = BeautifulSoup(html, 'html.parser')
        links = []
        for anchor in soup.find_all('a', href=True):
            full_url = urljoin(base_url, anchor['href'])
            # 简单的 URL 过滤
            if urlparse(full_url).netloc == urlparse(base_url).netloc:
                links.append(full_url)
        return links

    async def worker(self):
        """爬虫工作协程"""
        async with aiohttp.ClientSession() as session:
            while True:
                url = await self.queue.get()
                
                if url in self.visited:
                    self.queue.task_done()
                    continue
                
                print(f"Crawling: {url}")
                self.visited.add(url)
                
                html = await self.fetch(session, url)
                if html:
                    new_links = self.extract_links(html, url)
                    for link in new_links:
                        if link not in self.visited:
                            await self.queue.put(link)
                
                self.queue.task_done()

    async def run(self):
        # 启动多个并发 worker
        workers = [asyncio.create_task(self.worker()) for _ in range(self.max_concurrency)]
        await self.queue.join()  # 等待队列处理完毕
        
        for w in workers:
            w.cancel()

if __name__ == "__main__":
    seeds = ["https://example.com"]
    crawler = SimpleCrawler(seeds)
    asyncio.run(crawler.run())

```

### 总结建议：

1. **图表优先：** 面试时先画出 `URL Frontier` 到 `Downloader` 再到 `Storage` 的流程图。
2. **权衡讨论：** 重点讨论 DFS vs BFS。爬虫通常使用 **BFS**，因为它能控制深度且更容易管理“礼貌性”。
3. **监控与告警：** 提到你会监控爬虫的健康状况、抓取成功率以及对目标服务器的压力。
