Keywords
apache nutch 评测, 网页抓取, 开源, 基准测试
Content
Apache Nutch 是 Apache Software Foundation 旗下的爬虫项目,开发始于 2004 年。它是一个基于 Hadoop 的 JVM 系统,运作方式更像循环流程,而不是单条流式命令:先把种子 URL inject 到持久化数据库中,然后按 generate → fetch → parse → updatedb 的轮次运行,并通过插件槽位处理协议、解析器、URL 过滤和评分。它的常见输出目标是 Solr 或 Elasticsearch 这类搜索索引,而不是 CSV。
我把 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),再把 segments 交给别的工具去生成索引。你如果拿它去抓商品目录、希望直接得到一个姓名和价格表,最后得到的只会是 crawldb。
这个架构也解释了后面的绝大多数现象。Nutch 比单体爬虫时代早了大约二十年,它是为 Hadoop 所解决的问题而生的:抓取超过单机承载能力的网页。把它放在笔记本上,对一个 12 页的测试样本跑起来,就像租一列货运火车去搬书架——这能很好地说明这列火车的能力,但拿自行车的使用感受去要求它就不公平了。
当前版本是 1.22,官方在 2026 年 2 月 17 日 发布。它采用 Apache-2.0 许可证;我在 2026 年 7 月 27 日查看时,仓库有 3,272 个 star,8 个 open issues,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 代码加载之前就被虚拟机拒绝:
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 才能干活。事实并非如此。它的本地模式使用 Hadoop 内置的 LocalJobRunner——没有 HDFS 守护进程,没有 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 — 虚拟机拒绝启动 |
| 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 安装,不会改动系统默认 JDK。Nutch 自己的 CI 也以 Java 17 为目标,而且项目已经 公开说明:1.22 是最后一个还能在 Java 11 上运行的版本,1.23 将要求 Java 17。因此,LTS JDK 不是临时补丁,而是受支持的配置。这里的错位在于:Nutch 支持什么,和 brew install openjdk 在 2026 年默认给你什么,是两回事,而这两件事恰好在第一条命令上撞在一起。
从这里开始,所有测试都运行在 OpenJDK 17.0.20 上,整个流程表现稳定。
真实安装成本:396 MB,以及一个会卡住一切的属性
“很重”是大家最爱用的形容词,但没有量化就没有意义。下面是解压后的 Nutch 1.22 二进制发行包里到底装了些什么:
| 项目 | Nutch 1.22 二进制发行包 |
|---|---|
| 解压后体积 | 约 396 MB |
lib/ 下的 jars | 188 个(约 113 MB) |
| ——其中自带的 Hadoop 组件 | 13 个 |
| 插件目录 | 78 个 |
| 这些插件目录中的 jars | 533 个 |
| 配置文件 | 35 个 |
bin/ 中的脚本 | 2 个 —— crawl 和 nutch |
拿现代 Go 爬虫 katana 来对比,它通常只是一个大约 50 MB 的二进制文件,没有 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(主机范围限制)以及一个种子 URL 文件。数量不算多,只是比 crawler run <url> 多了三个文件。
它找到了什么:真正起作用的插件开关

测试站点故意设置了三类不同的端点,Nutch 的表现也正好沿着这三类清晰分化:
- A 类 —— 普通 HTML 链接(4 个页面,外加一个深度为 3 的链接链)
- B 类 —— 只存在于某个已链接 JavaScript 文件中的字符串字面量端点:一个是函数调用参数,
fetch('/api/js-endpoint-7'),另一个是赋值语句,const other = "/api/js-endpoint-8" - C 类 —— 只有在 JavaScript 运行后才会被注入 DOM 的端点
以下结果来自服务器端命中日志,且重复三次:
| 插件配置 | A 类(HTML 链接) | B 类(JS 文件字面量) | 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) | 未触达 |
三次重复结果完全一致,说明是确定性的。
B 类的提升最容易被低估。Nutch 在不运行浏览器的情况下就找到了这两个埋在 JavaScript 里的端点,靠的是 parse-js 插件对 JavaScript 内容的正则扫描。app.js 文件本身在两种配置下都会被抓取——因为 Nutch 会把 <script src> 当作外链处理——所以真正的差别只在于:有没有谁会去读取文件内容并寻找像 URL 的字符串。打开这个插件后,两种字面量形式都能被识别出来。
在这个测试样本上,Nutch 的默认模式和 katana 的标准模式都到达了同样的 A 类集合;而 Nutch 加 parse-js、以及 katana 加 -jc 时,都可以在不依赖浏览器的前提下达到 A 类和 B 类。本文没有记录 katana 的具体版本和完整命令,因此这一结论属于上下文对照,而不是严格的产品基准测试。
C 类是诚实的上限。任何静态插件配置都没能触达它,这也在预料之中:只在脚本执行后才出现的端点,必须真的执行脚本才能恢复。我确实尝试过把 protocol-http 换成 protocol-htmlunit,也就是 Nutch 自带的纯 Java、可执行 JS 的协议。它能加载并运行而不崩溃,但在同一个四轮测试框架里,它只完成了一轮,只抓到了种子页和 app.js,A/B/C 全都没有到达,而且第二轮日志显示 0 records selected for fetching。这说明的是配置不足,而不是 HtmlUnit 能力不足。它能证明的范围更窄:把一个可执行 JS 的协议直接替换进去,不是即插即用的改法,而且我测试的所有配置里,C 类都没有被触达。
爬取控制与失败行为
深度不是一个开关。Nutch 里没有 --depth 3 这种参数;深度取决于你跑了多少轮 generate → fetch → parse → updatedb,因为第 R 轮抓的是第 R-1 轮发现的前沿。我的深度链测试把这一点验证得很清楚:
| 运行轮数 | 到达的最深路径 |
|---|---|
| 2 | /depth/1 |
| 3 | /depth/2 |
| 4 | /depth/3 |
这套机制很清楚,也很机械,但它意味着深度是脚本里的循环次数,而不是一个单独参数。
再来看一个坑。Nutch 随包默认值是 db.ignore.external.links=false,同时配了一个宽松的 +. URL 过滤规则——这意味着默认爬取会跟着链接跑到种子主机之外。我给一个页面放了一个站内链接和一个指向不同主机的外链,结果爬虫真的抓到了外部主机。两个独立信号都证明了这一点:Nutch 自己的 crawldb 把它标成了 db_fetched,而另一台主机的服务器计数器也记录到了命中。
要把范围限制在站内,必须主动配置,而且下面两种办法都确实有效:
| 配置 | crawldb 中是否出现外部主机 | 外部主机服务器命中 | 是否被圈住 |
|---|---|---|---|
db.ignore.external.links=false(随包默认) | db_fetched | +1 | 否 |
db.ignore.external.links=true | 不存在 | 0 | 是 |
在 regex-urlfilter.txt 中设置主机规则(先写 +^http://127.0.0.1:,再写 -.) | 不存在 | 0 | 是 |
如果你只抓一个站点,在第一次正式运行前就把其中一种方式设好。这里有一个方法学上的提醒:这个测试对本地服务器的负载比较敏感,所以这三行数据来自一次没有其他任务干扰的干净运行。行为本身非常明确,而且由两个独立信号共同支持;但具体行值只是一次干净测试的结果,不是多次平均值。
Sitemap 是另一步。礼貌抓取是开启的——正常爬取会请求 /robots.txt——但 sitemap 本身需要单独执行命令:
| 方法 | 是否请求了 /sitemap.xml? | 仅存在于 sitemap 中的端点 |
|---|---|---|
| 正常爬取 | 从未请求 | 0/2 |
显式对 crawldb 运行 bin/nutch sitemap | 已抓取 | 2/2 条目被注入,召回率完整 |
katana 的内联 -kf 已知文件模式,同一测试样本,IP 主机上运行 | 未记录 | 0/2 |
这和那种在抓取过程中内联拉取已知文件的爬虫是不同模型,不过它确实多花一条命令,但能完整完成任务。
另外两个较小的行为也表现稳定。
错误处理: 对一个同时链接到 500 和 404 页面进行爬取时,整个流程依然顺利完成,A 类的四个页面都抓到了,并且对每个失败都分别记录了状态:
| 页面中链接到的失败响应 | crawldb 记录状态 |
|---|---|
| 500 | db_unfetched(可重试) |
| 404 | db_gone |
没有任何环节被拖垮。
礼貌性(politeness): 在每个队列只开一个线程时,两次同主机抓取之间的间隔与设置一致:
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 秒 | 一个进程 |
这个差距并不是抓取速度造成的;两者请求的页面数量其实差不多。原因在架构:Nutch 的各阶段本来就是按 MapReduce 作业设计的,所以每一阶段都要付固定的进程成本。在一个很小的本地爬取任务里,准备开销反而成了主角。随着任务变大,这个固定成本应该会占比下降,但这次测试并没有测到 Nutch 和 katana 的交叉规模,也没有验证两者比例是否会反转。
优缺点
优点
- 静态发现是确定性的:HTML A 类 4/4,深度链 3/3,三次重复结果完全一致。
parse-js可以在不使用浏览器的情况下恢复 JavaScript 文件字面量端点(2/2),同时识别调用参数和赋值两种形式。- 有两种经过验证的范围控制方式,能把爬取完整限制在站内(
db.ignore.external.links和主机规则regex-urlfilter)。 - 通过
bin/nutch sitemap进行 sitemap 采集,对正常爬取完全错过的端点实现了 2/2 的完整召回。 - 容错能力不错:500 和 404 都会被分别记录到 crawldb 中,爬取还能继续。
- 在这次本地测试里,实际同主机间隔与配置的 1.0 秒延迟一致;随包默认是 5.0 秒。
- Apache-2.0 许可证,仍在积极维护,78 个插件,并且拥有一个持久化 crawldb,能跨轮次跟踪每个 URL 的状态。
- 本地模式可运行,不需要集群、HDFS,也不需要 Solr。
缺点
- 在 JDK 24 及以上版本无法运行,SecurityManager 移除会直接触发问题(我在 26.0.1 上实测失败)——随包的 Hadoop 3.4.2 早于上游修复,而那个逃生参数也已经没了,因此固定到 LTS JDK 不是偏好,而是硬性前提。
- 解压后约 396 MB,包含 188 个库 jar、78 个插件目录、35 个配置文件。
- 每条命令都要新起 JVM,导致每个阶段约有 1.77 秒固定开销;12 页、深度 4 的爬取大约要 45 秒,而单体二进制爬虫在同样条件下约 13 秒。
- 随包默认会跟着外链走到其他主机;要只爬一个站点,必须主动设置。
http.agent.name默认是空的,不设置就不会跑。- 没有 depth 参数——深度完全靠你自己控制循环轮次。
- 运行时 DOM 端点在我测试的所有配置里都无法触达,而且替换成可执行 JS 的协议也不是即插即用。
- 我只在单主机、较小样本的本地模式下做了测试。分布式/HDFS 模式、Solr 索引、hostdb、resume,以及增量重爬调度,都不在这次测试范围内——这里应视为未测试,而不是已验证可用。
适合谁,不适合谁
如果爬取本身就是难点,那么 Nutch 就很值。若你正在搭建搜索索引、要做大范围多域名爬取、需要一个带每个 URL 状态和重试语义的持久化 URL 数据库,或者最终会把工作分布到多台机器上,这就是一套自 Hadoop 之前就一直在干这件事的基础设施。它的插件系统意味着你可以在不 fork 的情况下改协议、解析器、过滤器和评分逻辑。它的礼貌性默认值也相当保守,这说明维护者确实认真考虑过怎样做一个“好邻居”。
如果你想从少量页面里直接拿到结构化数据,那就别选它。Nutch 会抓、会解析,然后给你 crawldb 和 segments,接着等你自己接索引器。如果目标是客户端渲染的单页应用,也别选它——我跑的所有配置里,C 类都没触达。如果你的团队本来就不用 JVM,也别选它,因为这会把 Java 工具链、LTS JDK 锁定,以及 396 MB 的 jars 一起塞进你原本没有这些东西的技术栈里。要是你的工作只是“每周抓一次一个网站,深度四层”,那你花在轮次循环和配置文件上的时间,可能比爬取本身还多。
对大多数在找爬虫的人来说,后面那种情况才是现实。这里并不是批评 Nutch,而是工具和任务不匹配。如果你想看看更广泛的选项,我们的 开源爬虫盘点 和 最佳网页抓取 GitHub 项目 对更轻量的方案有更详细的介绍。
替代方案,以及我们自己的方案放在什么位置
先说公平的一面:Nutch 是免费的、Apache 许可、自托管的,而且你可以永久自用,没有按请求计费。这是实打实的优势,下面任何比较都不会抹掉这一点。
相关评测:Browsertrix Crawler 评测。
在开源领域,怎么比较取决于你优化什么。如果你想要一个 Python 框架,强调抓取控制和“先发请求”的思路,Scrapy 对很多项目来说更像对照组;本文没有按同样标准测它的安装体积。如果你想要一个没有浏览器、体积更紧凑的 Go 爬虫,Colly 也是值得评估的形态。如果你的问题不是发现 URL,而是把网页转成适合 LLM 使用的内容,Crawl4AI 面向的是另一层需求。
像 Thunderbit 这样的托管服务,则把抓取、渲染和抽取都放到 API 后面,而 Nutch 则把爬取状态和基础设施掌握在你自己手里。Thunderbit 没有在这个样本上跑,因此这里比较的是所有权模型,而不是对召回率或动态页面表现的对等断言。
这是一笔所有权和开销之间的交换,而且一点也不含糊。Nutch 给你完全控制权、持久化 crawldb、按设计可扩展到集群,以及零边际成本——代价是 JVM、LTS JDK 锁定、396 MB 的 jars、轮次循环和你自己的索引层。托管 API 的好处是第一次调用就给你结构化输出,也不用你操心基础设施——代价是 按调用计费 和对爬取前沿的控制更少。如果你的任务是“给 5000 万页面建索引”,Nutch 的模型才是正确答案,API 反而荒唐。如果你的任务是“周四前把 200 个商品页里的结构化记录拿出来”,那结论正好相反。
结论
如果你要长期运行、多域名爬取,而且团队本身就维护 JVM 基础设施,那么 Apache Nutch 值得评估。在这次测试样本上,它的静态发现结果在重复运行中保持一致,parse-js 找到了两个 JavaScript 字面量端点,失败状态也如实保留在 crawldb 中,实际请求间隔与配置延迟一致。
但要诚实评估进入门槛。Nutch 1.22 在这里的 JDK 26.0.1 上失败了;本次评测中真正验证过并可用的 LTS 配置是 OpenJDK 17.0.20,而 Java 21 没有测试。然后再设置 http.agent.name,明确你的爬取范围,并把这次小规模本地测试里观察到的每阶段约 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 也不是一次就能直接跑通的改动。对于客户端渲染应用,你要么准备好 JS 执行型协议并投入真正的配置工作,要么直接换工具。
Nutch 需要安装 Hadoop 和 Solr 吗?
不需要。本地模式使用 Hadoop 的 LocalJobRunner 进程内执行——没有集群,没有 HDFS 守护进程,也没有 YARN——完整的 inject → generate → fetch → parse → updatedb 流程在一台机器上就能跑,不需要额外安装别的东西。Solr 通常是索引去向,但抓取本身并不依赖它。不过要注意,Hadoop jars 是打包自带的(13 个,版本 3.4.2),这也正是 JDK 兼容性问题存在的原因。
怎么阻止 Nutch 去抓别的网站?
要显式设置,因为随包默认不会帮你限制。Nutch 1.22 以 db.ignore.external.links=false 和宽松的 URL 过滤规则发布,在我的测试里,默认爬取确实跟着外链抓到了不同主机。你可以在 nutch-site.xml 中设置 db.ignore.external.links=true,或者在 conf/regex-urlfilter.txt 里加主机规则(例如先写 +^https://example\.com/,再写 -.)。这两种方式都能把爬取完全限制在站内,这一点已通过 Nutch 自己的 crawldb 和另一台服务器的请求日志共同验证。


