- 原文地址:Design a web crawler
- 原文做者:Donne Martin
- 译文出自:掘金翻译计划
- 译者:吃土小2叉
- 校对者:lsvih
注意:这个文档中的连接会直接指向系统设计主题索引中的有关部分,以免重复的内容。你能够参考连接的相关内容,来了解其总的要点、方案的权衡取舍以及可选的替代方案。前端
把全部须要的东西汇集在一块儿,审视问题。不停的提问,以致于咱们能够明确使用场景和约束。讨论假设。react
咱们将在没有面试官明确说明问题的状况下,本身定义一些用例以及限制条件。android
用更传统的系统来练习 —— 不要使用 solr 、nutch 之类的现成系统。 ios
若是你须要进行粗略的用量计算,请向你的面试官说明。git
简便换算指南:程序员
列出全部重要组件以规划概要设计。 github
对每个核心组件进行详细深刻的分析。web
假设咱们有一个初始列表 links_to_crawl
(待抓取连接),它最初基于网站总体的知名度来排序。固然若是这个假设不合理,咱们可使用 Yahoo、DMOZ 等知名门户网站做为种子连接来进行扩散 。 面试
咱们将用表 crawled_links
(已抓取连接 )来记录已经处理过的连接以及相应的页面签名。 redis
咱们能够将 links_to_crawl
和 crawled_links
记录在键-值型 NoSQL 数据库中。对于 crawled_links
中已排序的连接,咱们可使用 Redis 的有序集合来维护网页连接的排名。咱们应当在 选择 SQL 仍是 NoSQL 的问题上,讨论有关使用场景以及利弊 。
crawled_links
中,检查待抓取页面的签名是否与某个已抓取页面的签名类似
links_to_crawl
中删除该连接crawled_links
中插入该连接以及页面签名向面试官了解你须要写多少代码。
PagesDataStore
是爬虫服务中的一个抽象类,它使用 NoSQL 数据库进行存储。
class PagesDataStore(object):
def __init__(self, db);
self.db = db
...
def add_link_to_crawl(self, url):
"""将指定连接加入 `links_to_crawl`。"""
...
def remove_link_to_crawl(self, url):
"""从 `links_to_crawl` 中删除指定连接。"""
...
def reduce_priority_link_to_crawl(self, url)
"""在 `links_to_crawl` 中下降一个连接的优先级以免死循环。"""
...
def extract_max_priority_page(self):
"""返回 `links_to_crawl` 中优先级最高的连接。"""
...
def insert_crawled_link(self, url, signature):
"""将指定连接加入 `crawled_links`。"""
...
def crawled_similar(self, signature):
"""判断待抓取页面的签名是否与某个已抓取页面的签名类似。"""
...复制代码
Page
是爬虫服务的一个抽象类,它封装了网页对象,由页面连接、页面内容、子连接和页面签名构成。
class Page(object):
def __init__(self, url, contents, child_urls, signature):
self.url = url
self.contents = contents
self.child_urls = child_urls
self.signature = signature复制代码
Crawler
是爬虫服务的主类,由Page
和 PagesDataStore
组成。
class Crawler(object):
def __init__(self, data_store, reverse_index_queue, doc_index_queue):
self.data_store = data_store
self.reverse_index_queue = reverse_index_queue
self.doc_index_queue = doc_index_queue
def create_signature(self, page):
"""基于页面连接与内容生成签名。"""
...
def crawl_page(self, page):
for url in page.child_urls:
self.data_store.add_link_to_crawl(url)
page.signature = self.create_signature(page)
self.data_store.remove_link_to_crawl(page.url)
self.data_store.insert_crawled_link(page.url, page.signature)
def crawl(self):
while True:
page = self.data_store.extract_max_priority_page()
if page is None:
break
if self.data_store.crawled_similar(page.signature):
self.data_store.reduce_priority_link_to_crawl(page.url)
else:
self.crawl_page(page)复制代码
咱们要谨防网页爬虫陷入死循环,这一般会发生在爬虫路径中存在环的状况。
向面试官了解你须要写多少代码.
删除重复连接:
sort | unique
的方法。(译注: 先排序,后去重)class RemoveDuplicateUrls(MRJob):
def mapper(self, _, line):
yield line, 1
def reducer(self, key, values):
total = sum(values)
if total == 1:
yield key, total复制代码
比起处理重复内容,检测重复内容更为复杂。咱们能够基于网页内容生成签名,而后对比二者签名的类似度。可能会用到的算法有 Jaccard index 以及 cosine similarity。
要按期从新抓取页面以确保新鲜度。抓取结果应该有个 timestamp
字段记录上一次页面抓取时间。每隔一段时间,好比说 1 周,全部页面都须要更新一次。对于热门网站或是内容频繁更新的网站,爬虫抓取间隔能够缩短。
尽管咱们不会深刻网页数据分析的细节,咱们仍然要作一些数据挖掘工做来肯定一个页面的平均更新时间,而且根据相关的统计数据来决定爬虫的从新抓取频率。
固然咱们也应该根据站长提供的 Robots.txt
来控制爬虫的抓取频率。
咱们使用 REST API 与客户端通讯:
$ curl https://search.com/api/v1/search?query=hello+world复制代码
响应内容:
{
"title": "foo's title",
"snippet": "foo's snippet",
"link": "https://foo.com",
},
{
"title": "bar's title",
"snippet": "bar's snippet",
"link": "https://bar.com",
},
{
"title": "baz's title",
"snippet": "baz's snippet",
"link": "https://baz.com",
},复制代码
对于服务器内部通讯,咱们可使用 远程过程调用协议(RPC)
根据限制条件,找到并解决瓶颈。
重要提示:不要直接从最初设计跳到最终设计!
如今你要 1) 基准测试、负载测试。2) 分析、描述性能瓶颈。3) 在解决瓶颈问题的同时,评估替代方案、权衡利弊。4) 重复以上步骤。请阅读设计一个系统,并将其扩大到为数以百万计的 AWS 用户服务 来了解如何逐步扩大初始设计。
讨论初始设计可能遇到的瓶颈及相关解决方案是很重要的。例如加上一套配备多台 Web 服务器的负载均衡器是否可以解决问题?CDN呢?主从复制呢?它们各自的替代方案和须要权衡的利弊又有哪些呢?
咱们将会介绍一些组件来完成设计,并解决架构规模扩张问题。内置的负载均衡器将不作讨论以节省篇幅。
为了不重复讨论,请参考系统设计主题索引相关部分来了解其要点、方案的权衡取舍以及替代方案。
有些搜索词很是热门,有些则很是冷门。热门的搜索词能够经过诸如 Redis 或者 Memcached 之类的内存缓存来缩短响应时间,避免倒排索引服务以及文档服务过载。内存缓存一样适用于流量分布不均匀以及流量短时高峰问题。从内存中读取 1 MB 连续数据大约须要 250 微秒,而从 SSD 读取一样大小的数据要花费 4 倍的时间,从机械硬盘读取须要花费 80 倍以上的时间。1
如下是优化爬虫服务的其余建议:
是否深刻这些额外的主题,取决于你的问题范围和剩下的时间。
请参阅安全。
请参阅每一个程序员都应该知道的延迟数。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。