- 原文地址: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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。