← Courses Catalogue

#设计一个网页爬虫

注意:这个文档中的链接会直接指向系统设计主题索引中的有关部分,以避免重复的内容。你可以参考链接的相关内容,来了解其总的要点、方案的权衡取舍以及可选的替代方案。

#第一步:简述用例与约束条件

把所有需要的东西聚集在一起,审视问题。不停的提问,以至于我们可以明确使用场景和约束。讨论假设。

我们将在没有面试官明确说明问题的情况下,自己定义一些用例以及限制条件。

#用例

#我们把问题限定在仅处理以下用例的范围中

#无需考虑

#限制条件与假设

#提出假设

用更传统的系统来练习 —— 不要使用 solrnutch 之类的现成系统。

#计算用量

如果你需要进行粗略的用量计算,请向你的面试官说明。

简便换算指南:

#第二步: 概要设计

列出所有重要组件以规划概要设计。

Imgur

#第三步:设计核心组件

对每一个核心组件进行详细深入的分析。

#用例:爬虫服务抓取一系列网页

假设我们有一个初始列表 links_to_crawl(待抓取链接),它最初基于网站整体的知名度来排序。当然如果这个假设不合理,我们可以使用 YahooDMOZ 等知名门户网站作为种子链接来进行扩散 。

我们将用表 crawled_links (已抓取链接 )来记录已经处理过的链接以及相应的页面签名。

我们可以将 links_to_crawlcrawled_links 记录在键-值型 NoSQL 数据库中。对于 crawled_links 中已排序的链接,我们可以使用 Redis 的有序集合来维护网页链接的排名。我们应当在 选择 SQL 还是 NoSQL 的问题上,讨论有关使用场景以及利弊

向面试官了解你需要写多少代码

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爬虫服务的主类,由PagePagesDataStore 组成。

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)

#处理重复内容

我们要谨防网页爬虫陷入死循环,这通常会发生在爬虫路径中存在环的情况。

向面试官了解你需要写多少代码.

删除重复链接:

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)

#第四步:架构扩展

根据限制条件,找到并解决瓶颈。

Imgur

重要提示:不要直接从最初设计跳到最终设计!

现在你要 1) 基准测试、负载测试。2) 分析、描述性能瓶颈。3) 在解决瓶颈问题的同时,评估替代方案、权衡利弊。4) 重复以上步骤。请阅读设计一个系统,并将其扩大到为数以百万计的 AWS 用户服务 来了解如何逐步扩大初始设计。

讨论初始设计可能遇到的瓶颈及相关解决方案是很重要的。例如加上一套配备多台 Web 服务器负载均衡器是否能够解决问题?CDN呢?主从复制呢?它们各自的替代方案和需要权衡的利弊又有哪些呢?

我们将会介绍一些组件来完成设计,并解决架构规模扩张问题。内置的负载均衡器将不做讨论以节省篇幅。

为了避免重复讨论,请参考系统设计主题索引相关部分来了解其要点、方案的权衡取舍以及替代方案。

有些搜索词非常热门,有些则非常冷门。热门的搜索词可以通过诸如 Redis 或者 Memcached 之类的内存缓存来缩短响应时间,避免倒排索引服务以及文档服务过载。内存缓存同样适用于流量分布不均匀以及流量短时高峰问题。从内存中读取 1 MB 连续数据大约需要 250 微秒,而从 SSD 读取同样大小的数据要花费 4 倍的时间,从机械硬盘读取需要花费 80 倍以上的时间。1

以下是优化爬虫服务的其他建议:

#其它要点

是否深入这些额外的主题,取决于你的问题范围和剩下的时间。

#SQL 扩展模式

#NoSQL

#缓存

#异步与微服务

#通信

#安全性

请参阅安全

#延迟数值

请参阅每个程序员都应该知道的延迟数

#持续探讨