14.1. Синтаксический анализ простого документа XML

14.1. Синтаксический анализ простого документа XML

Проблема

Имеется некоторая совокупность данных, хранимых в документе XML. Требуется выполнить синтаксический анализ документа и превратить эти данные в объекты C++. Документ XML имеет достаточно небольшой размер и может поместиться в оперативной памяти, причем в документе не используется внутреннее определение типа документа (Document Type Definition — DTD) и отсутствуют пространства имен XML.

Решение

Используйте библиотеку TinyXml. Во-первых, определите объект типа TiXmlDocument и вызовите его метод LoadFile(), передавая полное имя файла вашего XML-документа в качестве его аргумента. Если LoadFile() возвращает значение «true», то это означает, что анализ вашего документа завершился успешно. В этом случае вызовите метод RootElement() для получения указателя на объект типа TiXmlElement, представляющего корневой элемент документа. Этот объект имеет иерархическую структуру, которая соответствует структуре вашего документа XML; выполняя проход по этой структуре, вы можете извлечь информацию о документе и использовать ее для создания набора объектов С++.

Например, предположим, что у вас имеется XML-документ animals.xml, описывающий некоторое количество животных цирка, как показано в примере 14.1. Корень документа имеет имя animalList и содержит несколько дочерних элементов animal, каждый из которых представляет одно животное, принадлежащее цирку Feldman Family Circus. Предположим также, что у вас имеется класс C++ с именем Animal, и вам нужно сконструировать вектор std::vector, состоящий из объектов Animal, представляющих животных, перечисленных в документе.

Пример 14.1. Документ XML со списком животных цирка

<?xml version="1.0" encoding="UTF-8"?>

<!- Животные цирка Feldman Family Circus -->

<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, который определен также в примере 14.2. Пример 14.3 показывает, как можно использовать TinyXml для синтаксического анализа документа animals.xml, просмотра разобранного документа и заполнения вектора std::vector объектов Animal, используя извлеченные из документа данные.

Пример 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>

// Представляет ветеринара или дрессировщика

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;

  // Используйте Boost.Regex, чтобы убедиться, что телефон

  // задач в форме (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_;

};

// Сравнить на равенство два объекта класса Contact; используется в рецепте

// 14.9 (для полноты следует также определить operator!=)

bool operator--(const Contact& lhs, const Contact& rhs) {

 return lhs.name() == rhs.name() && lhs.phone() == rhs.phone();

}

// Записывает объект класса Contact в поток ostream

std::ostream& operator(std::ostream& out, const Contact& contact) {

 out << contact.name() << " " << contact.phone(); return out;

}

// Класс Animal представляет животное

class Animal {

public:

 // Конструктор по умолчанию класса Animal; этот конструктор будет вами

 // использоваться чаще всего Animal() {}

 // Конструирование объекта Animal с указанием свойств животного;

 // этот конструктор будет использован в рецепте 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)

  }

 // Функции доступа к свойствам животного

 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_; }

 // Функции задания свойств животного

 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_;

};

// Сравнение на равенство двух объектов Animal; используется в рецепте 14.9

// (для полноты следует также определить 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();

}

// Записывает объект Animal в поток ostream

std::ostream& operator<<(std::ostream& out, const Animal& animal) {

 out << "Animal { "

  << " name=" << animal.name() << "; "

  << " species=" << animal.species() << "; "

  << date-of-birth=" << animal.dateOfBirth() << "; "

  << " veterinarian=" << animal.veterinarian() << "; "

  << " trainer=" << animal.trainer() << "; "

  << "}";

 return out;

}

#endif // #ifndef ANIMALS_HPP_INCLUDED

Пример 14.3. Синтаксический анализ animals.xml с помощью TinyXml

#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;

// Извлекает текстовое содержимое элемента XML

const char* textValue("TiXmlElement* e) {

 TiXmlNode* first = fi->FirstChild();

 if (first != 0 && first == e->LastChild() &&

  first->Type() == TiXmlNode::TEXT) {

  // элемент «е» имеет один дочерний элемент типа TEXT;

  // возвратить дочерний элемент

  return first->Value();

 } else {

  throw runtime_error(string("bad ") + e->Value() + " element");

 }

}

// Конструирует объект класса Contact из элементов ветеринара или

// дрессировщика ("veterinarian" или "trainer")

Contact nodeToContact(TiXmlElement* contact) {

 using namespace std;

 const char *name, *phone;

 if (contact->FirstChild() == 0 &&

  (name = contact->Attribute("name")) &&

  (phone = contact->Attribute("phone"))) {

  // Элемент contact не имеет дочерних элементов и имеет атрибуты имени

  // и телефона ("name" и "phone"); используйте эти значения для

  // конструирования объекта Contact

  return Contact(name, phone);

 } else {

  throw runtime_error(string("bad ") + contact->Value() + " element");

 }

}

// Конструирует объект Animal из элемента животного ("animal")

Animal nodeToAnimal(TiXmlElement* animal) {

 using namespace std;

 // Убедиться, что animal соответствует элементу "animal"

 if (strcmp(animal->Value(), "animal") != 0) {

  throw runtime_error(string("bad animal: ") + animal->Value());

 }

 Animal result; // Возвратить значение

 TiXmlElement* element = animal->FirstChildElement();

 // Прочитать элемент клички животного

 if (element && strcmp(element->Value(), "name") == 0) {

  // Первым дочерним элементом объекта animal является кличка (элемент

  // name"); используйте ее текстовое значение для установки клички

  // в объекте result

  result.setName(textValue(element));

 } else {

  throw runtime_error("no name attribute");

 }

 // Прочитать элемент вида животного

 element = element->NextSiblingElement();

 if (element && strcmp(element->Value(), species") == 0) {

  // Вторым дочерним элементом animal является вид животного

  // (элемент "species"); используйте его текстовое значение для

  // установки вида в объекте result

  result.setSpecies(textValue(element));

 } else {

  throw runtime_error(""no species attribute");

 }

 // Прочитать элемент даты рождения

 element = element->NextSiblingElement();

 if (element && strcmp(element->Value(), "dateOfBirth") == 0) {

  // Третьим дочерним элементом animal является дата рождения

  // (элемент "dateOfBirth"));

  // используйте его текстовое значение для установки даты

  // рождения в объекте result

  result.setDateOfBirth(textValue(element));

 } else {

  throw runtime_error("no dateOfBirth attribute");

 }

 // Прочитать элемент ветеринара

 element = element->NextSiblingElement();

 if (strcmp(element->Value(), "veterinarian") == 0) {

  // Четвертым дочерним элементом animal является ветеринар (элемент

  // "veterinarian"); используйте его для конструирования объекта

  // Contact и установки имени ветеринара в объекте result

  result.setVeterinarian(nodeToContact(element));

 } else {

  throw runtime_error("no veterinarian attribute");

 }

 // Прочитать элемент дрессировщика

 element = element->NextSiblingElement();

 if (strcmp(element->Value(), "trainer") == 0) {

  // Пятым элементом animal является дрессировщик (элемент "trainer");

  // используйте его для конструирования объекта

  // Contact и установки дрессировщика в объекте result

  result.setTrainer(nodeToContact(element));

 } else {

  throw runtime_error("no trainer attribute");

 }

 // Убедиться в отсутствии других дочерних элементов

 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;

  // Обработать "animals.xml"

  TiXmlDocument doc("animals.xml");

  if (!doc.LoadFile())

   throw runtime_error("bad parse");

  // Убедиться, что корневым является список животных

  TiXmlElement* root = doc.RootElement();

  if (strcmp(root->Value(), "animalList") != 0) {

   throw runtime_error(string("bad root: ") + root->Value());

  }

  // Просмотреть все дочерние элементы корневого элемента, заполняя

  // список животных

  for (TiXmlElement* animal = root->FirstChildElement();

   animal; animal = animal->NextSiblingElement()) {

   animalList.push_back(nodeToAnimal(animal));

  }

  // Напечатать клички животных

  for (vector<Animal>::size_type i = 0, n = animalList.size(); i < n; ++i) {

   cout << animalList[i] << " ";

  }

 } catch (const exception& e) {

  cout << e.what() << " ";

  return EXIT_FAILURE;

 }

}

Обсуждение

TinyXml (буквально «крошечный XML») очень хорошо подходит в тех случаях, когда требуется выполнять несложную обработку документов XML. Дистрибутив исходных текстов этой библиотеки небольшой, ее легко построить и интегрировать в проекты, и она имеет очень простой интерфейс. Она также имеет очень либеральную лицензию. Главными ограничениями TinyXml являются невосприимчивость к пространствам имен XML, невозможность контроля DTD или схемы, а также невозможность анализа документов XML с внутренним DTD. Если вам требуется какая-то из этих функций или какая-нибудь XML-технология, как, например, XPath или XSLT, то необходимо воспользоваться другими библиотеками, рассмотренными в данной главе.

На выходе парсера TinyXml получается документ XML в виде дерева, узлы которого представляют элементы, текст, комментарии и другие компоненты документа XML. Корень дерева представляет собственно документ XML. Такое иерархическое представление документа называется объектной моделью документа (Document Object Model - DOM). Модель DOM, полученная парсером TinyXml, аналогична модели, разработанной консорциумом W3C (World Wide Web Consortium), хотя она и не полностью соответствует спецификации W3C. Вследствие приверженности библиотеки TinyXml принципам минимализма модель TinyXml DOM проще W3С DOM, однако она обладает меньшими возможностями.

Получить доступ к узлам дерева, представляющего документ XML, можно с помощью интерфейса TiXmlNode, который содержит методы, обеспечивающие доступ к родительскому узлу, последовательный доступ ко всем дочерним узлам, удаление и добавление дочерних узлов. Каждый узел является экземпляром некоторого производного типа; например, корень дерева является экземпляром TiXmlDocument, узлы элементов являются экземплярами TiXmlElement, а узлы, представляющие текст, являются экземплярами TiXmlText. Тип TiXmlNode можно определить с помощью его метода Туре(); зная тип узла, вы можете получить конкретное его представление с помощью таких методов, как toDocument(), toElement() и toText(). Эти производные типы содержат дополнительные методы, характерные для узлов конкретного типа.

Теперь несложно разобраться с примером 14.3. Во-первых, функция textValue() извлекает текстовое содержимое из элементов, содержащих только текст, например name, species или dateOfBirth. В этом случае данная функция сначала убеждается, что имеется только один дочерний элемент и что он является текстовым узлом. Она затем получает текст дочернего элемента, вызывая метод Value(), который возвращает текстовое содержимое текстового узла или узла комментария, имя тега узла элемента и имя файла корневого узла.

На следующем шаге функция nodeToContact() получает узел, соответствующий элементу veterinarian или trainer, и конструирует объект Contact из значений атрибутов name и phone, получаемых с помощью метода Attribute().

Аналогично функция nodeToAnimal() получает узел, соответствующий элементу животного element, и конструирует объект Animal. Это делается путем прохода по дочерним узлам с помощью метода NextSiblingElement(), извлекая при этом содержащиеся в каждом элементе данные и устанавливая соответствующее свойство объекта Animal. Данные извлекаются, используя функцию textValue() для элементов name, species и dateOfBirth и функцию nodeToContact() для элементов veterinarian и trainer.

В функции main я сначала конструирую объект TiXmlDocument соответствующий файлу animals.xml, и выполняю его синтаксический разбор с помощью метода LoadFile(). Затем я получаю элемент TiXmlElement, соответствующий корню документа, вызывая метод RootElement(). На следующем шаге я просматриваю все дочерние узлы корневого элемента, конструируя объект Animal из каждого элемента animal с помощью функции nodeToAnimal(). Наконец, я прохожу по всем объектам Animal, записывая их в стандартный вывод.

В примере 14.3 не проиллюстрирована одна функция библиотеки TinyXml, а именно метод SaveFile() класса TiXmlDocument, который записывает в файл документ, представляемый объектом TiXmlDocument. Это позволяет выполнить синтаксический разбор документа XML, модифицировать его, используя интерфейс DOM, и сохранить модифицированный документ. Документ TiXmlDocument можно создать даже с чистого листа и затем сохранить его на диске.

// Создать документ hello.xml, состоящий

// из единственного элемента "hello"

TiXmlDocument doc;

TiXmlElement root("hello");

doc.InsertEndChild(root);

doc.SaveFile("hello.xml");

Смотри также

Рецепты 14.3 и 14.4.