Skip to content

Day 76: std.xml模块:简单的XML解析

XML(eXtensible Markup Language)是一种用于编码结构化数据的标记语言,广泛应用于配置文件、数据交换(如RSS、SOAP)和文档格式(如SVG、XHTML)中。

与JSON相比,XML的结构更复杂,包含标签、属性、文本内容、注释和处理指令等。 解析XML通常有两种主要方法:

  • DOM (Document Object Model): 将整个XML文档读入内存,构建一个树形结构。优点是易于遍历和操作,缺点是内存消耗大。
  • SAX (Simple API for XML): 以事件流的方式逐个报告解析到的XML组件(如“开始标签”、“文本”、“结束标签”),而不构建完整的树。优点是内存效率极高,适合处理大文件。

Zig的std.xml模块提供了一个SAX风格的流式解析器。

std.xml.Parser是核心的解析器结构体。它接收一个Reader(如文件或内存缓冲区)并提供一个next()方法,用于获取XML流中的下一个Token。

const std = @import("std");
const xml = std.xml;
pub fn main() !void {
const xml_data = "<root><item id=\"1\">Hello</item></root>";
var stream = std.io.fixedBufferStream(xml_data);
var parser = xml.Parser.init(stream.reader(), std.testing.allocator);
defer parser.deinit();
while (try parser.next()) |token| {
// 处理 token
}
}

parser.next()返回的Token是一个联合体(union),代表了不同类型的XML事件:

  • StartTag: 遇到了一个开始标签,如<item>。它包含标签名和属性列表。
  • EndTag: 遇到了一个结束标签,如</item>。
  • Text: 标签之间的文本内容。
  • Eof: 到达了输入的末尾。
  • 其他:Comment, ProcInst, Doctype等。
// ...接上文
while (try parser.next()) |token| {
switch (token) {
.StartTag => |tag| {
std.debug.print("Start Tag: {s}\n", .{tag.name});
for (tag.attributes) |attr| {
std.debug.print(" Attr: {s} = \"{s}\"\n", .{attr.name, attr.value});
}
},
.EndTag => |tag| {
std.debug.print("End Tag: {s}\n", .{tag.name});
},
.Text => |text| {
std.debug.print("Text: {s}\n", .{text});
},
.Eof => break,
else => {},
}
}

下面是一个简化的例子,用于从RSS feed中提取文章的标题。

const std = @import("std");
const xml = std.xml;
const rss_feed = """
<rss version="2.0">
<channel>
<title>My Blog</title>
<item>
<title>Zig is Awesome</title>
</item>
<item>
<title>Parsing XML in Zig</title>
</item>
</channel>
</rss>""";
pub fn main() !void {
var stream = std.io.fixedBufferStream(rss_feed);
var parser = xml.Parser.init(stream.reader(), std.testing.allocator);
defer parser.deinit();
var in_title_tag = false;
while (try parser.next()) |token| {
switch (token) {
.StartTag => |tag| {
if (std.mem.eql(u8, tag.name, "title")) {
in_title_tag = true;
}
},
.EndTag => |tag| {
if (std.mem.eql(u8, tag.name, "title")) {
in_title_tag = false;
}
},
.Text => |text| {
if (in_title_tag) {
std.debug.print("Found Title: {s}\n", .{text});
}
},
.Eof => break,
else => {},
}
}
}

5. 实践练习:构建一个简易的DOM

Section titled “5. 实践练习:构建一个简易的DOM”

基于std.xml.Parser,尝试构建一个简易的DOM树。你需要定义一个Node结构体,它可以包含一个标签名、属性和一个子Node的ArrayList。当解析器遇到StartTag时,创建一个新节点并将其压入栈中;遇到EndTag时,将节点从栈中弹出并添加到其父节点的子列表中。

  • 命名空间(Namespaces) std.xml解析器目前对XML命名空间的支持有限。它会将带前缀的标签(如<rss:item>)作为一个整体(rss:item)来报告,而不会自动解析URI。

  • 实体(Entities) 解析器会自动处理标准的XML实体,如&lt; (<), &gt; (>), &amp; (&)。对于自定义实体,你需要手动处理。

  • 验证 std.xml是一个非验证(non-validating)解析器。它不检查XML是否符合DTD或XML Schema等模式定义。

Zig的std.xml模块选择了SAX风格的API,这体现了Zig对性能和内存控制的重视。这种流式方法使得解析非常巨大的XML文件成为可能,而不会耗尽系统内存。

虽然SAX API在使用上比DOM API更复杂(因为它需要你手动维护状态,如in_title_tag),但它提供了极致的效率。对于需要构建完整文档树的场景,你可以在这个SAX解析器的基础上,像实践练习中那样,构建自己的DOM层。这种分层设计正是Zig强大灵活性的体现。