免費完整指南

網頁爬蟲

設計網頁爬蟲。請涵蓋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...

00

練習檢查點

面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。

  1. 01
    釐清範圍
  2. 02
    需求 + 規模
  3. 03
    API + 資料模型
  4. 04
    畫出架構
  5. 05
    深入探討
  6. 06
    取捨決策
01

形塑設計的需求

不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。

功能需求

01這場爬取是為了什麼——搜尋索引、LLM 訓練資料,還是封存?它決定了規模與新鮮度。

從種子開始抓取頁面,並儲存原始內容供下游使用——由消費端定義什麼叫「完成」。

02爬取如何成長——抽取出的連結會餵回來嗎?

每一個抓取到的頁面都會被解析出連結,經過過濾後再餵回 frontier——這場爬取靠種子自我維持。

03禮貌抓取是硬性需求,還是有的話更好?

這是一項硬性需求:遵守 robots.txt(含 crawl-delay),透過 user-agent 誠實表明身分,並且永遠不讓任何單一主機過載。

04營運者可以在爬取途中注入種子與優先 URL 嗎?

可以。維運人員可以用高優先權把 URL 推進來,排在爬蟲自己找到的連結前面。一場爬取可以被引導,而不只是放著跑。

05我們到底要儲存什麼——原始位元組、解析後的文字,還是兩者都要?

兩者都留。我們把原始內容存在 blob storage,加上抽取出的文字與 metadata。之後要重新處理,絕不能等於再抓一次頁面。

06robots.txt 必須多新?

數小時內要新鮮:絕不能拿超過幾小時前的 robots.txt 規則去爬一台主機——依過時的允許規則行事是一種合規風險。

範圍外搜尋排名與索引(爬蟲產出語料庫,而不是索引) · 動態頁面的 JavaScript 渲染 · 為了新鮮度的持續重爬(先做一次完整爬取)

非功能需求

01多少頁面,多快?

5 天內爬完 100 億頁——這一句話就決定了機隊規模與佇列吞吐量。

02什麼保護我們所爬的網站?

無論積壓多少都要遵守每台主機的 crawl-delay:為單一網站排隊的一百萬個 URL 必須緩慢排出,絕不能變成洪流。

03同一個頁面常常活在許多 URL 底下——鏡像、追蹤參數、www 變體。語料庫該讓每個頁面只留一份嗎?

絕不抓同一個 URL 兩次,而經由不同 URL 抵達的同一個頁面,在語料庫裡只能落地一次。

04如果一台機器在爬取途中當機,它手上的 URL 可以丟掉嗎,還是每個 URL 最終都必須被交代清楚?

什麼都不會無聲地遺失:每個 URL 最終要嘛被抓取、要嘛被明確記錄為失敗——一台機器當機絕不能讓工作消失。

05無限的 URL 空間怎麼辦?

深度上限、每網域的頁面預算,以及 URL 樣式過濾器,讓行事曆頁面與廣告農場不會吃掉整場爬取。

持續追問 —— 面試是一場對話

真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。

  • 抓下來的語料庫必須保留多久,我們必須遵從網站擁有者的下架或刪除請求嗎?
  • 這場爬取有頻寬或金錢預算嗎,還是 5 天期限是唯一的限制?
  • 我們需不需要一條「何時抓了什麼」的稽核軌跡——足以在網站擁有者抱怨時證明我們遵守了 robots.txt?
  • 我們要渲染 JavaScript,還是只抓原始 HTML?
  • 一次完整爬取就夠了,還是語料庫需要持續更新?
02

逼出架構決策的數字

把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。

01

所需抓取速率

100 億頁 ÷ 5 天10,000,000,000 ÷ 432,000 秒 ≈ 持續 23K pages/s

這是一個機隊,而不是一台伺服器——而且 frontier 每秒要派出 23K 個 URL,同時遵守禮貌抓取(不對同一台主機猛打)。

02

機隊規模

每頁平均抓取延遲約 2 秒,所以每條連線約 0.5 pages/s · 每台機器約 4K 條有效並發連線23K ÷ (4,000 × 0.5) ≈ 持續 12 台機器——為重試與慢速主機預留約 2 倍

二十幾台抓取器就能趕上期限;瓶頸在禮貌抓取,而不是運算力。

03

URL-seen 記憶體

100 億個 URL 放進一個 1% 偽陽性的 Bloom filter(每個約 10 bits)10B × 10 b ≈ 12 GB——相對於精確字串的約 400 GB

重要的是 Bloom 給的「肯定是新的」這個答案。一個 false positive 只是跳過一個 URL。false negative 永遠不會發生,所以沒有東西會被爬兩次。

04

語料庫的儲存

100 億頁 × 每頁約 100 KB 的已儲存 HTML——常被引用的數 MB 頁面重量大多是這個爬蟲從不抓取的圖片與腳本≈ 1 PB 原始資料

帶壓縮的 blob 儲存;中繼資料(雜湊、URL)留在一個獨立的儲存中保持可查詢。

05

DNS 壓力

23K fetches/s,每一次都需要解析沒有快取:23K lookups/s · 有每抓取器的 DNS 快取:每個新主機約 1 次

機隊內的 DNS 快取是必備的——公共解析器會把這場爬取限速到死。

決策範例

數字

每秒兩萬三千個頁面,連續五天。但真正的限制走的是反方向:任何單一網站感受到的流量,永遠不能超過一點點涓流。

我的選擇

我會把 frontier 分成兩個階段。front queue 依優先權排序 URL;back queue 給每個 host 自己的佇列,每個 host 配一個尊重 crawl-delay 的 token bucket。fetcher 租用 URL 而不是刪掉它們,成功就 ack,並讓租約逾時來從崩潰中復原。URL 在入列之前先過一道 Bloom filter 的看過檢查,抓完之後再用 content hash 抓出透過不同 URL 到達的同一個頁面。robots 規則依網域快取並帶 TTL,DNS 在機隊內部有自己的一份快取。

避免

我不會做的事:用一個全域的優先權佇列。一個大站一口氣倒進一百萬個 URL 的那一刻,fetcher 就猛捶那個 host,爬取就變成一場 DDoS。我也不會用一張精確的資料庫表來追蹤看過的 URL。那意味著 400 GB 的字串,加上每次入列都要查一次,而一個 12 GB 的 Bloom filter 免費就回答「肯定是新的」。它罕見的 false positive 只不過是跳過一個 URL,而那是偏安全的那個方向。

何時改變

如果語料庫需要的是持續的新鮮度而不是一次爬取,frontier 就會多出一個重爬排程器:頁面依變更頻率與重要性重新入列,而 seen 過濾器切換成一種支援老化淘汰的結構。

03

架構路徑

先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。

完整全貌

總覽 —— 每個元件

Seed URLsURLFrontier(兩階段)Fetcher FleetParser +ExtractorContent Store(blob)DNS Cache +robots.txt每主機規則URL Seen (Bloom)肯定是新的?Content HashDedup同一頁面、其他 URL
  • 抽取出的連結從解析器循環回到 frontier——這場爬取靠自己的發現餵養自己。
  • 在 frontier 內部,每台主機都有自己的佇列,URL 只以那台主機被允許的節奏發出。
  • 禮貌抓取內建在發放 URL 的結構裡——沒有任何一台抓取器能灌爆一個網站,就算是不小心也不行。

路徑 1

抓取路徑——禮貌抓取寫進架構裡

Frontier(每主機佇列)Token BucketFetcher(租用)Fetch + robots檢查Content Store

只有當一個 URL 的主機有 token 時它才會被發出;當機時租約回到佇列,重試會退避,而反覆失敗會落進一個 dead-letter 佇列。

路徑 2

發現路徑——餵養自己的迴圈

ParserLink ExtractorURLFilter(樣式、深度)URL Seen (Bloom)Frontier enqueue

每一頁都產出連結;過濾器丟掉陷阱與垃圾,Bloom filter 丟掉所有已經看過的,而存活下來的重新進入 frontier。

04

API 與資料模型

在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。

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。

核心實體

CrawlTask

url · domain · depth · status: queued/leased/done/failed · retries

工作的單位;在抓取器處理它時是被租用(而非刪除)的,所以當機能自我修復。

DomainState

domain (PK) · robots_rules · crawl_delay · last_fetch_at · pages_crawled

robots 規則依網域帶 TTL 快取;token bucket 就住在這裡。

Page

url · content_hash (SHA-256) · fetched_at · blob_ref

content_hash 驅動第二層去重——不同 URL 下的相同內容只儲存一次。

05

深入方向

在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。

重點

一百萬個 URL,一個小網站

frontier 為一個不起眼的網站握著一百萬個 URL。是什麼保證它永遠不會被猛打?

回答

那些 URL 全部進同一個以該 host 為單位的佇列。一個綁著網站 crawl-delay 的 token bucket 一次放行一個,fetcher 只能拿到 bucket 允許的量。加一千台機器,那個 host 看到的還是同樣的細流——是佇列在強制,不是靠善意。

避免

在抓取器內部做速率限制——禮貌抓取必須是結構性的,放在每主機佇列裡,否則一個橫向擴展的機隊就會把它破壞掉。

重點

我看過這個 URL 嗎

一百億個 URL——你如何在記憶體中對每次入列回答「以前看過嗎?」,而在這裡一次 Bloom 偽陽性的代價是什麼?

回答

每次入列前先查 Bloom filter。100 億個 URL 每個約 10 bits,大約塞進 12 GB,而存精確字串要 400 GB。它永遠不會給偽陰性,所以沒有東西會被爬兩次。偶爾的偽陽性頂多跳過一個 URL——這是漏掉時比較安全的方向。

避免

把偽陽性當成錯誤——跳過一個 URL 是設計好的代價;爬兩次才是失敗。

重點

同一頁面,不同 URL

鏡像、追蹤參數,以及 www/非 www 都提供一模一樣的內容。第二層去重坐在哪裡,用什麼 key?

回答

第二層在抓取之後,以內容雜湊為 key。對頁面主體做雜湊,再到一個「看過的雜湊」儲存區裡查。命中的話,保留一份正規副本,把另一個 URL 記成別名。正規化處理追蹤參數和 www 的變體;只有雜湊抓得到來自不同 host 的相同頁面。

避免

只靠 URL 正規化——只有內容雜湊才抓得到跨主機的真正重複。

重點

無限的行事曆

一個網站永無止境地產生有效的「下個月」連結。什麼為爬取設界,你又如何偵測這個陷阱?

回答

三道界線疊起來:一個深度上限、一個每網域的頁數預算,還有標記機器生成形狀(像不斷遞增的日期)的 URL 樣式過濾。真正在偵測的是那個預算。當一個小網域燒穿數千個幾乎一樣的頁面,它就會被限流或切斷——所以沒有任何單一陷阱能吃掉整個爬取。

避免

只靠深度——每網域的預算與 URL 樣式啟發法必須撐住它。

重點

一台抓取器握著 4,000 個 URL 時掛掉

一台機器在抓取途中當機。它進行中的工作會怎樣,復原的代價是什麼?

回答

什麼都不會遺失,因為每個 URL 是被租用,不是被刪掉。當死掉的機器停止 ack,它那 4,000 個租約會逾時,URL 就回到佇列給其他 fetcher。復原的代價只有租約逾時的延遲,加上重抓那些正在處理中的——有界而且便宜。

避免

在發出時就把 URL 從佇列刪掉——應該用帶逾時的租約,完成時再 ack。

準備好練習了嗎?

把 網頁爬蟲 大聲講一遍,讓 AI 為你的說明評分。

用 AI 練習這題 →