пятница, 25 октября 2013 г.

С++ работа с XML (TinyXML)



Зачастую необходимо вынести некоторые относительно постоянные настройки программы из хардкода (жестко зафиксированных в коде констант) куда-то во вне, делать какую-то выдачу, пересылать текстовую информацию и даже устраивать мини базу данных. Для этого годится текстовый формат XML.

Муки выбора

Писать велосипедный парсер - сложно новичкам, затратно по времени умелым, проще воспользоваться проверенной внешней библиотекой. Сейчас популярны две разновидности парсеров: работающих на основе DOM (document object model) и поточных, так называемых SAX-парсеров (Simple API for XML). SAX - стандарт парсеров, пришедший из Java API. Часто представляет собой базовый класс с набором виртуальных методов, которые вызываются по мере просмотра xml-файла (встретил открывающий тег - вызвал определенный метод, встретил аттрибут - вызвал другой метод, встретил закрывающий тег - вызвал третий, и т.д.). Этот класс можно расширить (унаследоваться), заменив необходимые методы (в базовом классе они ничего не делают) на что-то значимое в рамках нашей программы. Парсеры на основе DOM работают по принципу, близкому к доступу к DOM html-документа в javascript: имея ссылку на некоторый объект DOM, мы можем обратиться к его потомкам/родителю/соседним объектам. Безусловно, этот подход удобнее, чем в SAX, однако, требует загрузить в память весь файл, что бывает сложно, если он становится крупным объемом. SAX-парсер всего-навсего проходит вдоль всего xml, храня в памяти только текущий элемент, позволяя обрабатывать xml-файлы, ограниченные только правилами ОС.

TinyXML

Сейчас я легко задену один популярный XML-DOM-парсер TinyXML. Там же можно найти ссылку на SourceForge для скачивания исходников. Распространяется библиотека в виде исходных кодов. Для Windows, например, в виде тестового проекта под MSVS2010. Оттуда с корнем можно забрать 6 файлов (2 заголовочных (.h) и 4 исходных(.brush: cpp), все, кроме xmltest.brush: cpp) и просто добавить в свой проект и приинклудить tinyxml.h где необходимо.

Также внутри архива с библиотекой лежит неплохой справочник по классам, однако, туториал, на мой взгляд, там просто ужасен! =) Собственно, потому решил и писать данный Quick Guide.

К станку!

Для примера, пусть есть записная книжка с названием и набором записей, у записей есть название, сам текст записи и набор тегов, у тегов есть имя. У нас есть классы:



// Будем считать, что эти заголовки тоже включаем
#include <string> // Ради строк std::string
#include <vector> // Ради массивов std::vector
#include <memory> // Ради умных указателей std::shared_ptr и std::make_shared()

struct Note {
std::string title;
std::string text;
std::vector<std::string> tags;
};

struct Notebook {
std::string title;
std::vector<Note> notes;
};

Пусть это struct, чтобы не захламлять код методами доступа. У нас есть объект Notebook ntb. Напоминаю, что метод std::string::c_str() возвращаяет const char*, необходимый библиотеке. Нам нужно сгенерить и считать такой файл:

<?xml version="1.0" encoding="UTF-8"/> 
<Notebook title="Name of notebook">
<Note title="Title of note" text="Text of note">
<Tag name="tagName"/>
<Tag name="tagName2"/>
</Note>
<Note title="Title of note2" text="SomeText">
<Tag name="tagName"/>
</Note>
</Notebook>

Сначала нужно создать объект документа и добавить строчку декларации:

TiXmlDocument doc;
TiXmlDeclaration *decl = new TiXmlDeclaration("1.0", "UTF-8", "");
doc.LinkEndChild(decl);

Да! Как и в Qt, здесь дочерние объекты создаются динамически и удаляются автоматически (delete не нужен здесь и даже опасен!). Теперь нам нужно добавить элемент Notebook, заполнить аттрибут title.

auto *notebookItem = new TiXmlElement("Notebook"); // Создаем элементdoc.LinkEndChild(notebookItem);    // Цепляем его к документу в конец
notebookItem->SetAttribute("title", ntb.title.c_str()); // Назначаем аттрибут

Теперь нужен цикл для добавления отдельных записей:

for (int i=0, sizeOfNotesArray=ntb.notes.size(); i<sizeOfNotesArray; ++i) {
auto *noteItem = new TiXmlElement("Note");
notebookItem->LinkEndChild(noteItem); // Цепляем теперь уже к элементу записной книжки!
noteItem->SetAttribute("title", ntb.notes[i].text.c_str());
noteItem->SetAttribute("text", ntb.notes[i].text.c_str());
...

Отлично! Однако, внутри еще и теги, сделаем цикл и для них!

for (int j=0, sizeOfTagsArray=ntb.notes.size(); j<sizeOfTagsArray; ++j) {
auto *tagItem = new TiXmlElement("Tag");
noteItem->LinkEndChild(tagItem);
tagItem->SetAttribute("name", ntb.notes[i].tags[j].c_str());
}

Теперь нужно записать это либо в файл командой:

doc.SaveFile ("filepath");

или сохранить документ в строку, чтобы делать с ним ужасы:

TiXmlPrinter printer;
doc.Accept(&printer);
std::string result = printer.CStr();

Все, готово, вы великолепны! Итого:

std::shared_ptr<std::string> writeToXML( Notebook ntb ){
TiXmlDocument doc;
TiXmlDeclaration *decl = new TiXmlDeclaration("1.0", "UTF-8", "");
doc.LinkEndChild(decl);

// Adding notebook element
auto *notebookItem = new TiXmlElement("Notebook"); // Создаем элемент
doc.LinkEndChild(notebookItem); // Цепляем его к документу в конец
notebookItem->SetAttribute("title", ntb.title.c_str()); // Назначаем аттрибут

// Adding notes elements
for (int i=0, sizeOfNotesArray=ntb.notes.size(); i < sizeOfNotesArray; ++i) {
auto *noteItem = new TiXmlElement("Note");
notebookItem->LinkEndChild(noteItem); // Цепляем теперь уже к элементу записной книжки!
noteItem->SetAttribute("title", ntb.notes[i].text.c_str());
noteItem->SetAttribute("text", ntb.notes[i].text.c_str());
// Adding tags elements
for (int j=0, sizeOfTagsArray=ntb.notes.tags.size(); j < sizeOfTagsArray; ++j) {
auto *tagItem = new TiXmlElement("Tag");
noteItem->LinkEndChild(tagItem);
tagItem->SetAttribute("name", ntb.notes[i].tags[j].c_str());
}
}
TiXmlPrinter printer;
doc.Accept(&printer);
std::shared_ptr<std::string> result = std::make_shared < std::string > (printer.CStr());
return result;
}

Теперь аналогично запись. Предлагаю попробовать переделать пример, учитывая, что Notebook может быть несколько в документе и функция возвращает, например, std::vector.

Notebook readFromXML( const std::string& xmlString ){
// Пытаемся обработать документ
TiXmlDocument doc;
doc.Parse(xmlString.c_str(), 0, TIXML_ENCODING_UTF8);

// Обработка ошибок. Например, XML не валиден
if ( doc.Error() )
return 0;

// Считываем первый элемент документа (Notebook)
TiXmlElement *notebookItem = doc.FirstChildElement();
Notebook resultNotebook;
resultNotebook.title = notebookItem->Attribute("title"));

// Читаем записи
for (TiXmlElement *noteItem = notebookItem->FirstChildElement();
noteItem != nullptr;
noteItem=noteItem->NextSiblingElement("Note"))
{
// Читаем заголовок и текст записи из аттрибутов
Note newNote { noteItem->Attribute("title"), noteItem->Attribute("text") };

// Читаем теги
for (TiXmlElement *tag = noteItem->FirstChildElement();
tag != nullptr;
tag=tag->NextSiblingElement("Tag"))
{
newNote.tags.push_back(tag->Attribute("name"));
}
// Добавляем очередную запись в массив
resultNotebook.notes.push_back(newNote);
}
return resultNotebook;
}

Комментариев нет:

Отправить комментарий