Expat XML 解析器的 C++ 包装器
包含的类定义为 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 和这两个类之间的关系。
|
|
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::OnStartElement
、CMyXML::OnEndElement
和 CMyXML::OnCharacterData
方法。这些例程除非被启用,否则不会被调用。CMyXML::OnPostCreate
中的代码会启用这三个事件例程。
创建解析器
现在我们有了派生类,就可以使用它来创建 Expat 解析器。创建解析器非常简单。首先创建一个解析器类的实例,然后调用 Create
方法。
Create
方法有两个参数:文档编码和用于分隔命名空间名称的字符。编码是解析 XML 文档时使用的默认编码,除非 XML 文档本身指定了编码。命名空间分隔符用于在调用(如 OnStartElement
)中分隔命名空间和名称。
例如,如果 XML 文档中的名称是 SOAP_ENC:Envelope
,SOAP_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 解析器示例:CMyXMLBase
和 CMyXML
。CMyXML
派生自 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>
类的设计,只有模板参数列表中指定的类的成员方法才会被调用。这是设计使然。
有三种不同的方法可以解决这个问题。第一种方法是将 OnStartElement
在 CMyXMLBase
中声明为虚函数。第二种方法是将 CMyXMLBase
从 CExpatImpl <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;
}
};