A web crawling framework for MoonBit: URL normalization, RFC 9309 robots handling, polite async scheduling, CSS-selector extraction and structured export.
moon add guangting0905/mooncrawl@0.1.2moon add guangting0905/mooncrawl@0.1.2moon test --target js # 82 个离线测试
moon run cmd/main --target js -- https://books.toscrape.com/ --max-pages 12 --depth 1# 页面模式:每页一条元数据
moon run cmd/main --target js -- https://books.toscrape.com/ --max-pages 12 --depth 1
# 采集模式:按 CSS 选择器逐条提取记录
moon run cmd/main --target js -- \
"https://books.toscrape.com/catalogue/category/books/travel_2/index.html" \
--max-pages 1 --depth 0 \
--container "article.product_pod" \
--fields "title=h3 a@title;price=.price_color;link=h3 a@href"{"title":"It's Only the Himalayas","price":"£45.17","link":"https://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html"}
{"title":"Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel","price":"£36.94","link":"https://books.toscrape.com/catalogue/vagabonding-an-uncommon-guide-to-the-art-of-long-term-world-travel_552/index.html"}
# pages_fetched=1 ok=1 failed=0 robots_skipped=0 robots_unreachable=0 links=76{"url":"https://books.toscrape.com/","status":200,"depth":0,"links_found":94,"title":"All products | Books to Scrape - Sandbox"}
{"url":"https://books.toscrape.com/catalogue/category/books/travel_2/index.html","status":200,"depth":1,"links_found":76,"title":"Travel | Books to Scrape - Sandbox"}
# pages_fetched=12 ok=12 failed=0 robots_skipped=0 robots_unreachable=0 links=1089// URL 规范化:折叠语法等价,保留参数顺序
let u = @mooncrawl.parse_url("HTTP://Example.COM:80/a/./b?b=2&a=1#top").unwrap()
println(@mooncrawl.normalize_url(u)) // http://example.com/a/b?b=2&a=1
println(@mooncrawl.normalize_url_sorted_query(u)) // http://example.com/a/b?a=1&b=2
// 相对地址展开
let base = @mooncrawl.parse_url("https://example.com/a/b/index.html").unwrap()
let next = @mooncrawl.resolve_url(base, "page2.html").unwrap()
// 布隆去重
let seen = @mooncrawl.Bloom::new(100_000)
seen.add("https://example.com/page/1")
// robots 判定与 Crawl-delay
let rules = @mooncrawl.parse_robots(robots_txt)
if rules.allowed_url("MoonCrawl/0.1", u) { /* 可以抓 */ }
let delay = rules.crawl_delay_ms("MoonCrawl/0.1")
// CSS 选择器提取
let rows = @mooncrawl.extract_records(html, "article.product_pod", [
@mooncrawl.FieldSpec::text("title", "h3 a"),
@mooncrawl.FieldSpec::attr("link", "h3 a", "href"),
@mooncrawl.FieldSpec::text("price", ".price_color"),
])
// 引擎:传输层可替换(需在 async fn 中调用)
let stats = @mooncrawl.crawl(seed, options, @mooncrawl.http_fetch, on_page)Seeds ──▶ Frontier(FIFO + 布隆去重 + pop_batch 按 host 分流)
│
▼
Politeness(origin 间隔 + Crawl-delay)──▶ robots.txt(RFC 9309)
│
▼
Fetcher(可注入闭包 / moonbitlang/async/http)
│ 跨 origin 并行,同 origin 串行
▼
Extract(链接 / 标题 / 正文 / CSS 选择器字段)
│
▼
Export(JSONL / CSV)while 未达页数上限 {
batch = frontier.pop_batch(max_concurrency) // 每个 host 最多一个
阶段 1(串行):robots 加载与准入判定
阶段 2(并行):@async.all 跨 origin 抓取,各自 polite.gate
阶段 3(串行):结果处理、链接提取、入队、统计、回调
}| 模块 | 文件 | 说明 | 测试 |
|---|---|---|---|
| URL | url.mbt | 解析、规范化(保留参数顺序)、相对展开 | 14 |
| 去重 | bloom.mbt | 布隆过滤器(16 bit/元素,7 哈希,位打包) | 2 |
| robots | robots.mbt | RFC 9309 组选择与匹配、百分号解码、状态码语义、Crawl-delay | 18 |
| 队列 | frontier.mbt | FIFO(游标式)、pop_batch 按 host 分流、间隔推算 | 8 |
| 引擎 | crawl.mbt | 三阶段异步调度、robots 集成、统计计数 | 13 |
| 提取 | extract.mbt | <a href>、<title>、正文 | 5 |
| 选择器 | selector.mbt | CSS 子集、字段映射、逐容器成表 | 10 |
| 导出 | export.mbt | CSV(RFC 4180)、JSONL、通用字段行 | 7 |
| 边界 | boundary_test.mbt | 跨模块边界用例(URL/robots/选择器/十万元素) | 5 |
| 传输 | fetch_http.mbt | 官方 async http 适配:30s 超时、UA 透传 | — |
| CLI | cmd/main | 页面模式 / 采集模式 | — |
pub(all) struct AttrPredicate {
name : String
op : String
value : String
}pub struct Bloom {
// private fields
}pub(all) struct CrawlOptions {
max_pages : Int
max_depth : Int
delay_ms : Int
max_concurrency : Int
respect_robots : Bool
robots_unreachable : RobotsUnreachable
keep_body : Bool
user_agent : String
}pub struct CrawlStats {
pages_fetched : Int
pages_ok : Int
pages_failed : Int
pages_skipped_robots : Int
pages_skipped_unreachable : Int
links_discovered : Int
robots_unreachable : Int
}pub(all) struct FetchOutcome {
status : Int
body : String
error : String?
}pub(all) struct FieldSpec {
name : String
selector : String
attr : String?
}pub struct Frontier {
// private fields
}pub(all) struct PageResult {
url : String
status : Int
depth : Int
links_found : Int
title : String?
body : String?
error : String?
}pub struct RobotsRules {
// private fields
}pub(all) enum RobotsUnreachable {
Disallow
Allow
}pub(all) enum SimpleSelector {
TagSel(String)
ClassSel(String)
IdSel(String)
AttrSel(AttrPredicate)
FirstChildSel
}pub struct Url {
scheme : String
host : String
port : Int?
path : String
query : String?
}async fn crawl(seed : String, options : CrawlOptions, fetch : async (String, String) -> FetchOutcome, on_page : (PageResult) -> Unit) -> CrawlStatsfn extract_text(html : String) -> StringInstall
Download zipA web crawling framework for MoonBit: URL normalization, RFC 9309 robots handling, polite async scheduling, CSS-selector extraction and structured export.