# robots.txt 处理：RFC 9309 覆盖声明

本文逐条列出 MoonCrawl 对 [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309.html)
（Robots Exclusion Protocol，2022-09）的覆盖情况。目的是把「遵守 robots 协议」
这句承诺**收敛到可核查的范围**，而不是用「实用子集」一笔带过。

- 规范版本：RFC 9309
- 实现位置：`robots.mbt`（解析与匹配）、`crawl.mbt`（状态码与调度语义）
- 验证方式：`moon test --target js -f "robots*"` 与 `-f "crawl*"`
- 声明日期：2026-10-28（2026-10-30 复查，实现未变）

## 1. 逐条覆盖表

| 条款 | 要求 | 级别 | 本项目 | 测试 |
| --- | --- | --- | --- | --- |
| §2.2.1 组选择 | product token 大小写不敏感匹配 | MUST | 实现：UA 与 `User-agent` 值均小写后前缀匹配 | `robots: 注释、空行与大小写` |
| §2.2.1 组选择 | 多个组匹配同一 UA 时合并为一组 | MUST | 实现：取最长 token 长度，长度相同的**全部**组规则按文件顺序拼接 | `robots: 同一 UA 分散在多个组时规则合并` |
| §2.2.1 回退 | 无匹配组时遵守 `*` 组 | MUST | 实现：无具体匹配时合并全部 `*` 组 | `robots: 组选择取最长 UA 前缀` |
| §2.2.1 无规则 | 无匹配组且无 `*` 组 → 无规则适用 | MUST | 实现：允许一切 | `robots: 无匹配组时允许一切` |
| §2.2.2 匹配起点 | 从 path 第一个字节开始匹配 | MUST | 实现：模式与 `path?query` 从头比较 | `robots: 基本组与 Disallow` |
| §2.2.2 最具体优先 | 采用 octets 最多的匹配规则 | MUST | 实现：按模式长度取最大 | `robots: Allow/Disallow 同时命中取更长模式` |
| §2.2.2 等长并列 | Allow 与 Disallow 等价时优先 Allow | SHOULD | 实现：等长时 Allow 胜出 | `robots: Allow/Disallow 同时命中取更长模式` |
| §2.2.2 空规则 | 组内无匹配规则 → 允许 | MUST | 实现 | `robots: 空值 Disallow 为无操作` |
| §2.2.2 隐式允许 | `/robots.txt` 本身隐式允许 | MUST | 实现：`is_robots_txt_path` 短路 | `robots: /robots.txt 隐式允许` |
| §2.2.3 注释 | `#` 起行内注释 | MUST | 实现：解析前截断 | `robots: 注释、空行与大小写` |
| §2.2.3 `*` | 匹配任意字符序列（含空） | MUST | 实现：按 `*` 切段后递归匹配 | `robots: 通配符 * 与结尾 $` |
| §2.2.3 `$` | 锚定模式结尾 | MUST | 实现 | `robots: 通配符 * 与结尾 $` |
| §2.2.2 百分号解码 | 比较前解码 unreserved 字符的 `%XX` | MUST | 实现：规则与请求两侧都过 `decode_unreserved`，`%2F` 等保留字符不解码 | `robots: 百分号编码的 unreserved 字符参与比较` |
| §2.5 解析上限 | 解析限额 MUST ≥ 500 KiB | MUST | 实现：上限 1 MiB（`ROBOTS_MAX_CHARS`），超出截断，截断点前的规则照常生效 | `robots: 超过 1 MiB 的解析上限后截断` |
| §2.3.1.1 成功 | 成功下载则执行可解析规则 | MUST | 实现 | `crawl: 假站点端到端` |
| §2.3.1.3 4xx | unavailable → 可访问任意资源 | MAY | 实现：视为无规则并放行 | `crawl: robots.txt 返回 4xx 视为无规则并放行` |
| §2.3.1.4 5xx / 网络错误 | unreachable → **必须**假定完全禁止 | MUST | 实现：默认 `RobotsUnreachable::Disallow`，`Allow` 需显式选择 | `crawl: robots 不可达时默认全禁`、`crawl: robots.txt 返回 5xx` |
| §2.3.1.5 解析错误 | 逐行尽力解析，执行可解析部分 | MUST | 实现：无法识别的行忽略，其余照常 | `robots: 无匹配组时允许一切` 等 |

## 2. 明确未实现（不做承诺）

| 条款 | 要求 | 级别 | 状态与理由 |
| --- | --- | --- | --- |
| §2.3.1.2 重定向 | SHOULD 跟随至少 5 次连续重定向 | SHOULD | **不处理**。重定向由传输层（`moonbitlang/async/http`）自行跟随，本模块不解析重定向链，也不校验「5 次」上限。 |
| §2.4 缓存 | SHOULD 不超过 24 小时 | SHOULD | **不实现**。缓存仅在单次 `crawl` 调用内有效（每 origin 一次），不跨进程持久化。 |

> 结论：**§2.2 匹配算法与 §2.3.1 状态码语义的 MUST 条款全部实现**，未覆盖的
> 两项均为 SHOULD，且都在传输层/调用方一侧，本模块不介入。
>
> 另有 §2.2.3「匹配字面 `*`/`$` 时用 `%` 编码」：规则里的 `%2A` 与请求里的
> `%2A` 逐字节相等，因此可精确匹配字面星号，行为符合该条 SHOULD。

## 3. 状态码语义

| robots.txt 响应 | RFC 9309 | MoonCrawl 默认 | 可配置 |
| --- | --- | --- | --- |
| 2xx | 解析并执行 | 解析并执行 | — |
| 3xx | 跟随（≥5 次） | 交给传输层跟随；跟随后的 2xx 正文按规则解析 | — |
| 4xx | unavailable，可访问 | 放行，不产生规则 | — |
| 5xx | unreachable，完全禁止 | **禁止**，计入 `robots_unreachable` 与 `pages_skipped_unreachable` | `RobotsUnreachable::Allow` |
| 网络错误 | unreachable，完全禁止 | 同上 | 同上 |
| 解析失败 | 执行可解析部分 | 逐行容错 | — |

## 4. Crawl-delay

`Crawl-delay` **不是** RFC 9309 的内容，是广泛部署的扩展（Yandex 等支持）。
本项目把它当作附加约束：

- 解析为毫秒（支持小数，如 `1.5` → 1500ms）；
- 只用来**抬高**同 origin 的请求间隔，不会降低调用方设置的 `delay_ms`；
- 多个合并组都声明时取**最大值**（更保守）。

## 5. 与主流实现的一致性

- 组选择与合并、最长匹配、等长 Allow 优先：与 Google 搜索中心的
  robots.txt 规范描述一致。
- 5xx 视为完全禁止：遵循 RFC 9309 §2.3.1.4 的 MUST；这与部分老爬虫
  「robots 取不到就放行」的旧惯例相反，属于有意选择，并在
  `RobotsUnreachable` 中保留显式退出口。

## 6. 如何复核

```bash
moon test --target js -f "robots*"   # 18 个 robots 用例
moon test --target js -f "crawl*"    # 13 个引擎用例（含 4xx/5xx/不可达分支）
```
