14.3 解析一个复杂的XML文档
问题
有一序列的数据存储在一个使用内部DTD或者XML命名空间的XML文档中。如何解析这个文档,并且把这些数据存储到一个C++的集合对象中?
解决方案
使用SAX2 API(XML简单API,2.0版本)的Xerces实现。首先,从xercesc::ContentHandler派生出一个类;这个类将收到XML文档中结构和内容被解析的通知。接下来,如果你愿意,再从xercesc::ErrorHandler派生出一个类来接收警告和错误通知。构造一个xercesc::SAX2XMLReader类型的解析器,使用这个解析器的setContentHandler()和setErrorHandler()方法注册一个handler类的实例。最后调用解析器的parse()方法,给这个方法传入你的文档文件的路径名作为参数。
例如,假设你想解析示例14-1中的animals.xml的XML文档,并且构造一个Animal类型的std::vector来代表这个文档中的动物(有关Animal类的定义,请参考示例14-2)。示例14-3中,我说明了如何使用TinyXml。为了使得这个问题更具有挑战性,让我们给这个文档加上命名空间,如示例14-5所示。
示例14-5 使用XML命名空间,列举马戏团的动物
<?xml version="1.0" encoding="UTF-8"?>
<!-- Feldman Family Circus Animals with Namespaces -->
<ffc:animalList xmlns:ffc="http://www.feldman-family-circus.com">
<ffc:animal>
<ffc:name>Herby</ffc:name>
<ffc:species>elephant</ffc:species>
<ffc:dateOfBirth>1992-04-23</ffc:dateOfBirth>
<ffc:veterinarian name="Dr. Hal Brown" phone="(801)595-9627"/>
<ffc:trainer name="Bob Fisk" phone="(801)881-2260"/>
</ffc:animal>
<!-- etc. -->
</ffc:animalList>
为了使用SAX2来解析这个文档,定义一个ContentHandler,如示例14-6所示,并且定义一个ErrorHandler,如示例14-7所示。然后构造一个SAX2XMLReader,并注册你自己的处理器,然后运行这个解析器。如示例14-8所示。
示例14-6 解析animals.xml文档的SAX2 ContentHandler
#include <stdexcept> // runtime_error
#include <vector>
#include <xercesc/sax2/Attributes.hpp>
#include <xercesc/sax2/DefaultHandler.hpp> // Contains no-op
// implementations of
// the various handlers
#include "xerces_strings.hpp" // Example 14-4
#include "animal.hpp"
using namespace std;
using namespace xercesc;
// Returns an instance of Contact based
// on the given collection of attributes
Contact contactFromAttributes(const Attributes &attrs)
{
// For efficiency, store frequently used string
// in static variables
static XercesString name = fromNative("name");
static XercesString phone = fromNative("phone");
Contact result; // Contact to be returned.
const XMLCh* val; // Value of name or phone attribute.
// Set Contact's name.
if ((val = attrs.getValue(name.c_str())) != 0) {
result.setName(toNative(val));
} else {
throw runtime_error("contact missing name attribute");
}
// Set Contact's phone number.
if ((val = attrs.getValue(phone.c_str())) != 0) {
result.setPhone(toNative(val));
} else {
throw runtime_error("contact missing phone attribute");
}
return result;
}
// Implements callbacks that receive character data and
// notifications about the beginnings and ends of elements
class CircusContentHandler : public DefaultHandler {
public:
CircusContentHandler(vector<Animal>& animalList)
: animalList_(animalList)
{ }
// If the current element represents a veterinarian or trainer,
// use attrs to construct a Contact object for the current
// Animal; otherwise, clear currentText_ in preparation for the
// characters() callback
void startElement(
const XMLCh *const uri, // namespace URI
const XMLCh *const localname, // tagname w/ out NS prefix
const XMLCh *const qname, // tagname + NS pefix
const Attributes &attrs ) // elements's attributes
{
static XercesString animalList = fromNative("animalList");
static XercesString animal = fromNative("animal");
static XercesString vet = fromNative("veterinarian");
static XercesString trainer = fromNative("trainer");
static XercesString xmlns =
fromNative("http://www.feldman-family-circus.com");
// Check namespace URI
if (uri != xmlns)
throw runtime_error(
string("wrong namespace uri: ") + toNative(uri)
);
if (localname == animal) {
// Add an Animal to the list; this is the new
// "current Animal"
animalList_.push_back(Animal());
} else if (localname!= animalList) {
Animal& animal = animalList_.back();
if (localname == vet) {
// We've encountered a "veterinarian" element.
animal.setVeterinarian(contactFromAttributes(attrs));
} else if (localname == trainer) {
// We 've encountered a "trainer" element.
animal.setTrainer(contactFromAttributes(attrs));
} else {
// We've encountered a "name" , "species", or
// "dateOfBirth" element. Its content will be supplied
// by the callback function characters().
currentText_.clear();
}
}
}
// If the current element represents a name, species, or date
// of birth, use the text stored in currentText_ to set the
// appropriate property of the current Animal.
void endElement(
const XMLCh *const uri, // namespace URI
const XMLCh *const localname, // tagname w/ out NS prefix
const XMLCh *const qname ) // tagname + NS pefix
{
static XercesString animalList = fromNative("animal-list");
static XercesString animal = fromNative("animal");
static XercesString name = fromNative("name");
static XercesString species = fromNative("species");
static XercesString dob = fromNative("dateOfBirth");
if (localname!= animal && localname!= animalList) {
// currentText_ contains the content of the element
// which has ended. Use it to set the current Animal's
// properties.
Animal& animal = animalList_.back();
if (localname == name) {
animal.setName(toNative(currentText_));
} else if (localname == species) {
animal.setSpecies(toNative(currentText_));
} else if (localname == dob) {
animal.setDateOfBirth(toNative(currentText_));
}
}
}
// Receives notifications when character data is encountered
void characters( const XMLCh* const chars,
const unsigned int length )
{
// Append characters to currentText_ for processing by
// the method endElement()
currentText_.append(chars, length);
}
private:
vector<Animal>& animalList_;
XercesStringcurrentText_;
};
示例14-7 SAX2 ErrorHandler错误处理器
#include <stdexcept> // runtime_error
#include <xercesc/sax2/DefaultHandler.hpp>
// Receives Error notifications.
class CircusErrorHandler : public DefaultHandler {
public:
void warning(const SAXParseException& e)
{
/* do nothing */
}
void error(const SAXParseException& e)
{
throw runtime_error(toNative(e.getMessage()));
}
void fatalError(const SAXParseException& e) { error(e); }
};
示例14-8 使用SAX2 API来解析文档animals.xml
#include <exception>
#include <iostream>// cout
#include <memory> // auto_ptr
#include <vector>
#include <xercesc/sax2/SAX2XMLReader.hpp>
#include <xercesc/sax2/XMLReaderFactory.hpp>
#include <xercesc/util/PlatformUtils.hpp>
#include "animal.hpp"
#include "xerces_strings.hpp" // Example 14-4
using namespace std;
using namespace xercesc;
// RAII utility that initializes the parser and frees resources
// when it goes out of scope
class XercesInitializer {
public:
XercesInitializer() { XMLPlatformUtils::Initialize(); }
~XercesInitializer() { XMLPlatformUtils::Terminate(); }
private:
// Prohibit copying and assignment
XercesInitializer(const XercesInitializer&);
XercesInitializer& operator=(const XercesInitializer&);
};
int main()
{
try {
vector<Animal> animalList;
// Initialze Xerces and obtain parser
XercesInitializer init;
auto_ptr<SAX2XMLReader> ]
parser(XMLReaderFactory::createXMLReader());
// Register handlers
CircusContentHandlercontent(animalList);
CircusErrorHandler error;
parser->setContentHandler(&content);
parser->setErrorHandler(&error);
// Parse the XML document
parser->parse("animals.xml");
// Print animals' names
for ( vector<Animal>::size_type i = 0,
n = animalList.size();
i < n;
++i )
{
cout << animalList[i] << "\n";
}
} catch (const SAXException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XMLException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
讨论
一些XML解析器解析XML文档并且返回给用户一个复杂的C++对象。接下来的几节中的TinyXml和W3C DOM解析器都是以这种方式工作。相反,SAX2解析器使用一序列的回调函数来传达XML文档的有关信息给用户。这些回调函数被组织成一个处理接口:ContentHandler接收关于XML文档的元素、属性和文本信息的通知,ErrorHandler接收警告和错误通知,而DTDHandler则接收有关这个XML文档的DTD的通知。
围绕着这一序列的回调函数来设计解析器有很多重要的优点。例如,使得解析那些太大而不能完全装入内存的文档成为可能。此外,它避免无数的为构造XML文档的内部节点的动态分配,并且能让用户直接构造这个文档中他自己的节点,而不是像我在示例14-3中那样在这整个节点树中贯穿,从而能够节省处理时间。
示例14-8是非常直接的:我获取一个SAX2解析器,注册一个ContentHandler和ErrorHandler,然后解析这个animals.xml文档,然后打印由ContentHandler获得的Animal类的列表。这里有两点比较有趣:首先,函数XMLReaderFactory::createXML-Reader()返回一个动态分配的SAX2XMLReader实例,这个实例必须由用户显式地释放;为这个目的我使用std::auto_ptr来保证即使在有异常抛出的情况下这个实例也能被删除。第二,这个Xerces的框架必须使用xercesc::XMLPlatformUtils::Initialize()方法来初始化并且必须使用xercesc::XMLPlatformUtils::Terminate()方法来清除。我在XercesInitializer类中封装了这个初始化和清除动作,这个类在构造函数中调用XMLPlatformUtils::Initialize()方法,在它的析构函数中调用XMLPlatformUtils::
Terminate()方法。这就能保证即使有异常抛出的情况下也能调用Terminate()方法。这是示例8-3中说明的资源获取初始化(RAII)技术的一个例子。
让我们看看示例14-6中的CircusContentHandler类是如何实现SAX2 ContentHandler接口的。这个SAX2的解析器每当碰到一个元素的开始标签时就调用startElement()方法。如果这个元素有一个相关的命名空间,这第一个参数uri就包含这个元素的命名空间URI,并且第二个参数localname包含紧跟在命名空间前置的元素标签名的一部分。如果这个元素没有相关的命名空间,这两个参数就都是空的字符串。第三个参数包含这个元素的标签名; 如果这个元素确实有相关的命名空间的话,这个参数可能包含这个元素的标签名,就好像它出现在正在被处理的文档中一样,但它也可能是一个空的字符串。第四个参数是类Attribute的一个实例,这个类代表这个元素的属性集。
在示例14-6中startElement()方法的实现中,我忽略了animalList元素。当我碰到一个动物元素时,我就在这个动物列表中增加一个新的Animal对象,让我们把这个Animal对象当成是当前的Animal对象,并且把给这个Animal对象设置属性的工作交给处理器handler。当我碰到一个veterinarian或trainer元素时,我就调用函数contactFromAttributes来从这个元素的属性集中构造一个Contact类的实例,然后使用这个Contact类的实例来设置这个Animal对象的veterinarian或trainer属性。当我碰到一个name、species或dateOfBirth元素时,我就清除这个成员变量currentText_,这个成员变量用来存储这个元素的文本内容。
这个SAX2解析器调用characters()方法来分发这个元素包含的字符数据。 这个解析器按照串行的方式调用characters()来分发元素的字符;直到碰到这个元素的结束标签为止,但是不能保证这个元素的所有字符数据都能被分发。因此,在这个characters()方法的实现中,我简单地把提供的字符追加到这个成员变量currentText_中,只要碰到name、species或者dateofbirth的结束标签时,这个成员变量就用来设置当前Animal对象的name、species或dateofbirth。
每当SAX2解析器离开一个元素时就调用endElement()方法。它的参数与startElement()方法的前三个参数具有相同的阐述。在示例14-6中endElement()方法的实现中,我忽略除了name、species和dateOfBirth的所有元素。当对应于这些元素中的某个元素的回调函数发生时,标志着这个解析器正离开这个元素,我使用存储在currentText_中的字符数据来设置当前的Animal对象的name、species或者 dateofbirth。
SAX2的几个重要特性在示例14-6、14-7和14-8中都没有提到。例如,类SAX2XMLReader提供了parse()方法的一个重载函数,这个重载函数带一个xercesc::InputSource的实例作为参数来代替C语言风格的字符串。InputSource是一个封装了源字符数据的抽象类;它的具体子类包含xercesc::MemBufInputSource和xercesc::URLInputSource,让SAX2解析器来处理存储在任何位置上的XML文档,而不只是本地文件系统中的XML文档。
此外,这个ContentHandler包含很多附加的方法,如startDocument()和endDocmuent(),这标志着一个XML文档的开始和结束;以及setLocator(),它让你能指定一个Locator对象,这个Locator对象跟踪了当前正在处理的文件的当前位置。还有一些别的处理器接口,包含DTDHandler和EntityResolver,它们来自核心SAX2.0规范;以及DeclarationHandler和LexicalHandler,它们来自SAX2.0的标准扩展版本。
一个简单的类实现多个处理器接口也是可能的。类xercesc::DefaultHandler使得这项工作变得容易,因为它继承了所有的这些处理器接口并提供了这个虚函数的无操作的实现。因此,我把这个方法加到了从CircusErrorHandler到CircusContentHandler处理器中,示例14-8中的修改如下所示:
// Register handlers
CircusContentHandler handler(animalList);
parser->setContentHandler(&handler);
parser->setErrorHandler(&handler);
示例14-8中最后一个你应该注意的特性就是:CircusContentHandler并没有企图去验证这个正在解析的文档具有正确的结构,例如,它的根是一个animalList元素或者这个根的所有子元素都是animal元素。与示例14-3相反。例如,在示例14-3中的main()函数验证这个顶级元素是一个animalList,并且这个nodeToAnimal()函数验证它的参数代表一个精确地带有类型name、species、dateOfBirth、veterinarian和trainer的五个子元素的animal元素。
修改示例14-6是可能的,这样它就能执行这类错误检测。例如,示例14-9中的ContentHandler就验证这个文档的根元素是一个animalList,并且它的子元素是animal类型,animal元素的子元素不包含任何别的元素。它通过维持三个布尔标志:parsingAnimalList_、parsingAnimal_和parsingAnimalChild_,它们记录了在任何时间这个文档正被解析的地方。方法startElement()和endElement()为了一致性简单地更新这些标志并检查它们,它把这个更新当前Animal对象的工作委托给方法startAnimalChild()和endElementChild(),这两个方法的实现与示例14-6中的startElement()和endElement()方法相同。
示例14-9 验证animals.xml文档结构的SAX2 ContentHandler
// Implements callbacks which receive character data and
// notifications about the beginnings and ends of elements
class CircusContentHandler : public DefaultHandler {
public:
CircusContentHandler(vector<Animal>& animalList)
: animalList_(animalList), // list to be populated
parsingAnimalList_(false), // parsing state
parsingAnimal_(false), // parsing state
parsingAnimalChild_(false) // parsing state
{ }
// Receives notifications from the parser each time
// beginning of an element is encountered
void startElement(
const XMLCh *const uri, // Namespace uri
const XMLCh *const localname, // simple tag name
const XMLCh *const qname, // qualified tag name
const Attributes &attrs ) // Collection of attributes
{
static XercesString animalList = fromNative("animalList");
static XercesString animal = fromNative("animal");
static XercesString xmlns =
fromNative("http://www.feldman-family-circus.com");
// Validate the namespace uri
if (uri != xmlns)
throw runtime_error(
string("wrong namespace uri: ") + toNative(uri)
);
// (i) Update the flags parsingAnimalList_, parsingAnimal_,
// and parsingAnimalChild_, which indicate where we are
// within the document
// (ii) verify that the elements are correctly
// nested;
// (iii) Delegate most of the work to the method
// startAnimalChild()
if (!parsingAnimalList_) {
// We've just encountered the document root
if (localname == animalList) {
parsingAnimalList_ = true; // Update parsing state.
} else {
// Incorrect nesting
throw runtime_error(
string("expected 'animalList', got ") +
toNative(localname )
);
}
} else if (!parsingAnimal_) {
// We've just encountered a new animal
if (localname == animal) {
parsingAnimal_ = true; // Update parsing state.
animalList_.push_back(Animal());// Add an Animal to the list.
} else {
// Incorrect nesting
throw runtime_error(
string("expected 'animal', got ") +
toNative(localname )
);
}
} else {
// We're in the middle of parsing an animal element.
if (parsingAnimalChild_) {
// Incorrect nesting
throw runtime_error("bad animal element");
}
// Update parsing state.
parsingAnimalChild_ = true;
// Let startAnimalChild() do the real work
startAnimalChild(uri, localname, qname, attrs);
}
}
void endElement(
const XMLCh *const uri, // Namespace uri
const XMLCh *const localname, // simple tag name
const XMLCh *const qname ) // qualified tag name
{
static XercesString animalList = fromNative("animal-list");
static XercesString animal = fromNative("animal");
// Update the flags parsingAnimalList, parsingAnimal_,
// and parsingAnimalChild_; delegate most of the work
// to endAnimalChild()
if (localname == animal) {
parsingAnimal_ = false;
} else if (localname == animalList) {
parsingAnimalList_ = false;
} else {
endAnimalChild(uri, localname, qname);
parsingAnimalChild_ = false;
}
}
// Receives notifications when character data is encountered
void characters(const XMLCh* const chars, const unsigned int length)
{
// Append characters to currentText_ for processing by
// the method endAnimalChild()
currentText_.append(chars, length);
}
private:
// If the current element represents a veterinarian or trainer,
// use attrs to construct a Contact object for the current
// Animal; otherwise, clear currentText_ in preparation for the
// characters() callback
void startAnimalChild(
const XMLCh *const uri, // Namespace uri
const XMLCh *const localname, // simple tag name
const XMLCh *const qname, // qualified tag name
const Attributes &attrs ) // Collection of attributes
{
static XercesString vet = fromNative("veterinarian");
static XercesString trainer = fromNative("trainer");
Animal& animal = animalList_.back();
if (localname == vet) {
// We've encountered a "veterinarian" element.
animal.setVeterinarian(contactFromAttributes(attrs));
} else if (localname == trainer) {
// We've encountered a "trainer" element.
animal.setTrainer(contactFromAttributes(attrs));
} else {
// We've encountered a "name" , "species", or
// "dateOfBirth" element. Its content will be supplied
// by the callback function characters().
currentText_.clear();
}
}
// If the current element represents a name, species, or date
// of birth, use the text stored in currentText_ to set the
// appropriate property of the current Animal.
void endAnimalChild(
const XMLCh *const uri, // Namespace uri
const XMLCh *const localname, // simple tag name
const XMLCh *const qname ) // qualified tag name
{
static XercesString name = fromNative("name");
static XercesString species = fromNative("species");
static XercesString dob = fromNative("dateOfBirth");
// currentText_ contains the content of the element which has
// just ended. Use it to set the current Animal's properties.
Animal& animal = animalList_.back();
if (localname == name) {
animal.setName(toNative(currentText_));
} else if (localname == species) {
animal.setSpecies(toNative(currentText_));
} else if (localname == dob) {
animal.setDateOfBirth(toNative(currentText_));
}
}
vector<Animal>& animalList_; // list to be populated
bool parsingAnimalList_; // parsing state
bool parsingAnimal_; // parsing state
bool parsingAnimalChild_; // parsing state
XercesString currentText_; // character data of the
// current text node
};
比较示例14-9和示例14-6,你可能注意到了使用回调来验证一个文档的结构是多么复杂。此外,示例14-6仍然没有执行示例14-3中的那些验证:例如,它没有验证一个动物元素的子元素是按正确的顺序出现的, 幸运的是,SAX2中还是有更容易的方法来验证一个文档的结构,在14.5节和14.6节你将能看到。
请参考14.1节、14.4节、14.5节和14.6节。






