这篇文章用于介绍XML的基本知识,然后介绍C++的一个开源库pugixml用于操作xml,如果知道XML知识的直接跳转到【C++使用pugixml】阅读。
一、XML基本知识
1.xml介绍
XML是可扩展标记语言(英語:ExtensibleMarkupLanguage,简称:XML)是一种标记语言。XML 被设计用来结构化、传输和存储数据,而不是显示数据。
2.xml语法
参考资源:XML 树结构 | 菜鸟教程
XML的语法很简单。XML 文档第一行以 XML 声明开始,用来表述文档的一些信息,如:
<?xml version="1.0" encoding="UTF-8"?>
XML使用标签(<key> [content] </key>)的方式传递信息
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
可以看到标签可以嵌套。XML 语言没有预定义标签,XML 允许创作者定义自己的标签和自己的文档结构。
解释上面的内容:
第一行是 XML 声明。它定义 XML 的版本(1.0)和所使用的编码(UTF-8 : 万国码, 可显示各种语言)。
<note>是文档的根元素,接下来的几行描述根元素的子元素。所有元素都是以标签对的方式出现。
XML 文档形成一种树结构
XML 文档必须包含根元素。该元素是所有其他元素的父元素。XML 文档中的元素形成了一棵文档树。这棵树从根部开始,并扩展到树的最底端。
<root>
<child>
<subchild>.....</subchild>
</child>
<child1>
<subchild>.....</subchild>
</child1>
</root>
父、子以及同胞等术语用于描述元素之间的关系。父元素拥有子元素。相同层级上的子元素成为同胞(兄弟或姐妹)。
编辑
添加图片注释,不超过 140 字(可选)
XML语法注意点:
- XML 文档必须有根元素
- 所有的 XML 元素都必须有一个关闭标签
- XML 标签对大小写敏感
- XML 必须正确嵌套
- XML 属性值必须加引号,XML 元素也可拥有属性(名称/值的对)
<!-- 注释内容 -->
<note date=12/11/2007> <!--错误-->
<to>Tove</to>
<from>Jani</from>
</note>
<note date="12/11/2007"> <!--正确-->
<to>Tove</to>
<from>Jani</from>
</note>
- 实体引用。在 XML 中,一些字符拥有特殊的意义
如果您把字符 "<" 放在 XML 元素中,会发生错误,这是因为解析器会把它当作新元素的开始
这样会产生 XML 错误:
<message>if salary < 1000 then</message>
为了避免这个错误,请用实体引用来代替 "<" 字符:
<message>if salary < 1000 then</message>
在 XML 中,有 5 个预定义的实体引用:
<<less than>>greater than&&ersand''apostrophe""quotation mark
注释:在 XML 中,只有字符 "<" 和 "&" 确实是非法的。大于号是合法的,但是用实体引用来代替它是一个好习惯。
- XML 中的注释
<!-- This is a comment -->
- 在 XML 中,空格会被保留。在 XML 中,文档中的空格不会被删减。
- XML 以 LF 存储换行
在 Windows 应用程序中,换行通常以一对字符来存储:回车符(CR)和换行符(LF)。
在 Unix 和 Mac OSX 中,使用 LF 来存储新行。
在旧的 Mac 系统中,使用 CR 来存储新行。
XML 以 LF 存储换行。
3.XML元素
XML 元素指的是从(且包括)开始标签直到(且包括)结束标签的部分。
一个元素可以包含:
- 其他元素
- 文本
- 属性
- 或混合以上所有...
<bookstore>
<book category="CHILDREN">
<title>Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title>Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
<bookstore> 和 <book> 都有元素内容,因为他们包含其他元素。<book> 元素也有属性(category="CHILDREN")。<title>、<author>、<year> 和 <price> 有文本内容,因为他们包含文本。
XML 命名规则
XML 元素是可扩展的
在文档中添加新的信息后,应用程序不会中断或崩溃。
4.XML元素
属性(Attribute)提供有关元素的额外信息。属性值必须被引号包围,不过单引号和双引号均可使用。
<person sex="female">
<!-- 或者 -->
<person sex='female'>
元素 vs 属性
<person sex="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
<person>
<sex>female</sex>
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
在第一个实例中,sex 是一个属性。在第二个实例中,sex 是一个元素。这两个实例都提供相同的信息。
没有什么规矩可以告诉我们什么时候该使用属性,而什么时候该使用元素。但是尽量使用元素。属性难以阅读和维护,请尽量使用元素来描述数据。
针对元数据的 XML 属性
有时候会向元素分配 ID 引用。这些 ID 索引可用于标识 XML 元素
<messages>
<note id="501">
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
<note id="502">
<to>Jani</to>
<from>Tove</from>
<heading>Re: Reminder</heading>
<body>I will not</body>
</note>
</messages>
上面的 id 属性仅仅是一个标识符,用于标识不同的便签。它并不是便签数据的组成部分。
在此我们极力向您传递的理念是:元数据(有关数据的数据)应当存储为属性,而数据本身应当存储为元素。
5.XML 验证
拥有正确语法的 XML 被称为"形式良好"的 XML。通过 DTD 验证的XML是"合法"的 XML。
在前面的章节描述的语法规则:
- XML 文档必须有一个根元素
- XML元素都必须有一个关闭标签
- XML 标签对大小写敏感
- XML 元素必须被正确的嵌套
- XML 属性值必须加引号
二、C++使用pugixml
开源地址:
https://pugixml.orgpugixml.org/
1.使用方法
下载源码,然后在项目中使用下面三个文件,在使用库的位置包含pugixml.hpp头文件,当然也可以编译出库(使用CMake),我相信大家都会。因为比较简单,所以下面我使用的是直接添加源文件。
pugixml.hpp
pugiconfig.hpp
pugixml.cpp
文档对象模型
pugixml以类似dom的方式存储XML数据:整个XML文档(包括文档结构和元素数据)以树的形式存储在内存中。树可以从字符流(文件、字符串、c++ I/O流)加载,然后通过特殊的API或XPath表达式遍历。整个树是可变的:节点结构和节点/属性数据都可以在任何时候改变。最后,文档转换的结果可以保存到字符流(文件、c++ I/O流或自定义传输)。
树的根是文档本身,它对应于c++类型xml_document。文档有一个或多个子节点,对应于c++类型xml_node。节点有不同的类型;根据类型的不同,一个节点可以有一组子节点、一组属性(对应于c++类型xml_attribute)和一些附加数据(即名称)。
常见的节点类型:
- 文档节点【Document node】(node_document)——这是树的根,由几个子节点组成。该节点对应于xml_document类;注意,xml_document是xml_node的一个子类,因此整个节点接口也是可用的。
- 元素/标记节点【Element/tag node】(node_element)——这是最常见的节点类型,表示XML元素。元素节点有一个名称、一组属性和一组子节点(两者都可以为空)。属性是一个简单的名称/值对。
- 纯字符数据节点【Plain character data nodes】(node_pcdata)表示XML中的纯文本。PCDATA节点有一个值,但没有名称或子/属性。注意,纯字符数据不是元素节点的一部分,而是有自己的节点;例如,一个元素节点可以有几个子PCDATA节点。
尽管有几种节点类型,但只有三种c++类型表示树(xml_document、xml_node、xml_attribute);xml_node上的某些操作仅对某些节点类型有效。它们描述如下。
所有pugixml类和函数都位于pugi命名空间中;您必须使用显式的名称限定(即。Pugi:: xml_node),或者通过using指令来访问相关的符号(即。using pugi:: xml_node;或using namespace pugi;)。
xml_document是整个文档结构的所有者;销毁文档会破坏整个树。xml_document的接口由加载函数、保存函数和xml_node的整个接口组成,该接口允许文档检查和/或修改。注意,虽然xml_document是xml_node的子类,但xml_node不是多态类型;提供继承只是为了简化使用。
xml_node是文档节点的句柄;它可以指向文档中的任何节点,包括文档本身。所有类型的节点都有一个通用接口。注意,xml_node只是实际节点的句柄,而不是节点本身——可以有多个xml_node句柄指向同一个底层对象。销毁xml_node句柄不会销毁该节点,也不会从树中删除它。
xml_node类型有一个特殊值,称为null node或empty node。它不对应于任何文档中的任何节点,因此类似于空指针。然而,所有的操作都是在空节点上定义的;一般来说,这些操作不做任何事情,只返回空节点/属性或空字符串作为结果。这对于链接调用很有用;例如,你可以像这样获取一个节点的祖父节点:node.parent().parent();如果一个节点是空节点或者它没有父节点,第一个parent()调用返回空节点;第二个parent()调用也返回空节点,因此您不必两次检查错误。你可以通过隐式布尔转换来测试句柄是否为空if (node) { … }orif (!node) { … }。
xml_attribute是一个XML属性的句柄;它具有与xml_node相同的语义,即可以有多个xml_attribute句柄指向相同的底层对象,并且有一个特殊的null属性值,该值传播到函数结果。
在配置pugixml时,接口和内部表示有两种选择:您可以选择UTF-8(也称为char)接口或UTF-16/32(也称为wchar_t)接口。选择通过PUGIXML_WCHAR_MODE 定义;你可以通过pugiconfig.hpp或预处理器选项来设置它。所有处理字符串的树函数都可以处理c风格的空结束字符串或所选字符类型的STL字符串。有关Unicode接口的其他信息,Read the manual。
2.加载文件
pugixml提供了几个函数,用于从不同的位置文件、c++ iostreams、内存缓冲区加载XML数据。所有函数都使用非常快速的非验证解析器。这个解析器不完全符合W3C标准——它可以加载任何有效的XML文档,但不执行一些格式良好的检查。虽然在拒绝无效XML文档方面做了大量工作,但由于性能原因,有些验证没有执行。XML数据在解析之前总是转换为内部字符格式。pugixml支持所有流行的Unicode编码(UTF-8, UTF-16(大小端序),UTF-32(大小端序);UCS-2自然得到支持,因为它是UTF-16的严格子集)并自动处理所有编码转换。
XML数据最常见的来源是文件;pugixml提供了一个单独的函数,用于从文件加载XML文档。这个函数接受文件路径作为它的第一个参数,还有两个可选参数,它们指定解析选项和输入数据编码,这些在手册中有描述。
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("tree.xml");
std::cout << "Load result: " << result.description() << ", mesh name: " << doc.child("mesh").attribute("name").value() << std::endl;
load_file以及其他加载函数会销毁现有的文档树,然后尝试从指定的文件加载新树。操作的结果以xml_parse_result对象的形式返回;该对象包含操作状态和相关信息(例如,如果解析失败,最后一次成功解析在输入文件中的位置)。
解析结果对象可以隐式转换为bool类型;如果你不想彻底处理解析错误,你可以检查load函数的返回值,就好像它是一个bool:if (doc.load_file("file.xml")) { … } else { … }。否则,可以使用status成员获取解析状态,或者使用description()成员函数获取字符串形式的状态。这是一个处理加载错误的例子(samples/load error handling.cpp):
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_string(source);
if (result)
{
std::cout << "XML [" << source << "] parsed without errors, attr value: [" << doc.child("node").attribute("attr").value() << "]\n\n";
}
else
{
std::cout << "XML [" << source << "] parsed with errors, attr value: [" << doc.child("node").attribute("attr").value() << "]\n";
std::cout << "Error description: " << result.description() << "\n";
std::cout << "Error offset: " << result.offset << " (error at [..." << (source + result.offset) << "]\n\n";
}
加载文件的函数:
- load_string( )
- load_file( )
- load_buffer( )
加载文档 +----------- doc.load_file("tree.xml");
+----------- doc.load_string(source);
+----------- doc.load_string(source);
+----------- doc.load(stream);
3.访问文档数据
Pugixml提供了一个扩展的接口,用于从文档中获取各种类型的数据和遍历文档。可以使用各种访问器获取节点/属性数据,可以通过访问器或迭代器遍历子节点/属性列表,可以使用xml_tree_walker对象执行深度优先遍历,还可以使用XPath进行复杂的数据驱动查询。
您可以通过**name()访问器获取节点或属性名,通过value()**访问器获取值。注意,这两个函数都不会返回空指针——它们要么返回一个包含相关内容的字符串,要么返回一个空字符串(如果name/value缺失或句柄为空)。对于读取值,还有两个值得注意的事情:
- 通常将数据存储为某个节点的文本内容<node><description>This is a node</description></node> 。在这种情况下,<description>node没有值,而是有一个type node_pcdata的子节点,值为"This is a node"。Pugixml提供了child_value()和text()帮助函数来解析这些数据。
- 在许多情况下,属性值的类型不是字符串——例如,一个属性可能总是包含应该被视为整数的值,尽管它们在XML中表示为字符串。Pugixml提供了几种将属性值转换为其他类型的访问器。
for (pugi::xml_node tool = tools.child("Tool"); tool; tool = tool.next_sibling("Tool"))
{
std::cout << "Tool " << tool.attribute("Filename").value();
std::cout << ": AllowRemote " << tool.attribute("AllowRemote").as_bool();
std::cout << ", Timeout " << tool.attribute("Timeout").as_int();
std::cout << ", Description '" << tool.child_value("Description") << "'\n";
}
由于许多文档遍历都是查找具有正确名称的节点/属性,因此有专门的函数用于此目的。例如,child("Tool")返回第一个名为"Tool"的节点,如果没有这样的节点,则返回空句柄。例子:
std::cout << "Tool for *.dae generation: " << tools.find_child_by_attribute("Tool", "OutputFileMasks", "*.dae").attribute("Filename").value() << "\n";
for (pugi::xml_node tool = tools.child("Tool"); tool; tool = tool.next_sibling("Tool"))
{
std::cout << "Tool " << tool.attribute("Filename").value() << "\n";
}
子节点列表和属性列表是简单的双链表;虽然可以使用previous_sibling/next_sibling和其他类似的函数进行迭代,但pugixml还提供了节点和属性迭代器,因此可以将节点视为其他节点或属性的容器。所有迭代器都是双向的,并且支持所有常用的迭代器操作。如果迭代器所指向的节点/属性对象从树中移除,迭代器将失效;添加节点/属性不会使任何迭代器失效。例子:
for (pugi::xml_node_iterator it = tools.begin(); it != tools.end(); ++it)
{
std::cout << "Tool:";
for (pugi::xml_attribute_iterator ait = it->attributes_begin(); ait != it->attributes_end(); ++ait)
{
std::cout << " " << ait->name() << "=" << ait->value();
}
std::cout << std::endl;
}
如果你的c++编译器支持基于范围的for循环(这是c++ 11的一个特性,您可以使用它来枚举node/属性。
for (pugi::xml_node tool: tools.children("Tool"))
{
std::cout << "Tool:";
for (pugi::xml_attribute attr: tool.attributes())
{
std::cout << " " << attr.name() << "=" << attr.value();
}
for (pugi::xml_node child: tool.children())
{
std::cout << ", child " << child.name();
}
std::cout << std::endl;
}
上面描述的方法允许遍历某些节点的直接子节点;如果你想做一个深度树遍历,你将不得不通过递归函数或一些等效的方法来做。但是,pugixml为深度优先遍历子树提供了一个帮助器。为了使用它,必须实现xml_tree_walker接口并调用遍历函数。
struct simple_walker: pugi::xml_tree_walker
{
virtual bool for_each(pugi::xml_node& node)
{
for (int i = 0; i < depth(); ++i) std::cout << " "; // indentation
std::cout << node_types[node.type()] << ": name='" << node.name() << "', value='" << node.value() << "'\n";
return true; // continue traversal
}
};
simple_walker walker;
doc.traverse(walker);
最后,对于复杂的查询,通常需要更高级别的DSL。pugixml为此类查询提供了XPath 1.0语言的实现。关于XPath用法的完整描述可以在手册中找到,这里有一些例子:
pugi::xpath_node_set tools = doc.select_nodes("/Profile/Tools/Tool[@AllowRemote='true' and @DeriveCaptionFrom='lastparam']");
std::cout << "Tools:\n";
for (pugi::xpath_node_set::const_iterator it = tools.begin(); it != tools.end(); ++it)
{
pugi::xpath_node node = *it;
std::cout << node.node().attribute("Filename").value() << "\n";
}
pugi::xpath_node build_tool = doc.select_node("//Tool[contains(Description, 'build system')]");
if (build_tool)
std::cout << "Build tool: " << build_tool.node().attribute("Filename").value() << "\n";
注意:XPath函数错误时会抛出xpath_exception对象;上面的示例没有捕获这些异常。
4.修改文件数据
pugixml中的文档是完全可变的:您可以完全更改文档结构并修改节点/属性的数据。所有函数本身都负责内存管理和结构完整性,因此它们总是产生结构上有效的树——然而,也有可能创建无效的XML树(例如,通过添加具有相同名称的两个属性或通过将属性/节点名称设置为空/无效字符串)。树修改针对性能和内存消耗进行了优化,因此如果您有足够的内存,您可以使用pugixml从头创建文档,然后将它们保存到文件/流中,而不是依赖于容易出错的手动文本写入,并且没有太多的开销。
所有改变节点/属性数据或结构的成员函数都是非常量,因此不能在常量句柄上调用。然而,通过简单的赋值,你可以轻松地将常量句柄转换为非常量句柄:void foo(const pugi::xml_node& n) {pugi::xml_node nc = n;},所以这里的常量正确性主要提供额外的记录。
如前所述,节点可以有名称和值,它们都是字符串。根据节点类型,名称或值可能不存在。可以使用set_name和set_value成员函数进行设置。属性也可以使用类似的函数;然而,set_value函数对于除字符串以外的其他类型(如浮点数)是重载的。另外,属性值可以使用赋值操作符设置。这是设置节点/属性名称和值的示例:
pugi::xml_node node = doc.child("node");
// change node name
std::cout << node.set_name("notnode");
std::cout << ", new node name: " << node.name() << std::endl;
// change comment text
std::cout << doc.last_child().set_value("useless comment");
std::cout << ", new comment text: " << doc.last_child().value() << std::endl;
// we can't change value of the element or name of the comment
std::cout << node.set_value("1") << ", " << doc.last_child().set_name("2") << std::endl;
pugi::xml_attribute attr = node.attribute("id");
// change attribute name/value
std::cout << attr.set_name("key") << ", " << attr.set_value("345");
std::cout << ", new attribute: " << attr.name() << "=" << attr.value() << std::endl;
// we can use numbers or booleans
attr.set_value(1.234);
std::cout << "new attribute value: " << attr.value() << std::endl;
// we can also use assignment operators for more concise code
attr = true;
std::cout << "final attribute value: " << attr.value() << std::endl;
如果没有文档树,节点和属性就不存在,因此如果不将它们添加到某个文档中,就不能创建它们。节点或属性可以创建在节点/属性列表的末尾,也可以创建在其他节点之前或之后。所有插入函数成功时返回新创建对象的句柄,失败时返回空句柄。即使操作失败(例如,如果您试图将一个子节点添加到PCDATA节点),文档仍保持一致状态,但不会添加所请求的节点/属性。
注意 attribute()和child()函数不会向树中添加属性或节点,因此代码类似于node.attribute ("id") = 123;如果节点没有名称为“id”的属性,则不执行任何操作。如果需要的话,通过添加已有的属性/节点来确保您正在操作它们。
这是一个向文档添加新属性/节点的示例:
// add node with some name
pugi::xml_node node = doc.append_child("node");
// add description node with text child
pugi::xml_node descr = node.append_child("description");
descr.append_child(pugi::node_pcdata).set_value("Simple node");
// add param node before the description
pugi::xml_node param = node.insert_child_before("param", descr);
// add attributes to param node
param.append_attribute("name") = "version";
param.append_attribute("value") = 1.1;
param.insert_attribute_after("type", param.attribute("name")) = "float";
如果您不希望文档包含某些节点或属性,可以使用remove_attribute和remove_child函数将其删除。删除属性或节点会使指向同一底层对象的所有句柄失效,也会使指向同一对象的所有迭代器失效。删除node还会使其属性或子节点列表的所有遍历迭代器失效。注意确保所有这样的句柄和迭代器在删除属性/节点后不存在或不使用。
这是一个从文档中删除属性/节点的示例:
// remove description node with the whole subtree
pugi::xml_node node = doc.child("node");
node.remove_child("description");
// remove id attribute
pugi::xml_node param = node.child("param");
param.remove_attribute("value");
// we can also remove nodes/attributes by handles
pugi::xml_attribute id = param.attribute("name");
param.remove_attribute(id);
5.保存文件
通常在创建新文档或加载现有文档并对其进行处理后,需要将结果保存回文件。此外,有时将整个文档或子树输出到某个流也很有用;用例包括调试打印,通过网络或其他面向文本的媒体序列化等。pugixml提供了几个函数来将文档的任何子树输出到文件、流或其他通用传输接口;这些函数允许自定义输出格式,并执行必要的编码转换。
在写入目标之前,根据节点类型正确格式化节点/属性数据;所有特殊的XML符号,例如<,&被正确地转义了。为了防止忘记节点/属性名,将空的节点/属性名打印为":anonymous"。为了得到格式良好的输出,请确保所有节点和属性名都设置为有意义的值。
如果想要将整个文档保存到一个文件中,可以使用save_file函数,如果保存成功,该函数将返回true。这是一个简单的XML文档保存到文件的例子:
// save document to file
std::cout << "Saving result: " << doc.save_file("save_file_output.xml") << std::endl;
为了增强互操作性,pugixml提供了将文档保存到任何实现了c++ std::ostream接口的对象的函数。这允许你将文档保存到任何标准的c++流(即文件),最值得注意的是,这允许简单的调试输出,因为你可以使用std::cout流作为保存目标。有两个函数,一个处理窄字符流,另一个处理宽字符流。流)或任何第三方兼容的实现(即Boost Iostreams)。
// save document to standard output
std::cout << "Document:\n";
doc.save(std::cout);
以上所有的保存功能都是通过写入器接口实现的。这是一个具有单一函数的简单接口,在以文档数据块作为输入的输出过程中多次调用该函数。为了通过一些自定义传输(例如套接字)输出文档,您应该创建一个实现xml_writer_file接口的对象,并将其传递给xml_document::save函数。
这是一个简单的自定义写入器的例子,用于保存文档数据到STL字符串;阅读示例代码以获得更复杂的示例:
struct xml_string_writer: pugi::xml_writer
{
std::string result;
virtual void write(const void* data, size_t size)
{
result.append(static_cast<const char*>(data), size);
}
};
虽然前面描述的函数将整个文档保存到目标,但保存单个子树很容易。而不是调用xml_document::save,只需在目标节点上调用xml_node::print函数。您可以通过这种方式将节点内容保存到c++ IOstream对象或自定义写入器。保存子树与保存整个文档略有不同;阅读手册了解更多信息。
版权归原作者 FL1768317420 所有, 如有侵权,请联系我们删除。