mooncrawl

    A web crawling framework for MoonBit: URL normalization, RFC 9309 robots handling, polite async scheduling, CSS-selector extraction and structured export.

    crawler
    spider
    scraping
    robots-txt
    url
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    6 days ago
    Downloads
    23

    #MoonCrawl 🕷️🥮

    CI

    MoonBit 的网页爬虫框架 —— 2026 MoonBit 黑客松 10 月赛参赛项目。

    已发布到 mooncakes.io:guangting0905/mooncrawl@0.1.2 (文档)

    moon add guangting0905/mooncrawl@0.1.2

    MoonCrawl 把「抓网页 → 提数据 → 出结果」串成一条可复用流水线:URL 规范化、 布隆去重、robots.txt 处理、按站点礼貌限速、跨 origin 异步调度、HTML 提取与 CSS 选择器字段提取、JSONL / CSV 导出。框架层是本项目的产出——HTTP 客户端 (moonbitlang/async)与 HTML 解析器(moonbit-community/html)直接复用生态 现有实现,不重复造轮子。

    #为什么做这个

    MoonBit 生态里 HTTP 客户端、HTML 解析器、CSV 解析器这些积木已经齐了, 但把它们串成可用采集流水线的框架层是空白的。空白不在于「能不能发请求」, 而在于这些工程约束:

    • 什么是同一个资源 —— 大小写、默认端口、点段、fragment 折叠; 查询参数顺序不折叠(见下)。
    • 什么是礼貌 —— robots.txt 的解析与匹配、Crawl-delay、按 origin 记账的 请求间隔。这些做错不会报错,只会悄悄变成滥用。
    • 怎么并发不失控 —— 跨站点并行提速,同站点必须串行。
    • 提取规则放哪 —— 写死在代码里换站点就得改代码;做成数据换选择器就行。

    申报前已按章程完成查重(详见 docs/查重报告.md): mooncakes.io 截至 2026-10-24 的全量 3,265 个模块中 crawl/spider/scrape 相关 0 命中(不含本项目自身); awesome-moonbit 官方清单无同类条目;往期黑客松 22 个参赛项目无同类选题。

    #快速开始

    作为依赖:

    moon add guangting0905/mooncrawl@0.1.2

    从源码构建与测试:

    moon test --target js # 82 个离线测试 moon run cmd/main --target js -- https://books.toscrape.com/ --max-pages 12 --depth 1

    后端选择:js 后端零额外依赖,开箱可用;native 后端需要 C 工具链 (Windows 上是 MSVC Build Tools + Windows SDK)以及 OpenSSL 头文件 (moonbitlang/async 的 TLS 依赖,Ubuntu 上装 libssl-dev)。 CI 在两种后端上都执行全量测试。

    #能力范围

    #URL 处理(url.mbt)

    • 解析绝对 http/https URL:scheme/host 小写化、userinfo 剥离、IPv6 字面量、 端口校验、fragment 丢弃;
    • 规范化:折叠 RFC 3986 §6.2.2 意义上的语法等价(大小写、默认端口、点段、 fragment),默认保留查询参数原始顺序;
    • 相对地址展开:同目录、根相对、scheme 相对、查询相对,javascript:/mailto: 等外部 scheme 拒绝解析;
    • 显式的 normalize_url_sorted_query:仅当调用方确认站点把 query 当无序 多重集时启用参数排序折叠。

    #robots.txt(robots.mbt)

    按 RFC 9309 实现匹配算法与状态码语义,覆盖边界逐条列在 docs/rfc9309-覆盖声明.md:

    • §2.2.1 组选择:大小写不敏感前缀匹配、多组匹配时合并、* 组回退;
    • §2.2.2 匹配:从首字节匹配、最长模式优先、等长 Allow 优先、/robots.txt 隐式允许;
    • §2.2.3 特殊字符:# 注释、* 通配、$ 锚定;
    • §2.2.2 百分号解码:比较前解码 unreserved 字符的 %XX(%7E 与 ~ 视为同一 路径),%2F 等保留字符不解码;
    • §2.5 解析上限:1 MiB 截断,截断点之前的规则照常生效;
    • §2.3.1 状态码:2xx 执行、4xx 放行、5xx 与网络错误默认完全禁止 (RobotsUnreachable::Disallow,可按需改为 Allow);
    • 扩展:Crawl-delay 解析并在合并组时取最大值,只抬高不降低请求间隔。

    #去重与队列(bloom.mbt / frontier.mbt)

    • 布隆过滤器:每元素 16 bit、7 哈希、理论误报率约 0.07%;位用 Array[UInt64] 手工打包,十万 URL 约 200 KiB;
    • 游标式 FIFO(无大数组 shift 退化)、已见去重、按 host 的间隔推算;
    • pop_batch(n):批量出队且每个 host 最多一个,为并发调度提供同站串行保证。

    #异步抓取引擎(crawl.mbt)

    • async fn crawl(...):深度/页数上限、robots 准入、按 origin 礼貌限速;
    • 三阶段调度:串行做准入判定 → 并行抓取(跨 origin)→ 串行做提取入队, 共享状态只在串行段修改;
    • max_concurrency 控制跨 origin 并行度,同一 origin 始终串行;
    • 传输层是可注入闭包(FetchFn),引擎能在无网络、无时钟依赖的离线环境下 对假站点确定性测试;默认实现 http_fetch 基于 moonbitlang/async/http (30 秒超时、User-Agent 透传、错误统一折叠);
    • 统计计数:抓取页数、成功/失败、robots 拒绝、robots 不可达拒绝、 发现链接数、不可达 origin 数。

    #提取(extract.mbt / selector.mbt)

    • 基础提取:<a href> 按文档顺序收集、<title> 提取、正文文本 (跳过 script/style/noscript 并折叠空白);
    • CSS 选择器子集:类型/类/ID/属性(= ~= ^= $= *= |=)、 后代与子代组合子、逗号并集、:first-child;
    • 字段映射:FieldSpec(名称 + 选择器 + 可选取属性)→ extract_fields(整页一条记录)与 extract_records(逐容器成表, 字段选择器不越过容器边界);
    • 明确不支持:伪元素、:not()/:has()、兄弟组合子 +/~、 nth-child 表达式、XPath。遇到无法识别的语法返回空选择器,不猜。

    #导出(export.mbt)

    • CSV(RFC 4180 转义,CRLF 行尾)与 JSONL(控制字符转义);
    • 通用 json_record / csv_record 接受任意字段对,配合字段提取使用;
    • 核心零文件 IO 依赖,输出走 stdout,便于重定向与管道。

    #CLI(cmd/main)

    两种模式,参数见 --help:

    # 页面模式:每页一条元数据 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"

    采集模式的真实输出(字段值里的相对链接会自动展开为绝对 URL):

    {"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(串行):结果处理、链接提取、入队、统计、回调 }

    模块文件说明测试
    URLurl.mbt解析、规范化(保留参数顺序)、相对展开14
    去重bloom.mbt布隆过滤器(16 bit/元素,7 哈希,位打包)2
    robotsrobots.mbtRFC 9309 组选择与匹配、百分号解码、状态码语义、Crawl-delay18
    队列frontier.mbtFIFO(游标式)、pop_batch 按 host 分流、间隔推算8
    引擎crawl.mbt三阶段异步调度、robots 集成、统计计数13
    提取extract.mbt<a href>、<title>、正文5
    选择器selector.mbtCSS 子集、字段映射、逐容器成表10
    导出export.mbtCSV(RFC 4180)、JSONL、通用字段行7
    边界boundary_test.mbt跨模块边界用例(URL/robots/选择器/十万元素)5
    传输fetch_http.mbt官方 async http 适配:30s 超时、UA 透传—
    CLIcmd/main页面模式 / 采集模式—

    82 个测试全部离线:引擎对内存假站点运行(注入 fetch 闭包), 不依赖网络与时钟,CI 在 js 与 native 双后端执行全量测试 + moon fmt 门禁。

    #已知限制

    写在前面,避免误用:

    1. 查询参数顺序不折叠。?a=1&b=2 与 ?b=2&a=1 被视为不同 URL, 同一资源可能被抓两次。这是有意的保守取舍:参数顺序在部分服务端是语义 (签名、重复键、按位置读参)。确认目标站点无序时用 normalize_url_sorted_query。
    2. 不处理重定向链。robots.txt 的重定向跟随交给传输层,本模块不解析 重定向链,也不校验 RFC 9309 §2.3.1.2 的「5 次」上限。
    3. robots 缓存不持久化。只在单次 crawl 调用内有效,不跨进程。
    4. 不做 JS 渲染。只解析服务端返回的 HTML,SPA 站点需要在渲染层另行解决。
    5. 不做 XPath。选择器只覆盖 CSS 子集。
    6. 单机内存队列。URL 全量驻留内存(布隆过滤器只做去重),不支持分布式调度。
    7. 同 origin 串行。跨 origin 才并行;单站点抓取的吞吐受 delay_ms 限制。

    #工程实践

    • 持续提交:提交历史按模块推进,功能提交标注测试数并关联 Issue。
    • 双目标 CI:GitHub Actions 在 js 与 native 后端跑全量测试 + moon fmt 门禁。
    • Issue 驱动:立项时建 10 个 Issue 作为看板,随里程碑关闭并附交付说明。
    • 接口稳定:moon info 生成 .mbti 接口文件入库,重构可见性有据可查。
    • 零告警:moon check 无警告无错误,moon fmt 无 diff。

    #查重声明

    立项前按章程完成查重(详见 docs/查重报告.md): mooncakes.io 全量 3,265 模块中 crawl/spider/scrape 相关 0 命中; awesome-moonbit 官方清单无同类条目;往期黑客松 22 个参赛项目无同类选题。 本项目复用社区积木,新增的是生态空白的框架层。

    #路线图

    已交付(10 月赛):

    后续方向(季度/年度持续建设):

    • robots 百分号解码与 500 KiB 解析上限
    • 重定向链显式处理(5 跳上限)
    • Sitemap 发现与优先级队列
    • 提取规则持久化(配置文件驱动的采集任务)
    • native 单文件二进制发布 + skills.mooncakes.io 技能上架

    #开源合规

    Apache-2.0,完整许可文本见 LICENSE。

    • NOTICE:第三方组件归属与所引用的规范清单见 NOTICE。 项目为原创实现,未复制或内联任何第三方源码;moonbitlang/async、 moonbit-community/html 均为 Apache-2.0,以 moon.mod 声明的方式作为依赖使用。
    • 规范引用:RFC 3986(URL 规范化)、RFC 9309(robots.txt)、 RFC 4180(CSV)为文档而非代码,引用处以 docs/ 与源码注释标注。
    • 打包范围:.moonignore 控制发布内容,参赛材料(含联系方式与审核沟通记录) 不随包分发。

    #作者

    guangting0905 · 2026 MoonBit 黑客松 10 月赛

    AttrPredicate

    pub(all) struct AttrPredicate {
    name : String
    op : String
    value : String
    }

    属性谓词:[name] 仅判存在,其余按 op 比较。

    Bloom

    pub struct Bloom {
    // private fields
    }

    固定容量布隆过滤器。

    Bloom::add

    fn Bloom::add(self : Bloom, key : String) -> Unit

    加入一个键。

    Bloom::maybe_contains

    fn Bloom::maybe_contains(self : Bloom, key : String) -> Bool

    判断键是否可能已添加。返回 false 表示一定未添加过。

    Bloom::new

    fn Bloom::new(expected_items : Int) -> Bloom

    创建期望容纳 expected_items 个元素的过滤器。

    每元素分配 16 位、7 个哈希函数,理论误报率 (1 - e^(-7/16))^7 ≈ 0.070%;10 万元素约 200 KiB。

    CompoundSelector

    pub(all) struct CompoundSelector {
    simples : Array[SimpleSelector]
    combinator : String
    }

    复合选择器:一串简单选择器 + 它与前一个复合选择器之间的组合子。 combinator 取 ""(链首)、" "(后代)、">"(子代)。

    CrawlOptions

    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
    }

    抓取配置。

    CrawlOptions::default

    fn CrawlOptions::default() -> CrawlOptions

    CrawlStats

    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
    }

    抓取统计。

    FetchOutcome

    pub(all) struct FetchOutcome {
    status : Int
    body : String
    error : String?
    }

    传输层结果:所有失败折叠进 error 字段,引擎不感知传输细节。

    FieldSpec

    pub(all) struct FieldSpec {
    name : String
    selector : String
    attr : String?
    }

    一个待提取字段:名称 + 选择器 + 可选取属性(None 取文本)。

    FieldSpec::attr

    fn FieldSpec::attr(name : String, selector : String, attr : String) -> FieldSpec

    属性字段构造(如 FieldSpec::attr("link", "a.next", "href"))。

    FieldSpec::text

    fn FieldSpec::text(name : String, selector : String) -> FieldSpec

    文本字段构造。

    Frontier

    pub struct Frontier {
    // private fields
    }

    Frontier::is_empty

    fn Frontier::is_empty(self : Frontier) -> Bool

    队列是否为空。

    Frontier::length

    fn Frontier::length(self : Frontier) -> Int

    待抓取数量。

    Frontier::mark_seen

    fn Frontier::mark_seen(self : Frontier, url : String) -> Unit

    直接管账式登记「已见过」(用于种子的去重基线),不入队。

    Frontier::new

    fn Frontier::new(min_host_delay_ms : Int) -> Frontier

    min_host_delay_ms:同一 host 两次请求的最小间隔(礼貌默认 1000)。

    Frontier::next_allowed_ms

    fn Frontier::next_allowed_ms(self : Frontier, url : String, now_ms : Int) -> Int

    该 URL 最早允许开始抓取的时刻(考虑 host 间隔与当前时刻)。

    Frontier::pop

    fn Frontier::pop(self : Frontier) -> String?

    FIFO 出队。空返回 None。

    Frontier::pop_batch

    fn Frontier::pop_batch(self : Frontier, limit : Int) -> Array[String]

    批量出队,每个 host 最多一个。

    供并发调度使用:同 host 的请求必须串行(礼貌限速按站记账), 不同 host 可以并行。本轮未取到的 URL 会被移到队尾,留待下一轮。 扫描上界取调用时的队列长度快照,因此不会因为回填而无限循环。

    Frontier::push

    fn Frontier::push(self : Frontier, url : String) -> Bool

    入队。已见过的 URL(含此前抓取过的)返回 false 且不入队。

    Frontier::record_visit

    fn Frontier::record_visit(self : Frontier, url : String, at_ms : Int) -> Unit

    抓取开始时登记:host 最近访问时刻 = at_ms。

    PageResult

    pub(all) struct PageResult {
    url : String
    status : Int
    depth : Int
    links_found : Int
    title : String?
    body : String?
    error : String?
    }

    单页抓取结果。status=0 表示传输失败或被策略跳过,具体原因见 error。

    RobotsRules

    pub struct RobotsRules {
    // private fields
    }

    解析后的 robots.txt 规则集。

    RobotsRules::allowed

    fn RobotsRules::allowed(self : RobotsRules, agent : String, path : String) -> Bool

    判定 agent 是否允许抓取 path(含 "?query",不含 host 与 fragment)。 无匹配组、组内无规则或路径为 /robots.txt 时允许。

    RobotsRules::allowed_url

    fn RobotsRules::allowed_url(self : RobotsRules, agent : String, url : Url) -> Bool

    便捷方法:对已解析的 Url 做 robots 判定(匹配目标为 path+query)。

    RobotsRules::crawl_delay_ms

    fn RobotsRules::crawl_delay_ms(self : RobotsRules, agent : String) -> Int?

    生效规则集里声明的 Crawl-delay(毫秒),多组合并时取最大值。 无组/未声明返回 None。

    RobotsRules::sitemaps

    fn RobotsRules::sitemaps(self : RobotsRules) -> Array[String]

    文件中声明的 Sitemap URL 列表。

    RobotsUnreachable

    pub(all) enum RobotsUnreachable {
    Disallow
    Allow
    }

    robots.txt 不可达(5xx 或网络错误)时的策略。

    Selector

    pub(all) struct Selector {
    chains : Array[Array[CompoundSelector]]
    }

    解析后的选择器:多个以逗号分隔的复杂选择器。

    Selector::empty

    fn Selector::empty() -> Selector

    解析失败或空输入时返回的选择器(匹配 0 个节点)。

    Selector::is_empty

    fn Selector::is_empty(self : Selector) -> Bool

    Selector::parse

    fn Selector::parse(input : String) -> Selector

    解析选择器字符串。非法语法返回 Selector::empty()。

    SimpleSelector

    pub(all) enum SimpleSelector {
    TagSel(String)
    ClassSel(String)
    IdSel(String)
    AttrSel(AttrPredicate)
    FirstChildSel
    }

    单个简单选择器。

    Url

    pub struct Url {
    scheme : String
    host : String
    port : Int?
    path : String
    query : String?
    }

    解析后的 URL。fragment 在解析阶段即被丢弃(不参与去重与请求)。

    crawl

    async fn crawl(seed : String, options : CrawlOptions, fetch : async (String, String) -> FetchOutcome, on_page : (PageResult) -> Unit) -> CrawlStats

    引擎主循环:按批取待抓 URL,批内跨 host 并行、同 host 严格串行。

    三阶段结构让并发不污染共享状态:
    1. 串行:robots 加载与准入判定(会修改 robots 缓存、统计、blocked);
    2. 并行:对通过准入的 URL 按 origin 礼貌等待后抓取(只读共享状态);
    3. 串行:处理抓取结果、提取链接、入队、统计与回调。

    max_concurrency = 1 时退化为完全串行,行为与单请求循环一致。 无效种子返回全零统计。

    csv_escape

    fn csv_escape(field : String) -> String

    单字段 CSV 转义:含逗号、引号、换行时整体加引号,内部引号翻倍。

    csv_header

    fn csv_header() -> String

    CSV 表头(与 jsonl_page 字段对应)。

    csv_page

    fn csv_page(p : PageResult) -> String

    CSV 数据行:Option 字段以空串表示缺失。

    csv_record

    fn csv_record(fields : Array[(String, String)]) -> String

    通用记录行:字段对按传入顺序输出为一行 CSV(字段名作为表头来源)。

    csv_record_header

    fn csv_record_header(fields : Array[(String, String)]) -> String

    通用表头行。

    csv_row

    fn csv_row(fields : Array[String]) -> String

    CSV 行组装(字段按传入顺序,CRLF 行尾符合 RFC 4180)。

    extract_fields

    fn extract_fields(html : String, fields : Array[FieldSpec]) -> Array[(String, String)]

    从整页提取一组字段(每个字段取首个匹配),返回 (字段名, 值) 对。 字段无匹配时值为空串,保证 CSV 列数稳定。
    fn extract_links(html : String) -> Array[String]

    收集页面中所有 的原始值(文档顺序,未解析)。

    extract_records

    fn extract_records(html : String, container : String, fields : Array[FieldSpec]) -> Array[Array[(String, String)]]

    列表页采集:对每个容器匹配,在容器子树内提取字段,得到一张表。

    字段选择器不会越过容器边界(matches_chain 的 root 约束),因此 容器内的相对选择器不会误取兄弟容器的内容。

    extract_text

    fn extract_text(html : String) -> String

    提取正文文本:body 子树内的可见文本,跳过 script/style, 相邻文本以空格合并并折叠连续空白。

    extract_title

    fn extract_title(html : String) -> String?

    提取 文本(去实体、去首尾空白)。无 title 返回 None。

    http_fetch

    async fn http_fetch(url : String, user_agent : String) -> FetchOutcome

    FetchFn 的默认实现。

    json_escape

    fn json_escape(s : String) -> String

    JSON 字符串值转义(引号/反斜杠/控制字符;其余 UTF-8 原样透传)。

    json_record

    fn json_record(fields : Array[(String, String)]) -> String

    通用记录行:字段对按传入顺序输出为一行 JSON。

    与 jsonl_page 的区别是不绑定任何结构体——配合 extract_fields / extract_records 使用,字段名来自调用方的 FieldSpec。

    jsonl_page

    fn jsonl_page(p : PageResult) -> String

    JSONL 行:抓取结果一行一条记录。

    normalize_url

    fn normalize_url(u : Url) -> String

    保守规范化:小写 scheme/host、省略协议默认端口、折叠点段、 丢弃 fragment,保留查询参数的原始顺序。

    折叠的每一项都是 RFC 3986 §6.2.2 意义上的语法等价;参数顺序涉及 §6.2.3 的「需 scheme 定义」情形,默认不动。

    normalize_url_sorted_query

    fn normalize_url_sorted_query(u : Url) -> String

    激进规范化:在 normalize_url 基础上再把查询参数按 key/value 排序。

    仅当调用方确认目标站点把 query 当作无序多重集时使用。对签名 URL、 依赖参数位置的接口、以重复键表达多值的接口,这样做会把不同资源折叠成 同一个 URL,造成漏抓。

    parse_robots

    fn parse_robots(text : String) -> RobotsRules

    解析 robots.txt 文本。容错:忽略注释与无法识别的行; 空的 Disallow/Allow 值视为无操作。

    parse_url

    fn parse_url(input : String) -> Url?

    解析绝对 http/https URL。 非绝对地址、非 http(s) 协议或端口非法时返回 None。

    resolve_url

    fn resolve_url(base : Url, href : String) -> Url?

    以 base 为基准展开相对地址。 空地址、纯 fragment、javascript:/mailto: 等非 http(s) 地址返回 None。

    select_attrs

    fn select_attrs(html : String, selector : String, attr : String) -> Array[String]

    按选择器提取指定属性的值;缺该属性的节点被跳过。

    select_count

    fn select_count(html : String, selector : String) -> Int

    按选择器统计匹配节点数。

    select_texts

    fn select_texts(html : String, selector : String) -> Array[String]

    按选择器提取文本,返回文档顺序的数组。