Portable Elmax:C++ XML DOM 解析器






4.64/5 (18投票s)
关于跨平台 C++ XML DOM 库的教程
目录
- 引言
- 写入元素
- 读取元素
- 写入属性
- 读取属性
- 写入注释
- 读取注释
- 写入 CDATA 部分
- 读取 CDATA 部分
- 命名空间
- Collection
- 迭代器
- C++ LINQ
- 预定义宏
- 结论
- 历史
- 0.9.5 版本中的重大更改
引言
Portable Elmax 是一个用 C++ 编写的跨平台、非验证性 XML DOM 解析器。在此版本之前,还有一个基于 MSXML 的非跨平台版本。为避免混淆,本文将该版本称为 MS Elmax。MS Elmax 在 API 边界提供对 MFC CString
的表面支持(意味着在任何 string
处理之前,CString
会被转换为 STL string
),而 Portable Elmax 可以通过在 config.h 文件中定义 ELMAX_USE_MFC_CSTRING
来使其原生支持 MFC CString
。本文是对 Portable Elmax 的简短教程。虽然 Portable Elmax 和 MS Elmax 在 API 调用方面非常相似,但 Portable Elmax **不是** MS Elmax 的即插即用替代品;用户必须了解一些关键差异才能正确有效地使用该库。
写入元素
让我们看看如何创建一个整数值并将其写入一个元素。下一段将进行解释。
#include "../PortableElmax/Elmax.h"
void WriteElement(std::string& xml)
{
using namespace Elmax;
RootElement root("Products");
root.Create("Product").Create("Qty").SetInt32(1234);
xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
}
代码的第一行包含 Elmax.h 头文件,其中包含了您所需的所有 XML 类。没有文档类。每个 Element
对象兼具读取和保存 XML 到文件或 string
的文档功能。与 MS Elmax 的主要区别在于,root
必须在构造函数中命名,否则在解析要检索的元素时会导致错误。与 MS Elmax 不同,无需调用 SetDomDoc
或 SetConverter
;该库使用 Boost lexical_cast
执行数据类型转换。[]
运算符始终返回第一个子节点;要检索子节点,应调用 GetChildren
。任何从根节点分离的元素都必须调用 Destroy
函数。Destroy
将删除内部 XML 树。ToPrettyString
函数唯一的 string
参数是用于美观打印的缩进。输出如下所示
<Products>
<Product>
<Qty>1234</Qty>
</Product>
</Products>
读取元素
接下来,将读取前一个示例中保存的 xml
并显示 qty
。
void ReadElement(const std::string& xml)
{
using namespace Elmax;
RootElement root;
root.ParseXMLString(xml);
int qty = root["Product"]["Qty"].GetInt32(0);
std::cout << "Qty:" << qty << std::endl;
}
请注意,这里的 root
没有名称,因为它将在解析 xml
string
时设置。即使在构造函数中为 root
命名,在解析 xml
string
后也会被覆盖。qty
的值显示如下
Qty:1234
写入属性
让我们看看创建和写入属性的代码。
void WriteAttr(std::string& xml)
{
using namespace Elmax;
RootElement root("Products");
Element elem = root.Create("Product");
elem.SetAttrInt32("Qty", 1234);
xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
}
在写入属性之前,元素必须存在,因此它必须使用 Create
创建。下面是显示的 qty
值。
<Products>
<Product Qty="1234"/>
</Products>
读取属性
void ReadAttr(const std::string& xml)
{
using namespace Elmax;
RootElement root;
root.ParseXMLString(xml);
Element elem = root["Product"];
int qty = elem.GetAttrInt32("Qty", 0);
std::cout << "Qty:" << qty << std::endl;
}
在读取属性之前,必须小心确保元素存在,否则将抛出 runtime_error
异常。说到异常处理,可能会抛出 Boost bad_lexical_cast
和 std::exception
派生异常,如 runtime_error
,因此代码应放在 try-catch
中。输出如下所示
Qty:1234
写入注释
可以通过调用 AddComment
添加注释。XML 注释以 <!--
开头,以 -->
结尾。
void WriteComment(std::string& xml)
{
using namespace Elmax;
RootElement root("Products");
Element elem = root.Create("Product");
elem.SetAttrInt32("Qty", 1234);
elem.AddComment("Qty must not be less than 100");
xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
}
XML 中的注释如下所示
<Products>
<Product Qty="1234">
<!--Qty must not be less than 100-->
</Product>
</Products>
读取注释
下面的代码示例展示了如何检索元素下的注释集合。
void ReadComment(const std::string& xml)
{
using namespace Elmax;
RootElement root;
root.ParseXMLString(xml);
Element elem = root["Product"];
int qty = elem.GetAttrInt32("Qty", 0);
std::vector<Comment> vec = elem.GetCommentCollection();
std::cout << "Qty:" << qty << std::endl;
if(vec.size()>0)
std::cout << "Comment:" << vec[0].GetContent() << std::endl;
}
Qty:1234
Comment:Qty must not be less than 100
写入 CDATA 部分
CDATA
是(未解析的)字符数据,其中的文本会被 XML 解析器忽略。可以通过 AddCData
添加 CDATA
。XML 中的 CDATA
以 <![CDATA[
开头,以 ]]>
结尾。
void WriteCData(std::string& xml)
{
using namespace Elmax;
RootElement root("Products");
Element elem = root.Create("Product");
elem.SetAttrInt32("Qty", 1234);
elem.AddCData("Hello world!");
xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
}
输出如下所示
<Products>
<Product Qty="1234">
<![CDATA[Hello world!]]>
</Product>
</Products>
出于最佳实践的考虑,不建议在 CDATA
部分存储二进制数据,因为有可能在数据中遇到 ]]>
。由于用于读写文件的文本文件库的工作方式,回车符和换行符具有特殊含义。回车符将从二进制数据中移除。这是使用文本文件库的限制。要克服这些限制,最好将数据存储在 Base64 格式。
读取 CDATA 部分
下面提供了一个示例,说明如何通过首先使用 GetCDataCollection
检索集合来获取 CDATA
。
void ReadCData(const std::string& xml)
{
using namespace Elmax;
RootElement root;
root.ParseXMLString(xml);
Element elem = root["Product"];
int qty = elem.GetAttrInt32("Qty", 0);
std::vector<CData> vec = elem.GetCDataCollection();
std::cout << "Qty:" << qty << std::endl;
if(vec.size()>0)
std::cout << "CData:" << vec[0].GetContent() << std::endl;
}
上面的代码显示了这些。
Qty:1234
CData:Hello world!
命名空间
Namespace
支持是有限的。要在 namespace
下创建 Element
,请使用 namespace
URI 调用 Create
。为了性能原因,Element
解析不考虑 namespace
。通过 []
运算符检索 element
时,请使用 XML 中出现的精确名称。
void NamespaceUri()
{
using namespace Elmax;
RootElement root("Products");
Element elem = root.Create("Product").Create("Grocery:Item", "http://www.example.com");
elem.SetInt32(1234);
std::string xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
}
这是上述代码示例的输出
<Products>
<Product>
<Grocery:Item xmlns:Grocery="http://www.example.com">1234</Grocery:Item>
</Product>
</Products>
Collection
有两种方法可以检索一组元素作为集合:AsCollection
和 GetChildren
。AsCollection
检索同一级别、同名的元素集合;类似于获取兄弟节点,但包含自身。GetChildren
的含义不言自明。
void AsCollection()
{
using namespace Elmax;
RootElement root("Products");
Element elem1 = root.Create("Product");
elem1.SetAttrInt32("Qty", 400);
elem1.SetString("Shower Cap");
Element elem2 = root.Create("Product");
elem2.SetAttrInt32("Qty", 600);
elem2.SetString("Soap");
Element elem3 = root.Create("Product");
elem3.SetAttrInt32("Qty", 700);
elem3.SetString("Shampoo");
std::string xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
Element::collection_t vec = root["Product"].AsCollection();
for(size_t i=0;i<vec.size(); ++i)
{
cout << vec[i].GetString("") << ":" << vec[i].GetAttrInt32("Qty", 0) << std::endl;
}
}
输出显示如下
<Products>
<Product Qty="400">Shower Cap</Product>
<Product Qty="600">Soap</Product>
<Product Qty="700">Shampoo</Product>
</Products>
Shower Cap:400
Soap:600
Shampoo:700
我们可以为 AsCollection
或 GetChildren
指定谓词 Lambda 或函数对象,以获取通过谓词测试的元素。
void AsCollectionLambda()
{
using namespace Elmax;
RootElement root("Products");
Element elem1 = root.Create("Product");
elem1.SetAttrInt32("Qty", 400);
elem1.SetString("Shower Cap");
Element elem2 = root.Create("Product");
elem2.SetAttrInt32("Qty", 600);
elem2.SetString("Soap");
Element elem3 = root.Create("Product");
elem3.SetAttrInt32("Qty", 700);
elem3.SetString("Shampoo");
std::string xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
Element::collection_t vec = root["Product"].AsCollection([](Element elem){
return (elem.GetAttrInt32("Qty", 0)>500);
});
for(size_t i=0;i<vec.size(); ++i)
{
cout << vec[i].GetString("") << ":" << vec[i].GetAttrInt32("Qty", 0) << std::endl;
}
}
在输出中,只显示了数量大于 500 的产品。
<Products>
<Product Qty="400">Shower Cap</Product>
<Product Qty="600">Soap</Product>
<Product Qty="700">Shampoo</Product>
</Products>
Soap:600
Shampoo:700
AsCollection
和 GetChildren
的用法相似,因此我跳过了 GetChildren
的代码示例。
迭代器
我们可以使用 Element::Iterator
,而不是返回一个 vector
来遍历集合。
void Iterators()
{
using namespace Elmax;
RootElement root(_TS("Products"));
Element elem1 = root.Create("Product");
elem1.SetAttrInt32("Qty", 400);
elem1.SetString("Shower Cap");
Element elem2 = root.Create("Product");
elem2.SetAttrInt32("Qty", 600);
elem2.SetString("Soap");
Element elem3 = root.Create("Product");
elem3.SetAttrInt32("Qty", 700);
elem3.SetString("Shampoo");
std::string xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
Element::Iterator it = root.Begin("*");
for(;it!=root.End(); ++it)
{
cout << (*it).GetString("") <<
":" << (*it).GetAttrInt32("Qty", 0) << std::endl;
}
}
通过将 "*
" 通配符指定给 Begin
,我告诉元素返回所有子元素,无论它们的名称如何。输出如下所示
<Products>
<Product Qty="400">Shower Cap</Product>
<Product Qty="600">Soap</Product>
<Product Qty="700">Shampoo</Product>
</Products>
Shower Cap:400
Soap:600
Shampoo:700
C++ LINQ
通过 Marten Range 的 C++ LINQ,我们现在可以使用 LINQ 将从 XML 中提取的数据填充到我们的数据结构中。在下面的代码示例中,我们创建了一组 book
和 author
元素。
void CppLinq()
{
using namespace Elmax;
RootElement root("Bookstore");
Element Books = root.Create("Books");
Element Book1 = Books.Create("Book");
Book1.SetAttrInt32("AuthorID", 1255);
Book1["Title"].SetString("The Joy Luck Club");
Element Book2 = Books.Create("Book");
Book2.SetAttrInt32("AuthorID", 2562);
Book2["Title"].SetString("The First Phone Call from Heaven");
Element Book3 = Books.Create("Book");
Book3.SetAttrInt32("AuthorID", 3651);
Book3["Title"].SetString("David and Goliath");
Element Authors = root.Create("Authors");
Element Author1 = Authors.Create("Author");
Author1.SetAttrInt32("AuthorID", 1255);
Author1["Name"].SetString("Amy Tan");
Author1["Gender"].SetString("Female");
Element Author2 = Authors.Create("Author");
Author2.SetAttrInt32("AuthorID", 2562);
Author2["Name"].SetString("Mitch Albom");
Author2["Gender"].SetString("Male");
Element Author3 = Authors.Create("Author");
Author3.SetAttrInt32("AuthorID", 3651);
Author3["Name"].SetString("Malcolm Gladwell");
Author3["Gender"].SetString("Male");
std::string xml = root.ToPrettyString(" ");
std::cout << xml << std::endl;
Elmax 生成的 XML 如下所示
<Bookstore>
<Books>
<Book AuthorID="1255">
<Title>The Joy Luck Club</Title>
</Book>
<Book AuthorID="2562">
<Title>The First Phone Call from Heaven</Title>
</Book>
<Book AuthorID="3651">
<Title>David and Goliath</Title>
</Book>
</Books>
<Authors>
<Author AuthorID="1255">
<Name>Amy Tan</Name>
<Gender>Female</Gender>
</Author>
<Author AuthorID="2562">
<Name>Mitch Albom</Name>
<Gender>Male</Gender>
</Author>
<Author AuthorID="3651">
<Name>Malcolm Gladwell</Name>
<Gender>Male</Gender>
</Author>
</Authors>
</Bookstore>
如下所示,使用 C++ LINQ,book
和 author
元素在通用的 AuthorID
属性上连接。title
和 author
名称将在 BookInfo
结构体的 vector
中返回,而性别信息将被丢弃。
using namespace cpplinq;
struct BookInfo
{
std::string title;
std::string author;
};
auto result =
from (root["Books"].GetChildren("Book"))
>> join (
from (root["Authors"].GetChildren("Author")),
// Selects the AuthorID on book element to join on
[](const Element& b) {return b.GetAttrInt32("AuthorID", -1);},
// Selects the AuthorID on author element to join on
[](const Element& a) {return a.GetAttrInt32("AuthorID", -1);},
// Gets book title and author name
[](const Element& b, const Element& a) -> BookInfo
{ BookInfo info = {b["Title"].GetString(""),
a["Name"].GetString("")}; return info;}
)
>> to_vector();
for(size_t i=0;i<result.size(); ++i)
{
std::cout << result[i].title << " is written by " << result[i].author << std::endl;
}
}
这是显示的 BookInfo
列表。
The Joy Luck Club is written by Amy Tan
The First Phone Call from Heaven is written by Mitch Albom
David and Goliath is written by Malcolm Gladwell
预定义宏
config.h 中有一些宏可以启用 Portable Elmax 的某些行为。本节将阐明哪些宏可以启用。例如,如果您想对字符串使用宽字符,应取消注释下面的宏。
//#define ELMAX_USE_UNICODE
如果您希望使用 MFC CString
,则必须定义 ELMAX_USE_MFC_CSTRING
。具体是 CStringA
还是 CStringW
取决于宏 ELMAX_USE_UNICODE
的存在。如果禁用此宏,则使用 STL string
。
//#define ELMAX_USE_MFC_CSTRING
以下是互斥的宏,用于确定使用哪种容器类来存储属性。可供选择的有 map
、unordered_map
、list
或 vector
。
//#define ELMAX_USE_MAP_FOR_ATTRS
//#define ELMAX_USE_UNORDERED_MAP_FOR_ATTRS
//#define ELMAX_USE_LIST_FOR_ATTRS
#define ELMAX_USE_VECTOR_FOR_ATTRS
结论
在本文中,我们简要介绍了如何编写和读取元素、属性等。有 105 个单元测试。当您取消注释任何预定义宏时,请记住构建并运行单元测试。该项目托管在 Github 上,因此用户应始终从那里下载最新的源代码。 Portable Elmax 不会托管在 Nuget 上,因为存在许多可能的配置,例如,使用 STL string
或 MFC CString
,使用 ASCII 或 Unicode 等。如果发现任何错误,请将您的 config.h 副本发送给我,以帮助我缩小问题范围。如果读者有任何功能请求,请在文章论坛中告知我。感谢您的阅读!
历史
- 2024-04-27:0.9.9 版本通过 PJ Arends 的解决方案修复了
RawElement::ReadAttributeValue
,使其能够接受单引号包围的属性,并修复了单元测试编译器和链接器错误。 - 2022-06-28:0.9.8 版本移除了 Boost
lexical_cast
- 2020-08-11:0.9.7 版本修复了 PJ Arends 报告的
RawElement::PrettyTraverse
问题,通过检查开始标记是否已写入再写入结束>
。 - 2020-05-03:0.9.6 版本包含 PJ Arends 提供的缺失函数实现和错误修复。
- 2015-06-14:0.9.5 Beta 版本。迁移到 Github
- 2013-11-26:初始发布
0.9.5 版本中的重大更改
- 无隐式类型转换:移除了隐式类型访问器和修改器。现在必须显式调用访问器和修改器。
- 使用 RootElement:为根元素使用
RootElement
以获得 RAII 销毁。RootElement
派生自Element
。 - Element 已简化:
Element
删除了所有其他数据成员,成为一个轻量级包装器,只有一个数据成员,即RawElement
指针。 - Attribute 类已移除:用户不能调用
Attr
方法来获取Attribute
。请改用Element
类上的Attribute
数据访问器和修改器。 - [] 不支持查询:用户不能通过查询来检索元素,例如
elem["Products|Books"]
,应使用elem["Products"]["Books"]
。 - [] 运算符是 const:由于
[]
运算符不修改数据成员,现在它遵循const
正确性(在cpplinq
中)。 - Create 和 CreateNew 的行为已更改:以前
Create
和CreateNew
会在节点不存在时创建自身。现在行为已更改:Create
用于创建新的子元素,并且必须提供名称。CreateNew
已不再使用。