0


[C++]XML-C++开源库pugixml使用

这篇文章用于介绍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 &lt; 1000 then</message>

在 XML 中,有 5 个预定义的实体引用:
<<less than>>greater than&&ampersand''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)和一些附加数据(即名称)。

常见的节点类型:

  1. 文档节点【Document node(node_document)——这是树的根,由几个子节点组成。该节点对应于xml_document类;注意,xml_documentxml_node的一个子类,因此整个节点接口也是可用的。
  2. 元素/标记节点【Element/tag node(node_element)——这是最常见的节点类型,表示XML元素。元素节点有一个名称、一组属性和一组子节点(两者都可以为空)。属性是一个简单的名称/值对。
  3. 纯字符数据节点【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_documentxml_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对象或自定义写入器。保存子树与保存整个文档略有不同;阅读手册了解更多信息。

标签: c++ xml 开源

本文转载自: https://blog.csdn.net/FL1768317420/article/details/136385775
版权归原作者 FL1768317420 所有, 如有侵权,请联系我们删除。

“[C++]XML-C++开源库pugixml使用”的评论:

还没有评论