65.9K
CodeProject 正在变化。 阅读更多。
Home

Expat XML 解析器的 C++ 包装器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (29投票s)

2002年2月18日

CPOL

7分钟阅读

viewsIcon

793537

downloadIcon

4600

包含的类定义为 Expat C API 提供了完整且易于使用的 C++ 包装器

引言

Expat XML 解析器是一个优秀且广泛使用的基于事件的 XML 解析器。Expat 最令人称道的功能之一是它提供了一个可供 C 程序使用的 API。尽管许多程序员在 C++ 环境中使用 Expat,但基于 C 的 API 使得从 DLL 导出此 API 变得容易。

然而,Expat 基于 C 的 API 并不意味着我们必须放弃 C++ 类。幸运的是,Expat 在设计时就考虑了可以被类扩展的可能性。

(定义: 基于事件的 XML 解析器 - 一个 XML 解析器,在解析 XML 结构时会调用方法(也称为事件)。这与 DOM(文档对象模型)风格的解析器不同,后者会解析 XML,然后以其逻辑层次结构格式将 XML 数据呈现给应用程序。)

设计理念

设计 Expat 包装器类时,主要考虑了完整性、简洁性和可扩展性。为了完整性,几乎所有的 Expat API 例程都被包装到了类中。这甚至包括像 XML_ExpatVersionInfo 这样的 API。为了简洁性,包装器类仅包装 Expat API,不提供其他任何功能。为了可扩展性,包装器类使得派生出提供增强功能的新类变得容易。

基础

这个 Expat 包装器包含 2 个类,一个基于模板的类(CExpatImpl <class _T>)和一个基于虚函数的类(CExpat)。每个类都具有适合特定解决方案的特性。

下表说明了 API 和这两个类之间的关系。

CExpat

CExpatImpl <class _T>

Expat C API

模板类 CExpatImpl <class _T> 提供了 C++ 和 Expat C API 之间的基础翻译层。模板设计的优势在于,如果应用程序只需要 Expat 的一些事件例程,那么这些事件例程的代码就不会被编译到最终的可执行文件中。诚然,浪费的空间很小,但为什么还要浪费呢。

CExpat 类是从模板类 CExpatImpl <class _T> 派生而来的。但是,除了默认构造函数之外,这个类中包含的唯一方法都是声明为虚函数的事件方法。CExpat 适用于更倾向于使用虚函数而非模板的情况。

在合理范围内,这两个类是可互换的。如果您有一个派生自 CExpat 的类,可以轻松地修改它以使用 CExpatImpl <class _T>,反之亦然,而无需修改任何其他源代码。有关更复杂的派生类的一些实现陷阱,请参见“实现说明”。

在本文档的其余部分,将只讨论 CExpatImpl <class _T> 类。如前所述,这两个包装器类几乎 100% 可互换。记录两者都会显得冗余。

入门

使用 CExpatImpl <class _T> 的第一步是派生一个新类,该类将提供应用程序特定的实现。派生类是必需的。与 Expat 一样,如果没有派生类,Expat 将只验证 XML 是否格式正确。

作为起点,让我们定义一个 XML 解析器,它将在元素开始、结束以及元素包含的数据时显示。

class CMyXML : public CExpatImpl <CMyXML> 
{
public:

	// Constructor 
	
	CMyXML () 
	{
	}
	
	// Invoked by CExpatImpl after the parser is created
	
	void OnPostCreate ()
	{
		// Enable all the event routines we want
		EnableStartElementHandler ();
		EnableEndElementHandler ();
		// Note: EnableElementHandler will do both start and end
		EnableCharacterDataHandler ();
	}
	
	// Start element handler

	void OnStartElement (const XML_Char *pszName, const XML_Char **papszAttrs)
	{
		printf ("We got a start element %s\n", pszName);
		return;
	}

	// End element handler

	void OnEndElement (const XML_Char *pszName)
	{
		printf ("We got an end element %s\n", pszName);
		return;
	}

	// Character data handler

	void OnCharacterData (const XML_Char *pszData, int nLength)
	{
		// note, pszData is NOT null terminated
		printf ("We got %d bytes of data\n", nLength);
		return;
	}
};

CExpatImpl <class _T> 将在 Expat 解析器创建后调用 CMyXML::OnPostCreate 方法。这提供了一种启用事件例程的便捷方法。当 XML 文本被解析时,Expat 将调用 CMyXML::OnStartElementCMyXML::OnEndElementCMyXML::OnCharacterData 方法。这些例程除非被启用,否则不会被调用。CMyXML::OnPostCreate 中的代码会启用这三个事件例程。

创建解析器

现在我们有了派生类,就可以使用它来创建 Expat 解析器。创建解析器非常简单。首先创建一个解析器类的实例,然后调用 Create 方法。

Create 方法有两个参数:文档编码和用于分隔命名空间名称的字符。编码是解析 XML 文档时使用的默认编码,除非 XML 文档本身指定了编码。命名空间分隔符用于在调用(如 OnStartElement)中分隔命名空间和名称。

例如,如果 XML 文档中的名称是 SOAP_ENC:EnvelopeSOAP_ENC 被定义为“http://schemas.xmlsoap.org/soap/envelope/",并且 "#" 被指定给 Create,那么 OnStartElement 将会以字符串 "http://schemas.xmlsoap.org/soap/envelope/#Envelope" 调用。

bool ParseSomeXML (LPCTSTR pszXMLText)
{
	CMyXML sParser;
	sParser .Create ();
	
	// do something useful
}

解析简单的文本字符串

接下来,我们需要实际将 XML 文档发送给解析器。有两种不同的方法可以将文档发送给 XML 解析器:直接发送或通过内部缓冲区。这两种方法中,直接发送数据给解析器更简单。但是,它的速度也稍慢一些。

要发送简单的字符串给解析器,应用程序调用 Parse (LPCTSTR pszBuffer, int nLength = -1, bool fIsFinal = true) 方法。第一个参数是指向要解析的数据字符串的指针。已经为 ANSI 和 UNICODE 字符串定义了例程。第二个参数是字符串的字符长度(char 或 wchar_t,取决于 ANSI 或 UNICODE)。如果 nLength 小于零,则要求 pszBuffer 指向的字符串是 NUL 终止的,并且长度将从字符串中确定。如果 nLength 大于或等于零,则字符串不需要 NUL 终止,并且长度不应包含 NUL 字符(如果存在)。第三个参数告知 XML 解析器何时没有更多数据。如果整个 XML 文档可以包含在一个简单的字符串中,那么第一次可以将 fIsFinal 设置为 true。否则,在还有更多数据要解析时,fIsFinal 应保持为 false。在读取完所有数据后,可以使用 nLength 设置为零并将 fIsFinal 设置为 true 来调用 Parse

bool ParseSomeXML (LPCTSTR pszXMLText)
{
	CMyXML sParser;
	sParser .Create ();
	
	// Send this simple string to the parser
	
	return sParser .Parse (pszXMLText);
}

使用内部缓冲区解析

为了减少额外的内存复制次数,可以使用 Expat 解析器内部的缓冲区,而不是将数据传递给解析器,然后再让 Expat 解析器将数据复制到内部缓冲区。使用内部缓冲区需要 3 个步骤:请求缓冲区,将数据读入缓冲区,将数据提交给解析器。

bool ParseSomeXML (LPCSTR pszFileName)
{

	// Create the parser 
	
	CMyXML sParser;
	if (!sParser .Create ())
		return false;
	
	// Open the file
	
	FILE *fp = fopen (pszFileName, "r");
	if (fp == NULL)
		return false;
	
	// Loop while there is data
	
	bool fSuccess = true;
	while (!feof (fp) && fSuccess)
	{
		LPSTR pszBuffer = (LPSTR) sParser .GetBuffer (256); // REQUEST
		if (pszBuffer == NULL)
			fSuccess = false;
		else
		{
			int nLength = fread (pszBuffer, 1, 256, fp); // READ
			fSuccess = sParser .ParseBuffer (nLength, nLength == 0); // PARSE
		}	
	}

	// Close the file
	
	fclose (fp);
	return fSuccess;
}

正如您所见,这种方法比其他方法更复杂,但当您修改上一节中的示例以读取文件时,复杂性差异很小。

处理事件例程

事件例程向应用程序提供有关已解析内容的实际信息。CExpatImpl <class _T> 类中的方法名称选择得非常容易理解,可以知道哪个例程适用于哪个 Expat 事件。

在 Expat 中

设置事件处理程序例程XML_Set[Event Name]Handler
事件处理程序的名称应用程序特定

在 CExpatImpl <class _T> 中

启用事件处理程序例程Enable[Event Name]Handler
事件处理程序的名称On[Event Name]
内部事件处理程序的名称[Event Name]Handler

因此,如果您希望接收 StartElement 事件,则需要定义一个名为 OnStartElement 并带有正确参数的方法,然后使用 true 作为唯一参数调用 EnableStartElementHandler。之后可以通过再次调用 EnableStartElementHandler 并将 false 作为唯一参数来禁用该事件例程。

每个事件例程的具体细节超出了本文档的范围。有关事件和 Expat 解析器本身的更多信息,请参阅 http://www.xml.com/pub/a/1999/09/expat/index.html。本文档中包含的大部分信息在 CExpatImpl <class _T> 中都有同名的对应内容。

实现说明

如前所述,在创建复杂的派生类层次结构时,应用程序需要注意一些陷阱。让我们考虑一个由两个类组成的 XML 解析器示例:CMyXMLBaseCMyXMLCMyXML 派生自 CMyXMLBase,而 CMyXMLBase 派生自 Expat 类包装器之一。

考虑从 CExpatImpl <class _T> 模板类派生类的情况。

class CMyXMLBase : public CExpatImpl <CMyXMLBase> 
{
public:

	CMyXMLBase () 
	{
	}
	
	void OnStartElement (const XML_Char *pszName, const XML_Char **papszAttrs) 
	{
		// do useful stuff here... 
		return;
	}
};

class CMyXML : public CMyXMLBase
{ 
public:

	CMyXML ()
	{
	}
	
	void OnStartElement (const XML_Char *pszName, const XML_Char **papszAttrs) 
	{
		// do derived useful stuff here...
		return;
	}
};

在这种情况下,程序员期望 Expat 解析器调用 OnStartElement。然而,由于 CExpatImpl <class _T> 类的设计,只有模板参数列表中指定的类的成员方法才会被调用。这是设计使然。

有三种不同的方法可以解决这个问题。第一种方法是将 OnStartElementCMyXMLBase 中声明为虚函数。第二种方法是将 CMyXMLBaseCExpatImpl <class _T> 派生改为从 CExpat 派生。第三种方法需要将 CMyXMLBase 从普通类更改为模板。此更改为 CExpatImpl <class _T> 提供了用于查找事件例程的类名。

template <class _T>
class CMyXMLBase : public CExpatImpl <_T> 
{
public:

	CMyXMLBase () 
	{
	}
	
	void OnStartElement (const XML_Char *pszName, const XML_Char **papszAttrs) 
	{
		// do useful stuff here... 
		return;
	}
};

class CMyXML : public CMyXMLBase <CMyXML>
{ 
public:

	CMyXML ()
	{
	}
	
	void OnStartElement (const XML_Char *pszName, const XML_Char **papszAttrs) 
	{
		// do derived useful stuff here...
		return;
	}
};
© . All rights reserved.