Apache Nutch 是 Apache Software Foundation 旗下的爬蟲,開發始於 2004 年。它是一個建立在 Hadoop 上的 JVM 系統,運作方式不是單一串流命令,而是以迴圈為核心:先把種子 URL 透過 inject 加入持久化資料庫,接著重複執行 generate → fetch → parse → updatedb,並透過外掛機制處理 protocol、parser、URL filter 和 scoring。它的典型輸出不是 CSV,而是送進 Solr 或 Elasticsearch 這類搜尋索引。
我將 Nutch 1.22 跑在一個可控的本地測試站上——這個站會在伺服器端記錄每一筆請求,因此評估結果看的是伺服器實際收到什麼,而不是爬蟲自稱做了什麼。這次執行受到四道邊界影響:JDK 版本、http.agent.name、爬取範圍,以及 plugin.includes 裡是否包含 parse-js。在測試設定下,整個流程反覆跑了多次;只要更換 JDK,或不設定 agent 身分,流程就會在抓到有用頁面之前停止。
JDK 的限制會在任何爬取開始前就先攔下來。Nutch 1.22 在 JDK 26.0.1 上無法啟動:第一個 Hadoop 工作在 Subject.getSubject() 內部失敗,原因是 Java 已移除了 SecurityManager 路徑。Nutch 內建的是 Hadoop 3.4.2,而修正是在 Nutch 1.22 發布七天後才進入 Hadoop 3.4.3。另一方面,parse-js 讓兩個 JavaScript 檔中的字串常值恢復率從 0/2 提升到 2/2,而且不需要真的執行瀏覽器。
Nutch 是拿來做什麼的,不是拿來做什麼的
Nutch 不是一般意義上的資料擷取器。結構化欄位抽取不是它的工作重點:它的任務是大規模發現與抓取 URL,維護這些 URL 及其狀態的持久化資料庫(crawldb),然後把 segment 交給別的工具去建立索引。若你把它對準一個目錄頁,期待拿到名稱與價格表格,最後得到的會是 crawldb,而不是表格。
這個架構也解釋了後面大部分現象。Nutch 比單一二進位爬蟲時代早了大約二十年,它是為 Hadoop 原本要解決的問題而生:爬取超過單機可容納規模的頁面。把它丟到筆電上,對一個只有 12 頁的測試樣本做抓取,就像租一台貨運火車搬書架——可以看出火車有什麼本事,但不該期待它有腳踏車般的輕巧手感。
目前版本是 1.22,已於 2026 年 2 月 17 日發布。它採用 Apache-2.0 授權;我在 2026 年 7 月 27 日查看時,GitHub repo 有 3,272 顆星、8 個開啟中的 issue,而 master 分支在那四天前仍有提交。這是一個仍在維護的專案,不是被放棄的專案——因此 JDK 問題應該被理解為發行窗口稍微早了一週,而不是缺乏維護。
版本矩陣:JDK 24+、Hadoop 3.4.2,以及一個兩行修正
阻礙來自三方版本互動,而你唯一能控制的是 Nutch 跑在哪個 JDK 上。最先跑的 Hadoop 工作,在主機預設 JDK 下就死在初始化階段:
java.lang.UnsupportedOperationException: getSubject is not supported
at java.base/javax.security.auth.Subject.getSubject(Subject.java:277)
at org.apache.hadoop.security.UserGroupInformation.getCurrentUser(UserGroupInformation.java:588)
at org.apache.nutch.crawl.Injector.inject(Injector.java:473)
回傳碼 255。沒有抓到任何頁面。bin/nutch inject 甚至還沒碰到網路——它先初始化 Hadoop 的 LocalJobRunner,然後去問目前使用者是誰,接著呼叫 Subject.getSubject(),而 JEP 486 在 JDK 24 永久移除 SecurityManager 之後,把這個呼叫變成了無條件例外。我的主機 JDK 是 OpenJDK 26.0.1,早已跨過這條線。
傳統的迴避方式也行不通。加入 -Djava.security.manager=allow——過去用來恢復舊行為的旗標——會在 Nutch 程式碼載入前就被 VM 拒絕:
Error occurred during initialization of VM
java.lang.Error: A command line option has attempted to allow or enable the Security
Manager. Enabling a Security Manager is not supported.
這是回傳碼 1,而且設計上就是死路一條——因為功能已經被移除,旗標也一起沒了。
根因在於 Nutch 1.22 內建的 Hadoop 版本。getSubject 問題對應的是 HADOOP-19212,並已在 Hadoop 3.4.3 與 3.5.0 修正;但 Nutch 1.22 內含的是 hadoop-common-3.4.2。Nutch 1.22 在 2026 年 2 月 17 日發布,而 Hadoop 3.4.3 大約一週後才跟上。
這也不是 Solr 或 Hadoop 叢集依賴問題。 常見誤解是 Nutch 非得依賴 Hadoop 叢集與正在運作的 Solr 才能工作。其實不是。local mode 會使用 Hadoop 內建的 LocalJobRunner,不需要 HDFS daemon、不需要 YARN,也不需要叢集。整個 inject → generate → fetch → parse → updatedb 流程都能在單機完成,不必另外安裝任何東西。JDK 這道牆完全是內建函式庫版本的問題,它會在你開始討論基礎設施前就把你擋住。
實測版本矩陣如下,三列都量過:
| 使用的 JDK | 指令 | 結果 |
|---|---|---|
| OpenJDK 26.0.1 | bin/nutch inject | 失敗,rc=255 — UnsupportedOperationException: getSubject is not supported |
| OpenJDK 26.0.1 | bin/nutch inject + -Djava.security.manager=allow | 失敗,rc=1 — VM 拒絕啟動 |
| OpenJDK 17.0.20(LTS) | bin/nutch inject | 成功,rc=0 — Total new urls injected: 1 |
修正方式只需要兩個指令:安裝 LTS JDK,然後讓 Nutch 指向它:
brew install openjdk@17
export NUTCH_JAVA_HOME=/opt/homebrew/opt/openjdk@17
這是 keg-only,不會碰到系統預設值。Nutch 自己的 CI 也是以 Java 17 為目標,而專案也已公開說明 1.22 是最後一個可在 Java 11 上執行的版本,1.23 起將要求 Java 17。所以 LTS JDK 不是臨時補丁,而是受支援的設定。這個不匹配,說穿了是 Nutch 支援的版本與 2026 年 brew install openjdk 預設給你的版本不同,而這兩件事剛好在第一個指令就撞上了。
以下內容都在 OpenJDK 17.0.20 上執行,因此整個流程是乾淨的。
以數據看安裝:396 MB,以及一個會擋住一切的屬性
大家一講到這類工具,最常用的形容詞就是「重量級」,但沒有實際測量就沒有意義。以下是 Nutch 1.22 二進位套件解壓後的真實內容:
| 項目 | Nutch 1.22 二進位套件 |
|---|---|
| 解壓後大小 | 約 396 MB |
lib/ 裡的 jars | 188(約 113 MB) |
| — 其中 bundled Hadoop stack | 13 |
| 外掛目錄 | 78 |
| 這些外掛目錄中的 jars | 533 |
| 設定檔 | 35 |
bin/ 裡的腳本 | 2 — crawl 和 nutch |
若要比對規模,像 katana 這類現代 Go 爬蟲通常就是一個約 50 MB 的單一 binary,沒有 JVM,也不需要外部 jars。
接著是很多人不會提醒你的那一道門檻。安裝包附帶的 nutch-site.xml 是空的,而 http.agent.name 預設又是空字串。若不設定它,我的第一次爬取抓到的是 0 個路徑,並記錄了:
ERROR Fetcher: No agents listed in 'http.agent.name' property.
只要設定這一個屬性——其他都不改——爬取就能正常進行。當屬性保持空白時,指令會結束但不抓頁面,日誌會如上顯示 agent-name 錯誤;它不是安靜失敗。
最低可用設定最後證明只需要三個東西:conf/nutch-site.xml(agent 名稱、外掛集合、scope)、conf/regex-urlfilter.txt(主機範圍限制),以及一個 seed URL 檔案。這個數量不算糟,畢竟只是比 crawler run <url> 多了三個檔案而已。
它找到了什麼:真正重要的外掛切換

測試站刻意設計了三種不同類型的端點,而 Nutch 的行為也清楚地分成三類:
- Class A — 一般 HTML 連結(4 個頁面,外加一條 3 層深的鏈)
- Class B — 只存在於某個被連結的 JavaScript 檔字串常值中的端點:一個是呼叫參數,
fetch('/api/js-endpoint-7'),另一個是指定值,const other = "/api/js-endpoint-8" - Class C — 必須等 JavaScript 執行後把端點注入 DOM 才會出現的端點
根據伺服器端 hit log,重複三次的結果如下:
| 外掛設定 | Class A(HTML 連結) | Class B(JS 檔字串常值) | Class C(執行期 DOM) |
|---|---|---|---|
預設安裝 — parse-(html|tika) | 4/4(召回率 1.0) | 0/2(召回率 0.0) | 未達到 |
加入 parse-js — parse-(html|tika|js) | 4/4(召回率 1.0) | 2/2(召回率 1.0) | 未達到 |
三次重複完全一致,結果可重現。
Class B 的提升常被低估。Nutch 在不執行瀏覽器的情況下,就找到兩個藏在 JavaScript 檔中的端點,靠的是 parse-js 外掛對 JavaScript 內容的 regex 掃描。app.js 這個檔案在兩種設定下都會被抓下來——因為 Nutch 會把 <script src> 視為 outlink——所以差別完全在於有沒有人去讀檔案內容並找出 URL 形狀的字串。啟用外掛後,兩種字串形式都能被抓到。
在這個樣本上,Nutch 的預設設定與 katana 的標準模式都能達到相同的 Class A 範圍;而 Nutch 加上 parse-js、以及 katana 加上 -jc,則能在不靠瀏覽器的情況下覆蓋 A 與 B 類。這篇文章沒有記錄 katana 的版本與完整命令,因此那個結果更像是情境參考,而不是嚴格的產品基準比較。
Class C 則是誠實的上限。任何靜態外掛組合都無法到達它,這完全符合預期:若端點只有在腳本執行後才存在,就必須真的執行腳本。我也確實試過把 protocol-http 換成 protocol-htmlunit,也就是 Nutch 純 Java 的 JS 執行 protocol。它能載入並執行而不當機,但在同一個四輪測試框架裡,它只完成了一輪,只抓到 seed page 和 app.js,A/B/C 都沒碰到,第二輪就報告 0 records selected for fetching。這代表 probe 設定不足,不是 HtmlUnit 能力的定論。它能證明的範圍較窄:把 JS 執行型 protocol 換進去,不是直接替換就能生效的事,而且我測到的每一種設定裡,Class C 都仍然無法到達。
爬取控制與失敗行為
深度不是一個旗標。Nutch 沒有 --depth 3;深度就是你跑了幾輪 generate → fetch → parse → updatedb,因為第 R 輪會抓取第 R-1 輪發現的 frontier。我的深度鏈也精準證實了這件事:
| 執行輪數 | 最深到達的路徑 |
|---|---|
| 2 | /depth/1 |
| 3 | /depth/2 |
| 4 | /depth/3 |
這個機制乾淨、機械化,但也代表深度是你在腳本裡自己控制的迴圈次數,而不是參數。
接下來是陷阱。Nutch 安裝包的預設值是 db.ignore.external.links=false,再搭配寬鬆的 +. URL filter——意思是 預設的 Nutch 爬取會跟著種子主機以外的連結走。我讓一個頁面同時連到一個範圍內路徑與一個不同 hostname 的連結,結果爬蟲真的抓了外部主機。兩個獨立訊號都一致:Nutch 自己的 crawldb 將它標成 db_fetched,而另一台主機的伺服器計數也確實記錄到 hit。
若要限制在範圍內,必須主動設定,而且兩種方法都確實有效:
| 設定 | crawldb 中的外部主機 | 外部主機伺服器 hit | 是否受控? |
|---|---|---|---|
db.ignore.external.links=false(安裝預設) | db_fetched | +1 | 否 |
db.ignore.external.links=true | 不存在 | 0 | 是 |
regex-urlfilter.txt 中的主機規則(+^http://127.0.0.1: 再接 -.) | 不存在 | 0 | 是 |
如果你只爬一個站,請在第一次正式執行前就先設定其中一種。方法上補充一句:這個測試對本機伺服器負載很敏感,因此這三列來自一個沒有其他程序碰觸 fixture 的單次執行。行為本身機械上非常清楚,並由兩個獨立訊號佐證;但這些列值是一次乾淨執行的結果,不是多次平均。
Sitemap 則是另一個步驟。禮貌機制是有的——正常爬取會先抓 /robots.txt——但 sitemap 本身得用自己的指令處理:
| 方法 | 是否請求 /sitemap.xml? | 只存在於 sitemap 的端點 |
|---|---|---|
| 一般爬取 | 從未請求 | 0/2 |
明確執行 bin/nutch sitemap 並對 crawldb 操作 | 有抓取 | 2/2 條目注入,完整召回 |
katana 的內建 -kf known-files 模式,使用相同樣本、在 IP 主機上執行 | 未記錄 | 0/2 |
這和那些會在爬取過程中直接順手抓 known files 的工具採用不同模型,但它多了一道指令,卻能完整完成工作。
另外兩項較小的行為也都表現正常。
錯誤處理: 爬取一個連到 500 與 404 的頁面時,整個流程照常完成所有輪次,仍然抓到全部四個 Class A 頁面,而且兩種失敗都被清楚記錄:
| 頁面連到的失敗回應 | 記錄到的 crawldb 狀態 |
|---|---|
| 500 | db_unfetched(可重試) |
| 404 | db_gone |
沒有任何東西被拖垮。
禮貌機制: 在每個 queue 只用一條執行緒時,同一主機兩次抓取之間的間隔,與設定值相符:
fetcher.server.delay | 同主機抓取間隔中位數 |
|---|---|
| 1.0 秒 | 1.009 秒(最小 1.006 秒) |
| 0.0 | 0.002 秒 |
這個旋鈕的作用正如字面所示。安裝包預設值是 5.0 秒,這相當保守,而且老實說,對一個設計來抓別人伺服器的工具來說也很合理。
以秒計算的批次成本
Nutch 的每個指令都是全新的 JVM。這個單一事實,比抓取本身更主導它的時間輪廓。
| 階段(每輪) | 中位秒數 |
|---|---|
inject(一次) | 1.81 |
generate | 3.93 |
fetch | 2.82 |
parse | 1.78 |
updatedb | 1.81 |
| 完整一輪 | 12.14 |
每個工作實際上的最低成本——也就是 JVM 啟動加上 Hadoop 初始化,以最便宜的「幾乎沒做什麼」階段來量——大約是 1.77 秒。把這個成本乘上每輪四個指令,再加上最初的 inject,整體爬取圖景就會變成這樣:
| 工具 | 12 頁樣本的 4 層深度爬取 | 行程數 |
|---|---|---|
| Nutch | 大約 45 秒(我在兩種設定下量到 45.8 秒與 45.0 秒) | 約 17 次 JVM 啟動,其中幾乎沒有一次真的在做網路工作 |
katana standard 模式,同樣樣本 | 約 13 秒 | 一個 process |
這個差距不是抓取吞吐量造成的;兩個工具抓到的頁面數量其實差不多。關鍵在架構。Nutch 每個階段都要付出固定的 process 成本,因為這些階段本來就是設計成 MapReduce 工作。對一個很小的本機爬取來說,setup 才是主角。理論上,固定成本在更長的工作裡應該會變成較小比例,但這次測試沒有量到 Nutch 與 katana 在什麼規模下會交叉,也沒有測到它們的比例是否會反轉。
優缺點
優點
- 靜態發現可重現:4/4 HTML 類別、3/3 深度鏈,三次重複執行結果一致。
parse-js可在不靠瀏覽器的情況下找回 JavaScript 檔中的字串常值端點(2/2),同時抓到呼叫參數與指定值兩種形式。- 有兩種可驗證的範圍控制方式,可完整封住爬取(
db.ignore.external.links與 hostregex-urlfilter)。 - 透過
bin/nutch sitemap匯入 sitemap,對一般爬取完全漏掉的端點達成 2/2 完整召回。 - 錯誤處理穩健:500 與 404 都會以不同 crawldb 狀態保留,爬取持續進行。
- 在這次本機執行中,觀察到的同主機間隔與設定的 1.0 秒延遲一致;安裝包預設則是 5.0 秒。
- Apache-2.0、持續維護、78 個外掛,以及能跨輪次追蹤每個 URL 狀態的持久化 crawldb。
- 可在 local mode 下運作,不需要叢集、不需要 HDFS,也不需要 Solr。
缺點
- 在 JDK 24 以上無法執行,因為 SecurityManager 移除會踩雷(我在 26.0.1 上量到失敗)——內建的 Hadoop 3.4.2 早於上游修正,而逃逸旗標也已不存在,所以固定鎖定 LTS JDK 不是偏好,而是硬性前提。
- 解壓後約 396 MB、188 個 library jars、78 個外掛目錄、35 個設定檔。
- 每個指令都要新啟動 JVM,因此每個階段都有約 1.77 秒的固定開銷;12 頁的 4 層深度爬取約 45 秒,而相同樣本的單一二進位爬蟲約 13 秒。
- 安裝包的預設會跟到外部主機;若只想留在單一站點,需手動開啟限制。
http.agent.name安裝時是空的,沒設定之前 fetcher 不會跑。- 沒有深度旗標——深度是你自己管理的迴圈次數。
- 執行期 DOM 端點在我測試的所有設定中都到不了,而換上 JS 執行型 protocol 也不是直接替換就能用。
- 我測的是單主機、本機模式、以及小型樣本。分散式/HDFS 模式、Solr 索引、hostdb、resume、以及增量重爬排程都不在這次範圍內——這裡只能視為未測,而不是已被背書。
誰適合用,誰應該直接離開
如果你的難點在於「爬取」本身,Nutch 就很值得。若你正在建立搜尋索引、做大範圍多網域爬取、需要帶有每個 URL 狀態與重試語意的持久化 URL 資料庫,或預期之後會把工作分散到多台機器上,那這就是一套從很多替代方案還不存在之前就一直在解這個問題的基礎設施。外掛系統讓你可以在不 fork 的情況下調整 protocol、parser、filter 與 scoring 行為。禮貌預設也相當保守,表示維護者確實有認真思考如何做個守規矩的爬蟲。
如果你要的是少量頁面的結構化資料,那就離開。Nutch 會抓取、會解析,然後把 crawldb 和 segments 丟給你,接著期待你自己準備 indexer。若你的目標是客戶端渲染的單頁應用,也請離開——我跑的每一種設定裡,Class C 都沒能到達。若你的團隊平常不跑 JVM,也請離開,因為你得把 Java 工具鏈、LTS JDK 鎖定,以及 396 MB 的 jars,一起加進目前根本沒有這些東西的堆疊裡。至於如果工作是「每週一次,把一個網站爬四層深」,那你花在輪次迴圈與設定檔上的時間,可能會比這個工作本身還多。
對多數人在找的「scraper」來說,最後這種情境其實才是常態。這不是在批評 Nutch,而是工具與任務不匹配。如果你想看看更廣的選擇範圍,我們整理的 開源爬蟲總覽 與 最佳網頁爬蟲 GitHub 專案 會更詳細介紹輕量級方案。
替代方案,以及我們自己的堆疊適合放在哪裡
先說最公平的前提:Nutch 免費、採 Apache 授權、自架即可長期使用,沒有每次請求費用。這確實是很大的優勢,而下面任何內容都不會抹掉這點。
相關評測:Browsertrix Crawler 評測。
在開源世界裡,比較重點要看你最在意什麼。如果你想要的是 Python 框架、帶有爬取控制、而且偏向 request-first 的哲學,Scrapy 對許多專案來說是更接近的對照;本文沒有用相同標準去量測它的安裝體積。如果你想要的是一個沒有瀏覽器、體積精簡的 Go 爬蟲,Colly 也是另一種值得評估的形態。如果你的問題不是找 URL,而是把頁面轉成適合 LLM 的內容,Crawl4AI 針對的是不同層級。
像 Thunderbit 這類託管服務,會把 fetching、rendering 與 extraction 包在 API 後面;而 Nutch 則把 crawl state 與基礎設施控制權留在你手上。Thunderbit 沒有在這個樣本上實跑,所以這裡比較的是 ownership 模式,而不是召回率或動態頁面表現的對照。
這個取捨本質上就是控制權與開銷,而且差異很明顯。Nutch 給你完全控制、持久化 crawldb、天生可擴展到叢集,以及零邊際成本——代價是你得接受 JVM、LTS JDK 鎖定、396 MB 的 jars、輪次迴圈,還有自己的索引層。託管 API 則是在第一次呼叫就給你結構化輸出,也不用管基礎設施——代價是 按次計費 與對爬取 frontier 的控制較少。若你的工作是「索引 5,000 萬頁」,Nutch 的模型才是正確答案,API 反而不合理。若你的工作是「在週四前從 200 個產品頁拿到結構化資料」,那就反過來了。
結論
如果你正在做持續性的多網域爬取,而且已經在營運 JVM 基礎設施,Apache Nutch 很值得評估。在這次樣本中,它的靜態發現行為在重複執行時都很穩定,parse-js 找到了兩個字串形式的 JavaScript 端點,失敗項目也會保留在 crawldb 中,而且觀察到的請求間隔與設定延遲一致。
但也要誠實評估進入成本。Nutch 1.22 在這裡無法於 JDK 26.0.1 上執行;我在這篇評測中實際驗證成功的是 OpenJDK 17.0.20,而 Java 21 並未測試。接著你還得設定 http.agent.name、明確決定 scope,並把這次小型本機測試中觀察到的每階段約 1.77 秒固定成本算進去。這個取捨值不值得,取決於爬取持續多久、範圍多大,以及你是否真的需要持久狀態。
試用 Thunderbit 進行網頁資料擷取 Get Started Free
常見問題
為什麼 Apache Nutch 會出現「getSubject is not supported」錯誤?
在 JDK 24 以上, JEP 486 讓 Subject.getSubject() 直接拋出例外,而內建的 Hadoop 3.4.2 仍然會呼叫它。所以第一個 Hadoop 工作會在任何頁面被抓取前就失敗,而過去可用的 -Djava.security.manager=allow 也已經無法再讓 VM 啟動。請改用已驗證的 Java 17 設定並設定 NUTCH_JAVA_HOME;Java 21 可能也支援,但這篇評測沒有在它上面跑完整流程。
Nutch 1.22 應該跑在哪個 Java 版本?
最穩妥的答案是 Java 17——Nutch 自己的 CI 就是以它為目標,而我在 OpenJDK 17.0.20 上測試也運作良好。Java 11 在 1.22 仍然支援,不過專案已宣布 1.23 將要求 Java 17。JDK 24 以上則無法執行。使用 keg-only 的 Homebrew 安裝(brew install openjdk@17)再搭配 NUTCH_JAVA_HOME,可以避免動到系統預設 JDK。
Nutch 能爬 JavaScript 很重的網站嗎?
只能部分做到,而且差異很重要。啟用 parse-js 外掛後,Nutch 可以找到那些只存在於連結的 JavaScript 檔中、以字串常值形式出現的兩個端點——2/2,且不需要瀏覽器。預設外掛組合則找不到它們。但若端點只會在 JavaScript 執行並修改 DOM 後才出現,那在我測試的所有靜態設定中都無法到達;而換成 HtmlUnit protocol 也不是在我的測試裡可直接替換的設定。若是 client-rendered 應用,請預期需要 JS 執行型 protocol 加上實際配置調整,或直接改用其他工具。
Nutch 需要安裝 Hadoop 和 Solr 嗎?
不需要。local mode 會使用 Hadoop 內建的 LocalJobRunner——不需要叢集、不需要 HDFS daemon,也不需要 YARN——整個 inject → generate → fetch → parse → updatedb 流程都能在單機上完成,不必額外安裝其他東西。Solr 通常是索引的落點,但爬取本身不需要它。不過,Hadoop jars 是內建的(13 個,版本 3.4.2),這也正是為什麼 JDK 相容性問題會存在。
我要怎麼阻止 Nutch 去爬別的網站?
要手動設定,因為安裝預設值並不會幫你限制。Nutch 1.22 的 db.ignore.external.links=false 搭配寬鬆的 URL filter,在我的測試中確實會跟著連結到不同主機並將其抓取。你可以在 nutch-site.xml 裡設定 db.ignore.external.links=true,或者在 conf/regex-urlfilter.txt 加上主機規則(例如先寫 +^https://example\.com/,再接 -.)。這兩種方式都能在測試中完整封住爬取,並可由 Nutch 自己的 crawldb 與另一台伺服器的請求日誌交叉驗證。


