基于模板的 C++ XML 解析方法
XML 是一种基于标记的数据描述语言,旨在允许开发人员使用描述性自定义标签创建结构化文档。XML 的目的是将数据描述与其预期用途分离,并允许数据在不同应用程序之间以非平台或架构特定的方式传输。XML 的另一个有用应用是以逻辑且有意义的方式描述一个流程,该流程可以在运行时由应用程序执行。
为了成功解析 XML 文件,开发人员必须首先创建一个可以被解析器处理的文件。解析器是一组共享对象或一个库,用于读取和处理 XML 文件。
解析器可能有两种类型:验证型或非验证型。验证型解析器扫描 XML 文件,并根据 XML 模式或文档类型定义 (DTD) 确定文档是否格式良好。非验证型解析器仅读取文件,并忽略 XML 模式或 DTD 指定的格式和布局。
最广泛使用的解析器代表两种不同的方法:事件驱动型和树型。事件驱动型解析器称为 SAX(XML 简单 API)。树型解析器在读取和解析 XML 文件时,在内存中创建一个 DOM(文档对象模型)树。
DOM 实现难以导航,并且不允许 XML 元素和特定领域对象之间进行清晰的映射。SAX 提供事件,允许开发人员在读取和解析 XML 文件时创建其特定领域对象。本文介绍了一个使用 SAX API 进行 XML 解析的框架设计。
C++ 最常用的两个解析器是 Apache Project 的开源 Xerces 和 IBM alphaWorks Project 创建的 XML4C。XML4C 基于 Xerces。
这两个解析器本质上提供了相同的源代码和库布局,因此可以互换使用。它们还支持基于 DOM 和 SAX 的 XML 解析。
本文档描述了使用 SAX 解析器和 Xerces 解析器的实现。与 XML 解析相关的 Xerces 源代码或二进制文件可以从 Xerces 网站下载(请参阅“资源”)。
为了开始使用 SAX API 解析 XML 文件,必须了解 SAX C++ 对象交互的布局。SAX 设计了两个基本接口
SAXParser
setDoValidationsetDoNamespacesetDoSchemasetValidationFullSchemaCheckingsetDocumentHandlersetErrorHandlerparse
和
HandlerBase
warningerrorfatalErrorstartElementcharactersignorableWhitespaceendElement
仔细检查 HandlerBase 对象中的方法会发现两种不同的方法类别:错误处理和文档处理。错误处理方法包括 warning、error 和 fatalError,而解析方法包括 startElement、characters、ignorableWhitespace 和 endElement。这些行为可以分离到单独的对象中,稍后将对此进行说明。
SAXParser 类负责设置基本属性和运行时要强制执行的所需行为。
以下示例代码说明了使用 C++ 中的 SAX 解析器解析 XML 文件的基本步骤
// Create a new instance of the SAX parser SAXParser parser; // Initialize the behavior you desire parser.setDoValidation(true); parser.setDoNamespaces(true); parser.setDoSchema(true); parser.setValidationSchemaFullChecking(true); // Add handlers for document and error processing parser.setDocumentHandler(&docHandler); parser.setErrorHandler(&errorHandler); // Parse file parser.parse("MyXMLFile.xml");
在解析发生时,您实例化的类 docHandler 和 errorHandler 会转发从解析触发的事件。这些类派生自 Xerces 基类 HandlerBase,并重写了适当的方法,以便根据其分类功能处理事件。
现在我们已经了解了如何使用 SAX 解析 XML,接下来让我们探讨一下我们的 XML 框架是如何实现的,以便利用 API 中提供的工具。
策略类,正如 Andrei Alexandrescu 的 Modern C++ Design(请参阅“资源”)中所描述和推广的那样,“定义一个类接口或一个类模板接口。该接口由以下一项或全部组成:内部类型定义、成员函数和成员变量。”
当使用基于模板的 C++ 设计创建策略类时,策略类的有用性在这个 XML 框架中得以实现。策略允许您以精细的粒度参数化和配置功能。在本设计中,创建策略是为了适应以下行为:文档处理、错误处理、域映射和解析。
将这些元素配置为策略允许创建更简洁的代码,任何有 C++ 经验和模板使用经验的开发人员都更容易维护。
XML 解析框架的主要类是 XMLSAXParser。它是一个自定义设计的类模板,实现了 XMLParserInterface,并包含一个 SAXParser 对象作为成员变量。模板参数包括文档处理程序和错误处理程序的策略类。所有解析最终都委托给 SAXParser 成员变量,在设置了各种处理程序和其他属性之后。
使用该框架实现自定义处理程序是一项相对简单的任务。这种类型的设计的优点在于,通过更改一个或多个策略,可以将相同的框架用于不同的解析 API 和不同的域映射对象——本文中未实现此练习。
为了创建自定义处理程序,从 HandlerBase 派生新创建的自定义类,并重写感兴趣的虚方法。XMLFactory 框架中创建了以下两种类型的自定义处理程序
XMLSAXHandler
startElementcharacterignorableWhitespaceendElement
和
XMLSAXErrorHandler
warningerrorfatalError
XMLSAXHandler 处理文档事件处理,XMLSAXErrorHandler 处理各种错误回调。
我们的 XML 解析框架的下一个方面是使用模板和策略类的宽松定义,将 XML 标签转换为可以在应用程序中使用的域相关对象。
XMLDomainMap 模板接受单个模板参数,称为 XMLNode。域映射对象的接口如下
XMLDomainMap
createaddupdateAttribute
XMLNode 在树结构中充当叶子和根,将其子节点聚合为子树。XMLNode 的接口是
XMLNode
operator==operator!=operator=addChildhasChildrennumChildrenvaluenamegetChildCountgetChildgetParent
这里的关键是对象公共接口的设计。有几个运算符重载,特别是等于运算符 (operator==)、不等于运算符 (operator!=) 和赋值运算符 (operator=)。这样做的好处是,该对象现在可以与许多标准模板库 (STL) 容器和算法一起使用,从而允许将高级功能与 C++ 语言一起使用。
到目前为止,重点一直放在单个类上,并描述为我们的 XML 处理框架创建的模板。下一步是将不同的接口链接在一起,并通过使用外观设计模式使它们看起来像一个有凝聚力的单元一样运行。
外观设计提供了一种简单而优雅的方式,将解析功能从外部客户端委托给将用于执行解析的内部策略类。
在 设计模式 中,作者将意图定义为“为子系统中的一组接口提供统一的接口。外观模式定义了一个更高级别的接口,使子系统更易于使用。”
XMLProcessor 是已创建的外观模式。它使用以下接口定义
XMLProcessor
parsegetParseEngine
一旦编写完所有源代码,就需要一个 XML 文件和一个测试客户端来运行我们的示例。
以下简单的 XML 文件显示了包含名称和帐号的客户记录的基本布局,已创建该文件以说明使用该框架的简易性
<?xml version="1.0" encoding="iso-8859-1"?> <customer> <name>John Doe</name> <account-number>555123</account-number> </customer>
现在,使用文本编辑器创建此文件并将其另存为 MyXMLFile.xml。
该框架的功能将用作向客户端应用程序提供简洁接口的机制。
框架的客户端将使用的主要方法可以用一个实际的,尽管很小的 C++ 源代码示例来描述
// --------------------------------------- // Sample source for parsing an XML doc // --------------------------------------- #include "XMLProcessor.hpp" #include "XMLDomainMap.hpp" #include "XMLSAXParser.hpp" #include "XMLNode.hpp" #include "XMLCommand.h" #include "XMLSAXHandler.hpp" #include "XMLSAXErrorHandler.hpp" #include <iostream> using namespace std; using namespace XML; // Let's get the ugly stuff out of the way first typedef XMLSAXHandler<XMLDomainMap<XMLNode> > DOCHANDLER; typedef XMLSAXErrorHandler ERRORHANDLER; typedef XMLSAXParser<DOCHANDLER, ERRORHANDLER> PARSER; typedef XMLProcessor<PARSER> XMLEngine; // Create a basic test client int main(void) { // Define a string object with our filename std::string xmlFile = "MyXMLFile.xml"; // Create an instance of our XMLFactory XMLEngine parser(xmlFile); // Turn off validation parser.doValidation(false); // Parse our XML file parser.parse(); // Get the tree root XMLNode root = parser.getXMLRoot(); // Print the name of our object cout << "Root = " << root.name() << endl; return 0; }
现在,代表树根的 XMLNode 对象的实例已被解析,可以访问根 XMLNode 的子元素。
最后一步是编译客户端。只需在命令行执行编译即可
g++ -o testClient -I. -I/path/to/xerces/include \ -I/path/to/xerces/include/xerces testClient.cpp \ -L/path/to/xerces/lib -lxerces-c
这将编译客户端应用程序。下一步是运行测试。请务必设置您的 LD_LIBRARY_PATH 环境变量,使其指向您的 Xerces 安装的 lib 目录的位置。由于共享库来自此目录,因此应用程序加载器需要一种方法在运行时导入所需的符号,以便一切正常运行。
当运行 testClient 时,预期会看到以下输出
$>testClient Adding child name Adding child account-number Root = customer
您现在拥有一个功能齐全的 XML 解析框架,该框架使用 C++ 模板,可让您将 XML 合并到新的或现有的应用程序中。示例代码可在 ftp.linuxjournal.com/pub/lj/listings/issue110/6655l1.tgz 获取。

John Dubchak 是一位在圣路易斯地区担任顾问的高级软件开发人员。他在过去 12 年中一直使用 C++ 编程,并且不敢相信他的第一行 C++ 代码实际上有多糟糕。他的妻子说他的爱好是“坐在电脑前编写小程序”。