文章介绍了xml协议的组成以及C++ xml解析库pugixml的常用操作。源于开发中每次遇到xml操作时,都要回过头查看pugixml库常用操作时什么样的,能不能有个更深刻和清晰的认识呢?其实搞清楚xml结构和pugixml组织结构的对照关系,以及pugixml中节点、属性的增删改查逻辑,可以帮助我们快速回忆起这些东西。遂,本文留作查询使用。
XML协议
XML(Extensible Markup Language)是一种可扩展标记语言,在后端开发中,常作为一种数据格式进行消息的传输、存储。我们先来了解一下XML协议通常包含哪些内容。
<?xml version="1.0" encoding="UTF-8"?>
xml声明常位于文件头,指明xml版本、编码、standalone。
- version: 指明xml版本
- encoding: 指明文本编码
- standalone: “yes” 表示不需要依赖外部文件,"no"表示依赖外部文件,一般用于schema校验等(后端目前没碰到这种场景,不再深究)
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
pi信息通常是指明样式表给到浏览器使用。
<!DOCTYPE library [
<!ELEMENT library (book+)>
<!ELEMENT book (title, author, description)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (firstName, lastName)>
<!ELEMENT firstName (#PCDATA)>
<!ELEMENT lastName (#PCDATA)>
<!ELEMENT description (#PCDATA)>
]>
文档类型,对xml内容的解释,常用于浏览器校验xml内容是否合法。
xml正文是由一个个节点组成,每个节点可以包含若干子节点、属性。下图给出了一个xml示例及其组成,最右侧是对应到pugixml库中的节点类型。
注意这里的data和cdata, 后者是原始文本不会被xml解析,例如有时我们的属性包含一些特殊字符,这些字符可能会破坏xml结构,此时要作用cdatta使用。
pugixml解析库
pugixml项目,是一个C++ xml解析库,主要实现包含在pugixml.h pugixml.cpp pugiconfig.hpp 三个文件中,提供了xml解析、生成、xpath等功能。主要包含以下三个类:
- xml_document: 对应着一个xml文档
- xml_node: 对应着xml中的节点
- xml_attribute: xml节点的属性
解析
pugixml解析提供从字符流、字符串、buffer、文件这几种方式加载,解析就是生成和初始化xml_document的过程。
下面是一个示例,
const std::string src = R"(
<Document user_id="5001">
<display>
<mode>1</mode>
<item color="blue" name="global" />
<item color="red" name="global" />
</display>
</Document>
)";
pugi::xml_document doc;
// 从字符串加载,不能指定编码类型
pugi::xml_parse_result result = doc.load_string(src.c_str(), pugi::parse_default);
// 从buffer中加载,可以指定buffer中的编码类型
result = doc.load_buffer(src.c_str(), src.size(), pugi::parse_default, pugi::encoding_utf8);
// 从本地文件中加载
result = doc.load_file("./test.xml", pugi::parse_default, pugi::encoding_utf8);
if (result.status == pugi::xml_parse_status::status_ok) {
doc.print(std::cout);
} else {
std::cout << result.description() << std::endl;
}
load返回的是parse_result类型,该类型记录了加载的状态码、编码、加载偏移、描述信息等。
struct PUGIXML_CLASS xml_parse_result {
// Parsing status (see xml_parse_status)
xml_parse_status status;
// Last parsed offset (in char_t units from start of input data)
ptrdiff_t offset;
// Source document encoding
xml_encoding encoding;
// Default constructor, initializes object to failed state
xml_parse_result();
// Cast to bool operator
operator bool() const;
// Get error description
const char* description() const;
};
查询
xml_document 继承自xml_node, 除了父类方法外,自己实现了load,reset这些逻辑,其他接口和父类一致,所以增删改查也就是xml_node的增删改查。增删改是在查到指定节点后进行set操作,所以掌握查询方法更为关键。
<Document user_id="5001" >
<display>
<mode>1</mode>
<item color="blue" name="global" />
<item color="red" name="global" />
</display>
</Document>
- 根据节点名称获取节点
例如查询mode节点相关的信息:
pugi::xml_node node = doc.child("Document").child("display").child("mode");
std::cout << "name=" << node.name() << ",text=" << node.text() << std::endl;
- 根据xpath获取节点(
select_node
)
这里需要明确,select返回的是xpath_node类型,该类型可能包含的是节点,也可能是属性,需要调用方清晰知道到底是哪一种类型以便调用不同的方法获取值。
pugi::xpath_node xpath_result = doc.select_node("/Document/display/mode");
std::cout << "name=" << xpath_result.node().name()
<< ",text=" << xpath_result.node().text() << std::endl;
- 根据属性名获取属性(
attribute("name")
)
std::cout << doc.child("Document").attribute("user_id").as_string() << std::endl;
- 根据xpath获取属性(
/.../node/@key
)
pugi::xpath_node first_item =
doc.select_node("/Document/display/item/@color");
std::cout << "name=" << first_item.attribute().name()
<< ",text=" << first_item.attribute().value() << std::endl;
- 获取指定属性的节点(
/.../node[@attr='val']
)
auto ret = doc.select_node("/Document/display/item[@color='blue']");
std::cout << "name=" << ret.node().name() << std::endl;
- 遍历节点
提供了循环遍历的迭代器,也可以使用for-each。遍历属性与此类似,不再赘述
// 1. children
{
auto children = display.children();
for (const auto &ele : children) {
std::cout << ele.name() << std::endl;
}
}
{
auto children = display.children("item");
for (const auto &ele : children) {
std::cout << ele.name() << std::endl;
}
}
// 2. itr
for (auto itr = display.begin(); itr != display.end(); ++itr) {
std::cout << itr->name() << std::endl;
}
修改
对于一个节点来说,有哪些内容呢。节点本身的name, text, 以及包含的属性(属性名称、属性值)。
注意节点的text同样是一个结构体,不存在直接的set_text()方法
插入节点时提供了一些带位置参数的接口,例如在某节点之前和之后插入。
pugi::xml_node ret =
doc.select_node("/Document/display/item[@color='blue']").node();
ret.set_name("item_new");
ret.text().set("abs");
// ret.set_text();
// 注意这里,设置text值,text是一个结构,不存在直接的set_text()方法
ret.set_value("abs");
ret.attribute("color").set_value("new blue"); // 修改已有属性值
ret.append_attribute("modify").set_value("2024"); // 添加新的属性
auto new_node = ret.parent().append_child("item"); // 在其后追加节点
new_node.append_attribute("color").set_value("white");
doc.print(std::cout);
删除
- xml_document 重置状态时reset接口。
- remove_child、remove_attribute 对应移除单个子节点和属性;remove_children、remove_attributes对应移除所有子节点和属性。
序列化
将document生成字符串的过程
- 字符流
format参数可以自定义,这里取得是不进行格式化。
std::stringstream out;
doc.save(out, "\t", pugi::format_raw);
std::cout << out.str() << std::endl;
- 字符串
class XmlStringWriter : public pugi::xml_writer {
public:
void write(const void *data, size_t size) {
result.assign(reinterpret_cast<const char *>(data), size);
}
std::string result;
};
XmlStringWriter writer;
doc.save(writer);
std::cout << writer.result << std::endl;
总结
首先,使用pugixml时要注意,该库支持unicode编码,是不支持gbk编码的,解析时对于gbk类型要自己进行转码。
其次,pugixml提供了一些解析的选项,默认选项有时可能会出问题,例如value包含tab时,默认的反序列化参数可能会直接消除tab,关于这些选项(const unsigned int parser_x
这些参数),仓库由比较明细的解释。同样,序列化时也有一些选项(const unsigned int format_xx
)。
然后,要快速掌握该库的使用,需要明确xml包含哪些结构,例如版本头、节点、属性、注释等,以及对应到pugixml库是哪些结构。需要留意的是对于属性值、text值都是一个结构体,你要知道你操作的不是一个简单的string类型。
最后,该库提供了xpath功能,select_node和select_nodes 返回值可能是包含的节点也可能是属性,调用者要知道自己在筛选什么。而对于xpath至少要搞清楚如何查节点(/.../node
)、查属性(/.../node/@attr
)、查指定属性的节点((/.../node[@attr='val']
)这几个xpath格式是什么样的。