14.1 解析一个简单的XML文档
问题
有一个存储在XML文档中的数据集合,如何解析这个文档,并把这些数据存到一个C++集合对象中?你的XML文档足够小以至于它可以全部放到内存中,并且不需要使用内部文档类型定义(DTD)或者XML命名空间。
解决方案
使用TinyXml类库。首先,定义一个类型为TiXmlDocument的对象并且调用它的LoadFile()方法,把你的XML文档的路径名作为参数传给它。如果LoadFile()方法返回真值的话,就表明你的XML文档被成功地解析。如果解析成功,就调用RootElement()方法来获得这个类型为TiXmlElement的对象,它代表这个XML文档的根对象。这个对象具有一个层次结构,它反映了你的XML文档的结构;通过遍历这个结构,你就可以获取关于你的文档的信息并且使用这些信息来创建一个C++的集合对象。
例如,假设你有一个XML文档animals.xml,它代表马戏团动物的集合,如示例14-1所示。这个文档的根是一个名为animalList的元素并且它还有很多子动物元素,每一个都代表Feldman家庭马戏团拥有的一个动物。假设你有一个名为Animal的C++类,并且你想根据这个文档中相应的动物来构造一个Animal类型的std::vector。
示例14-1 一个表示马戏团动物的XML文档
<?xml version="1.0" encoding="UTF-8"?>
<!-- Feldman Family Circus Animals -->
<animalList>
<animal>
<name>Herby</name>
<species>elephant</species>
<dateOfBirth>1992-04-23</dateOfBirth>
<veterinarian name="Dr. Hal Brown" phone="(801)595-9627"/>
<trainer name="Bob Fisk" phone="(801)881-2260"/>
</animal>
<animal>
<name>Sheldon</name>
<species>parrot</species>
<dateOfBirth>1998-09-30</dateOfBirth>
<veterinarian name="Dr. Kevin Wilson" phone="(801)466-6498"/>
<trainer name="Eli Wendel" phone="(801)929-2506"/>
</animal>
<animal>
<name>Dippy</name>
<species>penguin</species>
<dateOfBirth>2001-06-08</dateOfBirth>
<veterinarian name="Dr. Barbara Swayne" phone="(801)459-7746"/>
<trainer name="Ben Waxman" phone="(801)882-3549"/>
</animal>
</animalList>
示例14-2说明了这个Animal类的定义。Animal有五个数据成员,这五个数据成员对应于这个动物的名字、种类、出生日期、兽医和培训师。其中动物的名字和种类是std::string类型,出生日期是boost::gregorian::date类型,这个类型来自于Boost.Date_Time类型,并且它的兽医和培训师则是Contact类的一个实例,这个Contact类定义在示例14-2中。示例14-3说明如何使用TinyXml来解析这个animals.xml文档,遍历这个解析好的文档并且使用从这个文档中获取到的数据来填充这个Animals类型的std::vector。
示例14-2 animal.hpp头文件
#ifndef ANIMALS_HPP_INCLUDED
#define ANIMALS_HPP_INCLUDED
#include <ostream>
#include <string>
#include <stdexcept> // runtime_error
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/regex.hpp>
// Represents a veterinarian or trainer
class Contact {
public:
Contact() { }
Contact(const std::string& name, const std::string& phone)
: name_(name)
{
setPhone(phone);
}
std::string name() const { return name_; }
std::string phone() const { return phone_; }
void setName(const std::string& name) { name_ = name; }
void setPhone(const std::string& phone)
{
using namespace std;
using namespace boost;
// Use Boost.Regex to verify that phone
// has the form (ddd)ddd-dddd
static regex pattern("\\([0-9]{3}\\)[0-9]{3}-[0-9]{4}");
if (!regex_match(phone, pattern)) {
throw runtime_error(string("bad phone number:") + phone);
}
phone_ = phone;
}
private:
std::string name_;
std::string phone_;
};
// Compare two Contacts for equality; used in Recipe 14.9
// (for completeness, you should also define operator!=)
bool operator==(const Contact& lhs, const Contact& rhs)
{
return lhs.name() == rhs.name() && lhs.phone() == rhs.phone();
}
// Writes a Contact to an ostream
std::ostream& operator<<(std::ostream& out, const Contact& contact)
{
out << contact.name() << " " << contact.phone();
return out;
}
// Represents an animal
class Animal {
public:
// Default constructs an Animal; this is
// the constructor you'll use most
Animal() { }
// Constructs an Animal with the given properties;
// you'll use this constructor in Recipe 14.9
Animal( const std::string& name,
const std::string& species,
const std::string& dob,
const Contact& vet,
const Contact& trainer )
: name_(name),
species_(species),
vet_(vet),
trainer_(trainer)
{
setDateOfBirth(dob);
}
// Getters
std::string name() const { return name_; }
std::string species() const { return species_; }
boost::gregorian::date dateOfBirth() const { return dob_; }
Contact veterinarian() const { return vet_; }
Contact trainer() const { return trainer_; }
// Setters
void setName(const std::string& name) { name_ = name; }
void setSpecies(const std::string& species) { species_ = species; }
void setDateOfBirth(const std::string& dob)
{
dob_ = boost::gregorian::from_string(dob);
}
void setVeterinarian(const Contact& vet) { vet_ = vet; }
void setTrainer(const Contact& trainer) { trainer_ = trainer; }
private:
std::string name_;
std::string species_;
boost::gregorian::date dob_;
Contact vet_;
Contact trainer_;
};
// Compare two Animals for equality; used in Recipe 14.9
// (for completeness, you should also define operator!=)
bool operator==(const Animal& lhs, const Animal& rhs)
{
return lhs.name() == rhs.name() &&
lhs.species() == rhs.species() &&
lhs.dateOfBirth() == rhs.dateOfBirth() &&
lhs.veterinarian() == rhs.veterinarian() &&
lhs.trainer() == rhs.trainer();
}
// Writes an Animal to an ostream
std::ostream& operator<<(std::ostream& out, const Animal& animal)
{
out << "Animal {\n"
<< " name=" << animal.name() << ";\n"
<< " species=" << animal.species() << ";\n"
<< " date-of-birth=" << animal.dateOfBirth() << ";\n"
<< " veterinarian=" << animal.veterinarian() << ";\n"
<< " trainer=" << animal.trainer() << ";\n"
<< "}";
return out;
}
#endif // #ifndef ANIMALS_HPP_INCLUDED
示例14-3 使用TinyXml解析animals.xml
#include <exception>
#include <iostream> // cout
#include <stdexcept> // runtime_error
#include <cstdlib> // EXIT_FAILURE
#include <cstring> // strcmp
#include <vector>
#include <tinyxml.h>
#include "animal.hpp"
using namespace std;
// Extracts the content of an XML element that contains only text
const char* textValue(TiXmlElement* e)
{
TiXmlNode* first = e->FirstChild();
if ( first != 0 &&
first == e->LastChild() &&
first->Type() == TiXmlNode::TEXT )
{
// the element e has a single child, of type TEXT;
// return the child's
return first->Value();
} else {
throw runtime_error(string("bad ") + e->Value() + " element");
}
}
// Constructs a Contact from a "veterinarian" or "trainer" element
Contact nodeToContact(TiXmlElement* contact)
{
using namespace std;
const char *name, *phone;
if ( contact->FirstChild() == 0 &&
(name = contact->Attribute("name")) &&
(phone = contact->Attribute("phone")) )
{
// The element contact is childless and has "name"
// and "phone" attributes; use these values to
// construct a Contact
return Contact(name, phone);
} else {
throw runtime_error(string("bad ") + contact->Value() + " element");
}
}
// Constructs an Animal from an "animal" element
Animal nodeToAnimal(TiXmlElement* animal)
{
using namespace std;
// Verify that animal corresponds to an "animal" element
if (strcmp(animal->Value(), "animal") != 0) {
throw runtime_error(string("bad animal: ") + animal ->Value());
}
Animal result; // Return value
TiXmlElement* element = animal->FirstChildElement();
// Read name
if (element && strcmp(element->Value(), "name") == 0) {
// The first child element of animal is a "name"
// element; use its text value to set the name of result
result.setName(textValue(element));
} else {
throw runtime_error("no name attribute");
}
// Read species
element = element->NextSiblingElement();
if (element && strcmp(element->Value(), "species") == 0) {
// The second child element of animal is a "species"
// element; use its text value to set the species of result
result.setSpecies(textValue(element));
} else {
throw runtime_error("no species attribute");
}
// Read date of birth
element = element->NextSiblingElement();
if (element && strcmp(element->Value(), "dateOfBirth") == 0) {
// The third child element of animal is a "dateOfBirth"
// element; use its text value to set the date of birth
// of result
result.setDateOfBirth(textValue(element));
} else {
throw runtime_error("no dateOfBirth attribute");
}
// Read veterinarian
element = element->NextSiblingElement();
if (strcmp(element->Value(), "veterinarian") == 0) {
// The fourth child element of animal is a "veterinarian"
// element; use it to construct a Contact object and
// set result's veterinarian
result.setVeterinarian(nodeToContact(element));
} else {
throw runtime_error("no veterinarian attribute");
}
// Read trainer
element = element->NextSiblingElement();
if (strcmp(element->Value(), "trainer") == 0) {
// The fifth child element of animal is a "trainer"
// element; use it to construct a Contact object and
// set result's trainer
result.setTrainer(nodeToContact(element));
} else {
throw runtime_error("no trainer attribute");
}
// Check that there are no more children
element = element->NextSiblingElement();
if (element != 0) {
throw runtime_error(
string("unexpected element:") +
element->Value()
);
}
return result;
}
int main()
{
using namespace std;
try {
vector<Animal> animalList;
// Parse "animals.xml"
TiXmlDocument doc("animals.xml");
if (!doc.LoadFile())
throw runtime_error("bad parse");
// Verify that root is an animal-list
TiXmlElement* root = doc.RootElement();
if (strcmp(root->Value(), "animalList") != 0) {
throw runtime_error(string("bad root: ") + root->Value());
}
// Traverse children of root, populating the list
// of animals
for ( TiXmlElement* animal = root->FirstChildElement();
animal;
animal = animal->NextSiblingElement() )
{
animalList.push_back(nodeToAnimal(animal));
}
// Print the animals' names
for ( vector<Animal>::size_type i = 0,
n = animalList.size();
i < n;
++i )
{
cout << animalList[i] << "\n";
}
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
讨论
对于这种简单的XML处理程序,TinyXml是一个最好的选择。它的源代码很少,并且很容易和项目整合,接口也很简单。同时它的许可证也非常适合。它的主要缺点就是它不支持XML命名空间,不能使用DTD或者schema来对XML文档做合法性检查,并且不能处理内部包含DTD的XML文档。如果你需要这些功能特性或者其他的XML相关的技术(如XPath或XSLT)的话,你需要使用本章描述的其他类库。
这个TinyXml解析器产生一个代表XML文档的树,这个树的节点代表XML文档的元素、文本、注释和其他的XML文档组件。这个树的根代表XML文档本身。这个作为树的层次文档就叫做文档对象模型(DOM)。这个TinyXml的DOM和W3C联盟设计的DOM类似,但它不遵从W3C的规范。在保持TinyXml的最小特性下,TinyXml的DOM比W3C简单,但也没有W3C的DOM功能强大。
这个代表XML文档的树的节点可以通过TiXmlNode接口来访问,这个接口提供了方法来访问节点的父节点、枚举所有的子节点、删除子节点或者插入一些附加的子节点。每个节点事实上都是某个继承的类型的实例;例如,这个树的根节点是TiXmlDocument的一个实例,代表元素的节点是TiXmlElement的实例,而代表文本的节点是TiXmlText的一个实例。TiXmlNode节点的类型可以通过调用Type()方法来获取;一旦你知道一个节点的类型,你就可以调用某些方便的方法如toDocument()、toElement()和toText()来获取它所代表的内容。这些继承的类型还包含一些与这个继承类型相对应的方法。
示例14-3是很容易明白的。首先,这个textValue()函数可以获取这个仅包含文本的元素的文本内容,如名字、种类或者出生日期等。它首先验证这个元素只有一个子节点并且这个子节点是一个文本节点。然后通过调用Value()方法来获取这个子节点的文本内容,这个方法可以返回这个文本节点或注释节点的内容、元素节点的标签名字和一个根节点的文件名字。
接下来nodeToContact()函数带一个对应于兽医或培训师元素的节点并且从它的名字和电话属性的值构造一个Contact对象,而这些属性可以通过使用Attribute()方法来获取。
相同地,这个nodeToAnimal()带一个对应于动物元素的节点并且构造一个Animal对象。它这样做是通过使用NextSiblingElement()方法来枚举它的每个子节点,然后获取包含在每一个元素中的数据,并且给这个Animal对象设置相应的属性。这些数据的获取是通过在元素的name、species和dateOfBirth上调用textValue()方法得到的,并且通过在兽医和培训师元素上使用nodeToContact()获得对应的数据。
在这个主函数中,我首先构造一个对应于文件animals.xml的TiXmlDocument对象并且使用LoadFile()方法来解析它。然后我调用RootElement()方法来获取对应于这个XML文档根节点的TiXmlElement对象。接下来,我再枚举这个根节点元素的所有子节点,使用nodeToAnimal()来给每一个动物元素构造一个Animal对象。最后,我再枚举每一个Animal对象并把它们输出到标准输出上。
在示例14-3中没有谈到的关于TinyXml类库的一个特性就是TiXmlDocument类的SaveFile()方法,它把这个代表XML文档的TiXmlDocument写到一个文件中。这就可以让你解析一个XML文档,使用DOM接口修改它,并且把这个修改过的文档保存到一个文件中。你甚至可以从一个草稿中创建一个TiXmlDocument并把它保存到硬盘中:
// Create a document hello.xml, consisting
// of a single "hello" element
TiXmlDocument doc;
TiXmlElement root("hello");
doc.InsertEndChild(root);
doc.SaveFile("hello.xml");
请参考14.3节和14.4节。






