赞
踩
这篇文章用于介绍XML的基本知识,然后介绍C++的一个开源库pugixml用于操作xml,如果知道XML知识的直接跳转到【C++使用pugixml】阅读。
XML是可扩展标记语言(英語:ExtensibleMarkupLanguage,简称:XML)是一种标记语言。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语法注意点:
- <!-- 注释内容 -->
- <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 错误:
<message>if salary < 1000 then</message>
为了避免这个错误,请用实体引用来代替 "<" 字符:
<message>if salary < 1000 then</message>
在 XML 中,有 5 个预定义的实体引用:
< | < | less than |
---|---|---|
> | > | greater than |
& | & | ampersand |
' | ' | apostrophe |
" | " | quotation mark |
注释:在 XML 中,只有字符 "<" 和 "&" 确实是非法的。大于号是合法的,但是用实体引用来代替它是一个好习惯。
<!-- This is a comment -->
在 Windows 应用程序中,换行通常以一对字符来存储:回车符(CR)和换行符(LF)。
在 Unix 和 Mac OSX 中,使用 LF 来存储新行。
在旧的 Mac 系统中,使用 CR 来存储新行。
XML 以 LF 存储换行。
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 元素是可扩展的
在文档中添加新的信息后,应用程序不会中断或崩溃。
属性(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 属性仅仅是一个标识符,用于标识不同的便签。它并不是便签数据的组成部分。
在此我们极力向您传递的理念是:元数据(有关数据的数据)应当存储为属性,而数据本身应当存储为元素。
拥有正确语法的 XML 被称为"形式良好"的 XML。通过 DTD 验证的XML是"合法"的 XML。
在前面的章节描述的语法规则:
开源地址:
https://pugixml.orgpugixml.org/
下载源码,然后在项目中使用下面三个文件,在使用库的位置包含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)和一些附加数据(即名称)。
常见的节点类型:
尽管有几种节点类型,但只有三种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。
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";
- }
加载文件的函数:
- 加载文档 +----------- doc.load_file("tree.xml");
- +----------- doc.load_string(source);
- +----------- doc.load_string(source);
- +----------- doc.load(stream);
Pugixml提供了一个扩展的接口,用于从文档中获取各种类型的数据和遍历文档。可以使用各种访问器获取节点/属性数据,可以通过访问器或迭代器遍历子节点/属性列表,可以使用xml_tree_walker对象执行深度优先遍历,还可以使用XPath进行复杂的数据驱动查询。
您可以通过name()访问器获取节点或属性名,通过value()访问器获取值。注意,这两个函数都不会返回空指针——它们要么返回一个包含相关内容的字符串,要么返回一个空字符串(如果name/value缺失或句柄为空)。对于读取值,还有两个值得注意的事情:
- 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对象;上面的示例没有捕获这些异常。
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);
通常在创建新文档或加载现有文档并对其进行处理后,需要将结果保存回文件。此外,有时将整个文档或子树输出到某个流也很有用;用例包括调试打印,通过网络或其他面向文本的媒体序列化等。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对象或自定义写入器。保存子树与保存整个文档略有不同;阅读手册了解更多信息。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。