AAoM-02:符合 W3C 标准的 XML 解析器

发布: (2026年1月14日 GMT+8 10:32)
7 min read
原文: Dev.to

Source: Dev.to

技能

我仍在使用 Claude Code (Opus 4.5) 与 MoonBit system prompt and IDE skill
此外,我创建了一个名为 moonbit-lang 的新技能,用来向 AI 说明 MoonBit 语言的最佳实践和常见陷阱。该技能的头部如下:

---
name: moonbit-lang
description: "MoonBit language reference and coding conventions. Use when writing MoonBit code, asking about syntax, or encountering MoonBit-specific errors. Covers error handling, FFI, async, and common pitfalls."
---

# MoonBit Language Reference

@reference/fundamentals.md
@reference/error-handling.md
@reference/ffi.md
@reference/async-experimental.md
@reference/package.md
@reference/toml-parser-parser.mbt

在此技能文档中,我还提到了官方的文件 I/O 包 moonbitlang/x/fs,而 AI 并不熟悉它。
完整的技能文档和引用可以在 GitHub 上访问,我会持续更新我使用的技能。

AI(包括 Codex 和 Claude)在启动时只读取描述,随后按需读取其余内容。我保持技能文档简洁,因为根据我的经验,过长的文档会妨碍 AI 理解细节。

问题

XML 在配置文件、数据交换和遗留系统中仍然无处不在。符合规范的 XML 解析器必须能够处理:

  • 元素标签、属性和命名空间
  • 实体引用

下面是一个简单的测试,用于解析最小文档并检查生成的事件流:

let xml = "\n\n\n"
let reader = Reader::from_string(xml)
let events : Array[Event] = []
for {
  match reader.read_event() {
    Eof => {
      events.push(Eof)
      break
    }
    event => events.push(event)
  }
}
inspect(
  to_libxml_format(events),
  content="[DocType(\"doc\"), Empty({name: \"doc\", attributes: []}), Eof]",
)

一个 非良构 示例:

test "w3c/not-wf/not_wf_sa_001" {
  // Attribute values must start with attribute names, not "?".
  let xml = "\n\n\n"
  let reader = Reader::from_string(xml)
  let has_error = for {
    try reader.read_event() catch {
      _ => break true
    } noraise {
      Eof => break false
      _ => continue
    }
  }
  inspect(has_error, content="true")
}

共生成了 735 个测试,约 14 k 行代码。添加了一些手写测试后,套件现在包含 800 个测试。

Source:

解析器实现

由于 quick‑xml 是最初的参考,Claude 采用了受其启发的 pull‑parser 架构,我认为这对我们的目标是可接受的。API 如下:

let reader = @xml.Reader::from_string(xml)
for {
  match reader.read_event() {
    Eof => break
    Start(elem) => println("Start: \{elem.name}")
    End(name)   => println("End: \{name}")
    Text(content) => println("Text: \{content}")
    _ => continue
  }
}

因为 lxml 返回的是树结构,而我们的解析器发出事件,我让 Claude 实现了一个 to_libxml_format 函数,将我们的事件流转换为 lxml 生成的完全相同的格式。这样测试比较就变得很直接。

基本实现大约用了 4 小时 的纯 AI 工作(除偶尔的 “Please continue” 提示外)。最复杂的特性是 DTD 的解析和校验。我使用 Claude 的计划模式来组织实现。下面是该计划的概要:

计划概述

项目概述

Diagram

大约 1 小时 后,实现了 DTD 支持,且 726 个测试通过
随后又花了 3 小时 处理诸如以下的边缘情况:

  • 实体值展开
  • 文本拆分细节
  • UTF‑8 BOM 处理

结果

在此次工作结束时,800 个 W3C 合规性测试通过

  • 59 个测试tests‑gen 脚本跳过,原因是:
    • 有些是有效的但被 lxml 拒绝。
    • 另一些则不符合良好结构但被 lxml 接受。

这些被标记为 “lxml 实现的怪癖”。
由于边缘情况过于复杂,我没有对每一个进行详细验证,但其余 800 项测试已足以提供信心。

支持的特性

  • XML 1.0 + Namespaces 1.0
  • 用于内存高效流式处理的 Pull‑parser API
  • 用于 XML 生成的 Writer API
  • 支持实体展开的 DTD

反思

有哪些做得好的地方?

  • 使用官方测试套件 – W3C 合规性测试揭示了晦涩的边缘情况(字符引用、DTD 奇怪行为、命名空间处理等),这些是我手动测试时根本想不到的。
  • 切换参考实现quick‑xml 故意宽松,这使得合规性测试变得困难。切换到 libxml2 提供了严格的参考实现。
  • 为复杂特性使用计划模式 – 将 DTD 解析拆分为计划使工作有序;否则我会在无关的 bug 之间跳来跳去。

遇到的挑战

Claude 经常尝试 修改测试 而不是修复解析器:

  • 将测试期望改为匹配错误的输出。
  • 更新测试生成器以跳过失败的测试。
  • 将测试标记为“宽松”并跳过它们。

我必须一再提醒 Claude:“更新 MoonBit 实现,而不是测试。”

其他反复出现的问题:

  • 忘记项目约定(例如,不使用 moon‑ide 技能进行导航,使用 match (try? expr) 而不是 try/catch/noraise)。
  • 将这些约定添加到 CLAUDE.md 有所帮助,但并未根除问题。

我在 Reddit 上看到相关讨论(链接),指出 Opus 4.5Sonnet 4.5 存在 bug。希望它能尽快修复。

未来工作

我预计还需要实现或移植许多解析器。我的计划是把编写解析器和生成基于标准的测试脚本的经验转化为可复用的 技能命令,以便下一个项目能够受益于这些基础工作。

时间投入(≈ 10 小时)

活动小时
协作探索测试生成脚本2
自主实现基本功能4
规划并实现 DTD、命名空间、实体1
处理边缘情况(修复 17 项测试失败)3

代码已在 GitHub 上提供:

Back to Blog

相关文章

阅读更多 »

重叠标记

请提供您希望翻译的具体摘录或摘要文本,我才能为您进行简体中文翻译。