一个纯 Rust 实现的 libxml2 —— 开源世界中事实标准的 XML/HTML 解析库。
libxml2 于 2025 年 12 月正式停止维护,并存在已知的安全问题。xmloxide 旨在成为一个内存安全、高性能的替代方案,并通过相同的符合性测试套件。
unsafehtml5::sax),在不构建 DOM 树的情况下包装分词器css::select),包含组合器、伪类以及快速的 #id 查找matches()、replace()、tokenize()、upper-case()、lower-case()、abs()、min()、max() 等)serde 特性,用于 XML 与 Rust 类型之间的序列化/反序列化async 特性,用于从 tokio::io::AsyncRead 源进行解析xmllint CLI —— 用于解析、验证和查询 XML 的命令行工具Document 都是独立的,并且是 Send + Syncinclude/xmloxide.h),用于嵌入 C/C++ 项目encoding_rs(库本身零其他依赖;clap 仅用于 CLI)use xmloxide::Document;
let doc = Document::parse_str("<root><child>Hello</child></root>").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("root"));
assert_eq!(doc.text_content(root), "Hello");
use xmloxide::Document;
use xmloxide::serial::serialize;
let doc = Document::parse_str("<root><child>Hello</child></root>").unwrap();
let xml = serialize(&doc);
assert_eq!(xml, "<root><child>Hello</child></root>");
use xmloxide::Document;
use xmloxide::xpath::{evaluate, XPathValue};
let doc = Document::parse_str("<library><book><title>Rust</title></book></library>").unwrap();
let root = doc.root_element().unwrap();
let result = evaluate(&doc, root, "count(book)").unwrap();
assert_eq!(result.to_number(), 1.0);
use xmloxide::sax::{parse_sax, SaxHandler, DefaultHandler};
use xmloxide::parser::ParseOptions;
struct MyHandler;
impl SaxHandler for MyHandler {
fn start_element(&mut self, name: &str, _: Option<&str>, _: Option<&str>,
_: &[(String, String, Option<String>, Option<String>)]) {
println!("Element: {name}");
}
}
parse_sax("<root><child/></root>", &ParseOptions::default(), &mut MyHandler).unwrap();
use xmloxide::html::parse_html;
let doc = parse_html("<p>Hello <br> World").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("html"));
use xmloxide::css::select;
use xmloxide::Document;
let doc = Document::parse_str(r#"<div><p class="intro">Hello</p><p>World</p></div>"#).unwrap();
let root = doc.root_element().unwrap();
let intros = select(&doc, root, "p.intro").unwrap();
assert_eq!(intros.len(), 1);
assert_eq!(doc.text_content(intros[0]), "Hello");
use xmloxide::html5::parse_html5;
let doc = parse_html5("<p>Hello <b>world</b>").unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.node_name(root), Some("html"));
也支持片段解析(innerHTML 背后的算法):
use xmloxide::html5::{parse_html5_with_options, Html5ParseOptions};
let opts = Html5ParseOptions {
scripting: false,
fragment_context: Some("body".to_string()),
};
let doc = parse_html5_with_options("<p>fragment</p>", &opts).unwrap();
use xmloxide::html5::sax::{Html5SaxHandler, parse_html5_sax};
struct LinkExtractor { hrefs: Vec<String> }
impl Html5SaxHandler for LinkExtractor {
fn start_element(&mut self, name: &str, attrs: &[(String, String)], _sc: bool) {
if name == "a" {
if let Some((_, href)) = attrs.iter().find(|(n, _)| n == "href") {
self.hrefs.push(href.clone());
}
}
}
}
let mut handler = LinkExtractor { hrefs: Vec::new() };
parse_html5_sax(r#"<a href="/page">Link</a>"#, &mut handler);
assert_eq!(handler.hrefs, vec!["/page"]);
use xmloxide::parser::{parse_str_with_options, ParseOptions};
let opts = ParseOptions::default().recover(true);
let doc = parse_str_with_options("<root><unclosed>", &opts).unwrap();
for diag in &doc.diagnostics {
eprintln!("{}", diag);
}
# 解析并漂亮打印
xmllint --format document.xml
# 根据 schema 验证
xmllint --schema schema.xsd document.xml
xmllint --relaxng schema.rng document.xml
xmllint --schematron schema.sch document.xml
xmllint --dtdvalid schema.dtd document.xml
# XPath 查询
xmllint --xpath "//title" document.xml
# 标准 XML
xmllint --c14n document.xml
# 解析 HTML
xmllint --html page.html
解析吞吐量与 libxml2 相当 —— 大多数文档相差在 3-4% 以内,且在 SVG 上 快 12%。得益于基于竞技场的树设计,序列化 快 1.5-2.4 倍。在所有基准测试中,XPath 快 1.1-2.7 倍。
解析:
序列化:
XPath:
关键优化:基于竞技场的树实现快速序列化,用于字符验证的字节级预检查,批量文本扫描,名称解析的 ASCII 快速路径,零拷贝元素名称拆分,内联实体解析,XPath // 步骤融合与扩展轴融合,内联树访问器,以及子/后代轴的名称测试快速路径。
# 运行基准测试(需要 libxml2 系统库)
cargo bench --features bench-libxml2 --bench comparison_bench
cargo test --all-features
xmloxide 提供了一个 C 兼容的 API,用于嵌入 C/C++ 项目(如 Chromium、游戏引擎或任何当前使用 libxml2 的代码库)。
# 构建共享库 + 静态库(使用附带的 Makefile)
make
# 或单独构建:
make shared # .so / .dylib / .dll
make static # .a / .lib
# 构建并运行 C 示例
make example
#include "xmloxide.h"
xmloxide_document *doc = xmloxide_parse_str("<root>Hello</root>");
uint32_t root = xmloxide_doc_root_element(doc);
char *name = xmloxide_node_name(doc, root); // "root"
char *text = xmloxide_node_text_content(doc, root); // "Hello"
xmloxide_free_string(name);
xmloxide_free_string(text);
xmloxide_free_doc(doc);
完整的 API —— 包括树导航和修改、XPath 求值、序列化(普通和漂亮打印)、HTML/HTML5 解析、DTD/RelaxNG/XSD/Schematron 验证、C14N、SAX 流式、XmlReader、推送解析器和 XML 目录 —— 在 include/xmloxide.h 中声明。
线程安全: 与 libxml2 不同,xmloxide 没有全局状态。每个 Document 都是独立的,并且是 Send + Sync。FFI 层使用线程局部存储来存储最后一条错误消息 —— 每个线程都有自己的错误状态。无需初始化或清理函数。
xmloxide 包含用于安全测试的模糊测试目标:
# 安装 cargo-fuzz(需要 nightly)
cargo install cargo-fuzz
# 运行模糊测试目标
cargo +nightly fuzz run fuzz_xml_parse
cargo +nightly fuzz run fuzz_html_parse
cargo +nightly fuzz run fuzz_html5_parse
cargo +nightly fuzz run fuzz_html5_fragment
cargo +nightly fuzz run fuzz_xpath
cargo +nightly fuzz run fuzz_roundtrip
cargo +nightly fuzz run fuzz_sax
cargo +nightly fuzz run fuzz_reader
cargo +nightly fuzz run fuzz_push
cargo +nightly fuzz run fuzz_validation
cargo +nightly fuzz run fuzz_schematron
cargo build
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo bench
最低支持的 Rust 版本:1.81
PushParser)当前会缓冲所有推送的数据,并在 finish() 时执行完整解析,而不是像 libxml2 的 xmlParseChunk 那样真正流式处理。SAX 流式(XML 的 parse_sax,HTML5 的 html5::sax::parse_html5_sax)可作为内存受限的大文档处理的替代方案。namespace:: 轴 —— 当命名空间在作用域内匹配时,namespace:: 轴返回元素节点(而不是实例化单独的命名空间节点),遵循与属性轴相同的模式。参见 CONTRIBUTING.md 了解开发设置和指南。
参见 CHANGELOG.md 了解版本历史。
MIT
| 模块 | 描述 |
|---|
tree | 基于竞技场的 DOM 树(Document、NodeId、NodeKind) |
parser | XML 1.0 递归下降解析器,支持错误恢复 |
parser::push | 用于分块输入的推送/增量解析器 |
html | 容错的 HTML 4.01 解析器 |
html5 | WHATWG HTML 生活标准解析器(分词器 + 树构建器) |
html5::sax | HTML5 的流式 SAX 类 API(不构建 DOM 树) |
css | 用于查询文档树的 CSS 选择器引擎 |
sax | SAX2 流式事件驱动解析器 |
reader | XmlReader 拉取式解析 API |
serial | XML、HTML 和 HTML5 序列化器,以及标准 XML (C14N) |
xpath | XPath 1.0+ 表达式解析器和求值器 |
validation::dtd | DTD 解析和验证 |
validation::relaxng | RelaxNG 模式验证 |
validation::xsd | XML Schema (XSD) 验证 |
validation::schematron | ISO Schematron 基于规则的验证 |
serde_xml | Serde XML 序列化/反序列化(可选 serde 特性) |
async_xml | 通过 tokio::io::AsyncRead 进行异步解析(可选 async 特性) |
xinclude | XInclude 1.0 文档包含 |
catalog | OASIS XML 目录用于 URI 解析 |
encoding | 字符编码检测和转码 |
ffi | C/C++ FFI 绑定(include/xmloxide.h) |
| 文档 | 大小 | xmloxide | libxml2 | 结果 |
|---|
| Atom 订阅源 | 4.9 KB | 26.7 µs (176 MiB/s) | 25.5 µs (184 MiB/s) | 约慢 4% |
| SVG 绘图 | 6.3 KB | 58.5 µs (103 MiB/s) | 65.6 µs (92 MiB/s) | 快 12% |
| Maven POM | 11.5 KB | 76.9 µs (142 MiB/s) | 74.2 µs (148 MiB/s) | 约慢 4% |
| XHTML 页面 | 10.2 KB | 69.5 µs (139 MiB/s) | 61.5 µs (157 MiB/s) | 约慢 13% |
| 大文件 (374 KB) | 374 KB | 2.15 ms (169 MiB/s) | 2.08 ms (175 MiB/s) | 约慢 3% |
| 文档 | 大小 | xmloxide | libxml2 | 结果 |
|---|
| Atom 订阅源 | 4.9 KB | 11.3 µs | 17.5 µs | 快 1.5 倍 |
| Maven POM | 11.5 KB | 20.1 µs | 47.5 µs | 快 2.4 倍 |
| 大文件 (374 KB) | 374 KB | 614 µs | 1397 µs | 快 2.3 倍 |
| 表达式 | xmloxide | libxml2 | 结果 |
|---|
简单路径 (//entry/title) | 1.51 µs | 1.63 µs | 快 8% |
属性谓词 (//book[@id]) | 5.91 µs | 15.99 µs | 快 2.7 倍 |
count() 函数 | 1.09 µs | 1.67 µs | 快 1.5 倍 |
string() 函数 | 1.32 µs | 1.77 µs | 快 1.3 倍 |
| libxml2 | xmloxide (Rust) | xmloxide (C FFI) |
|---|
xmlReadMemory | Document::parse_str | xmloxide_parse_str |
xmlReadFile | Document::parse_file | xmloxide_parse_file |
xmlParseDoc | Document::parse_bytes | xmloxide_parse_bytes |
htmlReadMemory | html::parse_html | xmloxide_parse_html |
| (HTML5 解析) | html5::parse_html5 | — |
| (HTML5 片段 / innerHTML) | html5::parse_html5_with_options | — |
| (HTML5 流式) | html5::sax::parse_html5_sax | — |
(CSS 选择器 / querySelector) | css::select | — |
xmlFreeDoc | (丢弃 Document) | xmloxide_free_doc |
xmlDocGetRootElement | doc.root_element() | xmloxide_doc_root_element |
xmlNodeGetContent | doc.text_content(id) | xmloxide_node_text_content |
xmlNodeSetContent | doc.set_text_content(id, s) | xmloxide_set_text_content |
xmlGetProp | doc.attribute(id, name) | xmloxide_node_attribute |
xmlSetProp | doc.set_attribute(...) | xmloxide_set_attribute |
xmlNewNode | doc.create_node(...) | xmloxide_create_element |
xmlNewText | doc.create_node(Text{..}) | xmloxide_create_text |
xmlAddChild | doc.append_child(p, c) | xmloxide_append_child |
xmlAddPrevSibling | doc.insert_before(ref, c) | xmloxide_insert_before |
xmlUnlinkNode | doc.remove_node(id) | xmloxide_remove_node |
xmlCopyNode | doc.clone_node(id, deep) | xmloxide_clone_node |
xmlGetID | doc.element_by_id(s) | xmloxide_element_by_id |
xmlDocDumpMemory | serial::serialize(&doc) | xmloxide_serialize |
xmlDocDumpFormatMemory | serial::serialize_with_options | xmloxide_serialize_pretty |
htmlDocDumpMemory | serial::html::serialize_html | xmloxide_serialize_html |
xmlC14NDocDumpMemory | serial::c14n::canonicalize | xmloxide_canonicalize |
xmlXPathEvalExpression | xpath::evaluate | xmloxide_xpath_eval |
xmlValidateDtd | validation::dtd::validate | xmloxide_validate_dtd |
xmlRelaxNGValidateDoc | validation::relaxng::validate | xmloxide_validate_relaxng |
xmlSchemaValidateDoc | validation::xsd::validate_xsd | xmloxide_validate_xsd |
| (Schematron 验证) | validation::schematron::validate_schematron | xmloxide_validate_schematron |
xmlXIncludeProcess | xinclude::process_xincludes | xmloxide_process_xincludes |
xmlLoadCatalog | Catalog::parse | xmloxide_parse_catalog |
xmlSAX2... 回调 | sax::SaxHandler trait | xmloxide_sax_parse |
xmlTextReaderRead | reader::XmlReader | xmloxide_reader_read |
xmlCreatePushParserCtxt | parser::PushParser | xmloxide_push_parser_new |
xmlParseChunk | PushParser::push | xmloxide_push_parser_push |