Apache StormCrawler 3.6.0 构建并运行示例抓取应用指南
Apache StormCrawler 3.6.0 构建并运行示例抓取应用指南:基于 archetype 工程,用内置 MemorySpout/MemoryStatusUpdater 内存后端与 StdOutIndexer 组成最小拓扑,抓取本地测试站点并输出索引结果,含完整 flux 配置、运行命令与真实回显(4 页全部抓取成功)。
Apache StormCrawler 3.6.0 构建并运行示例抓取应用指南
目标:从零构建并运行一个基于 Apache StormCrawler 3.6.0 的最小抓取应用(本地模式、无需外部后端服务)。使用 StormCrawler 自带的 MemorySpout/MemoryStatusUpdater 内存后端组件与 StdOutIndexer 输出组件,抓取一个本地测试站点并验证抓取状态与索引输出。实测结果:4 个页面全部抓取成功(HTTP 200)并完成索引输出。所有命令与回显均在 Ubuntu 22.04.5 + OpenJDK 17 + Storm 2.8.8 环境实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS
- Java:OpenJDK 17.0.20.1
- 构建工具:Apache Maven 3.6.3
- StormCrawler 版本:3.6.0(官方 archetype 生成工程)
- Storm 运行时:Apache Storm 2.8.8(安装于
/opt/apache-storm-2.8.8/) - 示例工程目录:
/root/build/crawler-demo/ - 本地测试站点:
127.0.0.1:8080(python3 -m http.server 提供) - 演示账号:root
二、前置条件
已按《Ubuntu 22.04 安装 Apache StormCrawler 3.6.0 指南》完成 archetype 工程生成、构建与 Storm 2.8.8 安装,并固化 JDK 17 环境变量:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
cd /root/build/crawler-demo
mvn -B clean package
构建需以 BUILD SUCCESS 结尾,产物 target/crawler-demo-1.0-SNAPSHOT.jar(约 106MB)存在。
三、准备本地测试站点
在一个空目录放置若干互相链接的 HTML 页面,并用 python3 内置 HTTP 服务器发布:
mkdir -p /var/www/site/sub && cd /var/www/site
创建 4 个页面(首页链接到其余三页,子页回链首页):
cat > index.html <<'EOF'
<!DOCTYPE html>
<html><head><title>Local Test Site</title></head>
<body>
<h1>StormCrawler Local Test Site</h1>
<p>Welcome to the local crawler test site.</p>
<ul>
<li><a href="/page1.html">Page One</a></li>
<li><a href="/page2.html">Page Two</a></li>
<li><a href="/sub/page3.html">Page Three</a></li>
</ul>
</body></html>
EOF
cat > page1.html <<'EOF'
<!DOCTYPE html>
<html><head><title>Page One</title></head>
<body><h1>Page One</h1><p>Content about Apache StormCrawler and Java.</p><a href="/index.html">Home</a> <a href="/page2.html">Page Two</a></body></html>
EOF
cat > page2.html <<'EOF'
<!DOCTYPE html>
<html><head><title>Page Two</title></head>
<body><h1>Page Two</h1><p>Content about crawling, indexing and search.</p><a href="/index.html">Home</a></body></html>
EOF
cat > sub/page3.html <<'EOF'
<!DOCTYPE html>
<html><head><title>Page Three</title></head>
<body><h1>Page Three</h1><p>Deep page about storm topology bolts and spouts.</p><a href="/index.html">Home</a></body></html>
EOF
启动站点(前台进程,端口 8080;建议另开终端执行):
python3 -m http.server 8080 --bind 0.0.0.0
验证站点可访问(另开终端):
wget -qO- http://127.0.0.1:8080/index.html | head -4
实测回显:
<!DOCTYPE html>
<html><head><title>Local Test Site</title></head>
<body>
<h1>StormCrawler Local Test Site</h1>
四、示例拓扑配置 crawler-local.flux
StormCrawler 通过 Flux(YAML)声明拓扑。archetype 生成的 crawler.flux 默认使用 URLFrontier 状态后端(需要额外部署 gRPC 服务);示例应用改用 core 内置的内存后端组件,在工程根目录新建 crawler-local.flux:
name: "crawler-local"
includes:
- resource: true
file: "/crawler-default.yaml"
override: false
- resource: false
file: "crawler-conf.yaml"
override: true
spouts:
- id: "spout"
className: "org.apache.stormcrawler.spout.MemorySpout"
constructorArgs:
- - "http://localhost:8080/index.html"
parallelism: 1
bolts:
- id: "partitioner"
className: "org.apache.stormcrawler.bolt.URLPartitionerBolt"
parallelism: 1
- id: "fetcher"
className: "org.apache.stormcrawler.bolt.FetcherBolt"
parallelism: 1
- id: "sitemap"
className: "org.apache.stormcrawler.bolt.SiteMapParserBolt"
parallelism: 1
- id: "feed"
className: "org.apache.stormcrawler.bolt.FeedParserBolt"
parallelism: 1
- id: "parse"
className: "org.apache.stormcrawler.bolt.JSoupParserBolt"
parallelism: 1
- id: "shunt"
className: "org.apache.stormcrawler.tika.RedirectionBolt"
parallelism: 1
- id: "tika"
className: "org.apache.stormcrawler.tika.ParserBolt"
parallelism: 1
- id: "index"
className: "org.apache.stormcrawler.indexing.StdOutIndexer"
parallelism: 1
- id: "status"
className: "org.apache.stormcrawler.persistence.MemoryStatusUpdater"
parallelism: 1
streams:
- from: "spout"
to: "partitioner"
grouping:
type: SHUFFLE
- from: "partitioner"
to: "fetcher"
grouping:
type: FIELDS
args: ["key"]
- from: "fetcher"
to: "sitemap"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "sitemap"
to: "feed"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "feed"
to: "parse"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "parse"
to: "shunt"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "shunt"
to: "tika"
grouping:
type: LOCAL_OR_SHUFFLE
streamId: "tika"
- from: "tika"
to: "index"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "shunt"
to: "index"
grouping:
type: LOCAL_OR_SHUFFLE
- from: "fetcher"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
- from: "sitemap"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
- from: "feed"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
- from: "parse"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
- from: "tika"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
- from: "index"
to: "status"
grouping:
type: FIELDS
args: ["url"]
streamId: "status"
与官方默认拓扑(crawler.flux)的差异只有两处:
| 组件 | crawler.flux(默认) | crawler-local.flux(示例) |
|---|---|---|
| spout | org.apache.stormcrawler.urlfrontier.Spout | org.apache.stormcrawler.spout.MemorySpout |
| status | org.apache.stormcrawler.urlfrontier.StatusUpdaterBolt | org.apache.stormcrawler.persistence.MemoryStatusUpdater |
💡 说明:
MemorySpout与MemoryStatusUpdater是 StormCrawler 官方为本地模式提供的内存后端组件:StatusUpdater 把调度结果直接写回 MemorySpout 的内存队列,从而在无外部存储的情况下实现递归抓取(官方源码注释明确二者需搭配使用且仅适用于本地/单 worker 模式)。- MemorySpout 构造器签名为
MemorySpout(String... urls)(varargs)。Flux 对 varargs 构造器要求传「嵌套列表」形态(上例constructorArgs下的两层短横线);若写成普通单值- "http://localhost:8080/index.html",启动会报Couldn't find a suitable constructor for class 'org.apache.stormcrawler.spout.MemorySpout'。StdOutIndexer是官方自带的「打印到标准输出」的索引组件,把解析后的文档以「字段名 + 制表符 + 字段值」逐行打印,便于演示验证。
五、补充 tika-config.xml
crawler-conf.yaml 中引用了 parse.tika.config.file: "tika-config.xml",但 archetype 不生成该文件,需在工程根目录补一个最小配置:
cd /root/build/crawler-demo
cat > tika-config.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<properties>
</properties>
EOF
⚠️ 注意:缺省该文件时 Tika 解析 bolt(org.apache.stormcrawler.tika.ParserBolt)在初始化阶段可能加载失败,导致拓扑异常。
六、放行本地站点的 URL 过滤规则
archetype 自带的 src/main/resources/default-regex-filters.txt 默认包含一条拒绝 localhost/回环地址的规则(防止爬虫被诱导访问本机服务,防 SSRF)。抓取本机测试站点时,需注释该条规则(实测本例仅注释 localhost 行,保留私网与其它拒绝规则生效):
sed -i 's|^-^https?://(?:localhost|# -^https?://(?:localhost|' src/main/resources/default-regex-filters.txt
修改后该行形态(实测):
# -^https?://(?:localhost|127(?:\.(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))){3}|\[::1\])(?::\d+)?(?:/|$)
其余拒绝规则保持不变(默认仍拒绝 file/ftp/mailto、图片后缀与 10/192.168/172.16 私网地址)。
⚠️ 注意:该资源文件打包在 uberjar 内,修改后必须重新
mvn package才能生效。生产环境抓取公网站点时请恢复此规则。
七、构建与运行
重新构建(应用第五、六步的改动)后,用 Storm 本地模式运行拓扑:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
cd /root/build/crawler-demo
mvn -B clean package
STORM_JAR_JVM_OPTS="-Xms256m -Xmx512m" /opt/apache-storm-2.8.8/bin/storm local \
target/crawler-demo-1.0-SNAPSHOT.jar org.apache.storm.flux.Flux crawler-local.flux --local-ttl 150
实测回显(启动行,含堆参数注入):
Running: /usr/lib/jvm/java-17-openjdk-amd64/bin/java -client -Ddaemon.name= -Dstorm.options= -Dstorm.home=/opt/apache-storm-2.8.8 -Dstorm.log.dir=/opt/apache-storm-2.8.8/logs -Djava.library.path=/usr/local/lib:/opt/local/lib:/usr/lib:/usr/lib64 -Dstorm.conf.file= -cp /opt/apache-storm-2.8.8/*:/opt/apache-storm-2.8.8/lib/*:/opt/apache-storm-2.8.8/extlib/*:target/crawler-demo-1.0-SNAPSHOT.jar:/opt/apache-storm-2.8.8/conf:/opt/apache-storm-2.8.8/bin: -Xms256m -Xmx512m -Dstorm.local.sleeptime=150 -Dstorm.jar=target/crawler-demo-1.0-SNAPSHOT.jar -Dstorm.dependency.jars= -Dstorm.dependency.artifacts={} org.apache.storm.LocalCluster org.apache.storm.flux.Flux crawler-local.flux
参数说明:
| 参数 | 说明 |
|---|---|
| local | Storm 本地模式子命令(拓扑运行在进程内 LocalCluster,无需 Nimbus/ZooKeeper) |
| target/crawler-demo-1.0-SNAPSHOT.jar | uberjar |
| org.apache.storm.flux.Flux | Flux 拓扑启动主类(shade 已设为 jar 默认主类,显式写出更清晰) |
| crawler-local.flux | 拓扑定义文件 |
| --local-ttl 150 | 本地模式运行秒数,到时自动关闭拓扑并退出(Flux 默认仅 20 秒) |
| STORM_JAR_JVM_OPTS="-Xms256m -Xmx512m" | 注入客户端/worker JVM 堆参数,把堆限制在 256-512MB |
验证堆参数生效(另开终端,启动后执行):
jps -v | grep LocalCluster | head -2
实测回显(节选,可见 -Xms256m -Xmx512m 已生效):
8725 LocalCluster -Ddaemon.name= -Dstorm.options= -Dstorm.home=/opt/apache-storm-2.8.8 -Dstorm.log.dir=/opt/apache-storm-2.8.8/logs -Djava.library.path=/usr/local/lib:/opt/local/lib:/usr/lib:/usr/lib64 -Dstorm.conf.file= -Xms256m -Xmx512m -Dstorm.local.sleeptime=30 -Dstorm.jar=target/crawler-demo-1.0-SNAPSHOT.jar -Dstorm.dependency.jars= -Dstorm.dependency.artifacts={}
启动完成后 Flux 会打印拓扑组件清单(实测节选):
Topology Name: crawler-local
--------------- SPOUTS ---------------
spout [1] (org.apache.stormcrawler.spout.MemorySpout)
---------------- BOLTS ---------------
partitioner [1] (org.apache.stormcrawler.bolt.URLPartitionerBolt)
fetcher [1] (org.apache.stormcrawler.bolt.FetcherBolt)
sitemap [1] (org.apache.stormcrawler.bolt.SiteMapParserBolt)
feed [1] (org.apache.stormcrawler.bolt.FeedParserBolt)
parse [1] (org.apache.stormcrawler.bolt.JSoupParserBolt)
shunt [1] (org.apache.stormcrawler.tika.RedirectionBolt)
tika [1] (org.apache.stormcrawler.tika.ParserBolt)
index [1] (org.apache.stormcrawler.indexing.StdOutIndexer)
status [1] (org.apache.stormcrawler.persistence.MemoryStatusUpdater)
八、运行结果解读
8.1 抓取状态输出(status 流 / Fetcher 日志)
内存队列中的种子 URL 先被 fetcher 抓取,随后解析出的外链经 status 组件回流到 spout 队列,实现递归抓取。实测 4 个页面全部命中:
13:33:00.256 [FetcherThread #40] INFO o.a.s.b.FetcherBolt - [Fetcher #4] Fetched http://localhost:8080/index.html with status 200 in msec 4
13:33:01.009 [FetcherThread #6] INFO o.a.s.b.FetcherBolt - [Fetcher #4] Fetched http://localhost:8080/page1.html with status 200 in msec 6
13:33:02.015 [FetcherThread #35] INFO o.a.s.b.FetcherBolt - [Fetcher #4] Fetched http://localhost:8080/page2.html with status 200 in msec 4
13:33:03.021 [FetcherThread #26] INFO o.a.s.b.FetcherBolt - [Fetcher #4] Fetched http://localhost:8080/sub/page3.html with status 200 in msec 4
解析侧对应日志:
13:33:00.266 [Thread-39-parse-executor[6, 6]] INFO o.a.s.b.JSoupParserBolt - Parsing : starting http://localhost:8080/index.html
13:33:00.437 [Thread-39-parse-executor[6, 6]] INFO o.a.s.b.JSoupParserBolt - Parsed http://localhost:8080/index.html in 106 msec
13:33:00.582 [Thread-39-parse-executor[6, 6]] INFO o.a.s.b.JSoupParserBolt - Total for http://localhost:8080/index.html - 251 msec
抓取窗口内 FetcherBolt 的自定义指标(LoggingMetricsConsumer 每 10 秒打印,实测非零值):
13:33:10.062 [Thread-50] INFO o.a.s.m.LoggingMetricsConsumer - 1791811990 bc39476cfd54:1024 4:fetcher fetcher_average_persec {bytes_fetched_perSec=91.60461083341833, fetched_perSec=0.4080383556054269}
8.2 索引输出(StdOutIndexer)
每抓取解析完一页,StdOutIndexer 即向标准输出打印索引文档(字段名 + 制表符 + 值)。先打印字段映射日志:
13:32:59.311 [Thread-45-index-executor[5, 5]] INFO o.a.s.i.StdOutIndexer - Mapping key parse.title to field title
13:32:59.311 [Thread-45-index-executor[5, 5]] INFO o.a.s.i.StdOutIndexer - Mapping key parse.keywords to field keywords
13:32:59.311 [Thread-45-index-executor[5, 5]] INFO o.a.s.i.StdOutIndexer - Mapping key parse.description to field description
13:32:59.311 [Thread-45-index-executor[5, 5]] INFO o.a.s.i.StdOutIndexer - Mapping key domain to field null
13:32:59.311 [Thread-45-index-executor[5, 5]] INFO o.a.s.i.StdOutIndexer - Mapping key format to field null
随后逐文档输出(实测 4 篇,节选):
content StormCrawler Local Test Site Welcome to the local crawler test site. Page One Page Two Page Three
url http://localhost:8080/index.html
domain localhost
format html
title Local Test Site
content Page One Content about Apache StormCrawler and Java.Home Page Two
url http://localhost:8080/page1.html
domain localhost
format html
title Page One
content Page Two Content about crawling, indexing and search.Home
url http://localhost:8080/page2.html
domain localhost
format html
title Page Two
content Page Three Deep page about storm topology bolts and spouts.Home
url http://localhost:8080/sub/page3.html
domain localhost
format html
title Page Three
结果解读:
- url/content/title/domain/format 五个字段由 crawler-conf.yaml 的
indexer.url.fieldname、indexer.text.fieldname与indexer.md.mapping决定:parse.title=title把解析出的标题映射为 title 字段,domain、format原样使用元数据键作为字段名。 - 递归抓取链路:MemorySpout 发出种子 → FetcherBolt 抓取(fetcher→status 流上报 FETCHED 状态)→ JSoupParserBolt 解析出外链(parse→status 流上报 DISCOVERED 状态)→ MemoryStatusUpdater 调度后回写内存队列 → spout 继续发出下一批 URL,直到队列耗尽。
--local-ttl 150到期后拓扑自动关闭(实测Done shutting down in process zookeeper后进程退出),无需手动 kill。
九、注意事项
⚠️ 注意:
- 内存后端(MemorySpout + MemoryStatusUpdater)仅适用于本地模式调试,状态存于 JVM 内存,不持久化、不支持多 worker/分布式;生产请使用 URLFrontier、OpenSearch、Solr 或 SQL 后端。
- 修改
default-regex-filters.txt等资源文件后必须重新mvn package;示例完成后建议恢复 localhost 拒绝规则。--local-ttl若不指定,Flux 默认 20 秒即结束,观察不到完整抓取过程;示例给 150 秒足够 4 页递归抓取。- 生产环境 worker 建议通过
topology.worker.childopts或STORM_JAR_JVM_OPTS将 Java 堆限制在 256-512MB 起步,并用jps -v验证生效,避免默认大堆挤占内存。
十、常见问题
Q1:启动报 Couldn't find a suitable constructor for class 'org.apache.stormcrawler.spout.MemorySpout'?
MemorySpout 构造器是 varargs(String... urls),Flux 下必须用嵌套列表传参:constructorArgs 第一层是「参数列表」,第二层短横线列出每个 URL(见第四节 YAML)。写成单值字符串无法匹配构造器。
Q2:只抓到了种子 1 页,后续外链没有被抓取?
archetype 默认的 default-regex-filters.txt 拒绝 localhost/127.x 地址的 URL(防 SSRF 安全规则),解析出的本站外链全部被过滤。按第六节注释对应规则并重新构建后即可递归抓取。
Q3:拓扑启动失败,提示找不到 tika-config.xml?
crawler-conf.yaml 引用 parse.tika.config.file: "tika-config.xml" 而 archetype 未生成该文件。按第五节在工程根目录补最小配置文件后重新构建运行。
Q4:storm local 找不到命令或提示 JAVA_HOME 未设置?
确认已安装 Apache Storm 2.8.8 并使用 /opt/apache-storm-2.8.8/bin/storm 全路径或已配置 PATH;同时确认 JAVA_HOME 指向 JDK 17(见《Ubuntu 22.04 安装 Apache StormCrawler 3.6.0 指南》第 2.3 节)。
Q5:想要生产可用的抓取部署怎么做?
将 crawler-local.flux 的 spout/status 换回官方后端组件(如 URLFrontier 的 org.apache.stormcrawler.urlfrontier.Spout/StatusUpdaterBolt,或 OpenSearch/Solr/SQL 后端),种子通过后端注入(URLFrontier 可用工程内自带的 crawlercommons.urlfrontier.client.Client PutURLs -f seeds.txt 命令),再以 storm jar 提交到分布式集群。
十一、参考链接
- StormCrawler 官方入门文档:
https://stormcrawler.net/ - StormCrawler 源码仓库(含 archetype 源文件):
https://github.com/apache/stormcrawler - Apache Storm LocalCluster 文档:
https://storm.apache.org/releases/2.8.8/LocalCluster.html - Flux 拓扑定义规范:
https://storm.apache.org/releases/2.8.8/flux.html