|
||
![]() |
||
XML (от англ. Extensible Markup Language -- Расширяемый Язык Разметки) -- популярный формат файлов, используемый для обмена и хранения данных в текстовом виде.
Для работы с XML документами, Qt поддерживает два различных API:
SAX (от англ. Simple API for XML -- Простейший Прикладной Интерфейс для работы с XML) -- используется для выполнения синтаксического анализа методом обработки событий разбора прямо в приложении, с помощью виртуальных функций.
DOM (от англ. Document Object Model -- Объектная Модель представления Документов) -- преобразует XML-документ в древовидную структуру, в результате приложение получает возможность навигации по ней.
В этой главе мы покажем, как работать с XML-файлами посредством обоих API.
SAX -- это (де-факто) Java API стандарт для чтения XML-документов. Классы SAX, в библиотеке Qt, моделируют реализацию SAX2 Java, с небольшими отличиями в именованиях. Дополнительную информацию о SAX вы найдете по адресу: http://www.saxproject.org/.
Qt предоставляет SAX-парсер QXmlSimpleReader. Он распознает правильно оформленные XML-документы и поддерживает пространства имен XML. Во время анализа документа вызываются виртуальные функции классов-обработчиков событий разбора. (В данном случае, понятие "событие разбора" никак не пересекается с понятием событий в Qt.) Например, предположим, что парсер анализирует XML-документ со следующим содержимым:
<doc> <quote>Errare humanum est</quote> </doc>В этом случае парсер мог бы вызвать следующие обработчики событий разбора:
startDocument() startElement("doc") startElement("quote") characters("Errare humanum est") endElement("quote") endElement("doc") endDocument()Все вышеприведенные функции определены в классе QXmlContentHandler. С целью упрощения примера мы не приводим некоторые аргументы в функциях startElement() и endElement().
Класс QXmlContentHandler -- лишь один из многих, которые могут работать совместно с QXmlSimpleReader. Среди других классов можно назвать: QXmlEntityResolver, QXmlDTDHandler, QXmlErrorHandler, QXmlDeclHandler и QXmlLexicalHandler. Они реализуют исключительно виртуальные функции и предоставляют сведения о различного типа событиях разбора. В большинстве приложений используются только два класса: QXmlContentHandler и QXmlErrorHandler.
Для большего удобства, Qt так же предоставляет класс QXmlDefaultHandler, который наследует (через множественное наследование) и реализует все виртуальные функции других классов-обработчиков. Такая архитектура, со множеством абстрактных классов и единственным классом-наследником, довольно необычна для Qt, однако, она была принята в соответствии с моделью реализации, принятой в Java.
Рассмотрим на примере, как можно использовать классы QXmlSimpleReader и QXmlDefaultHandler для разбора XML-файла и отображения его содержимого в QListView. Наш класс, производный от класса QXmlDefaultHandler, будет называться SaxHandler. В его задачи будет входить разбор XML-документа, представляющего собой список терминов, использовавшихся в книге.
Рисунок 14.1. Дерево наследования класса SaxHandler.
<?xml version="1.0"?> <bookindex> <entry term="sidebearings"> <page>10</page> <page>34-35</page> <page>307-308</page> </entry> <entry term="subtraction"> <entry term="of pictures"> <page>115</page> <page>244</page> </entry> <entry term="of vectors"> <page>9</page> </entry> </entry> </bookindex>
Рисунок 14.2. Файл со списком терминов, использованных в книге, загруженный в QListView.
class SaxHandler : public QXmlDefaultHandler { public: SaxHandler(QListView *view); bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attribs); bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName); bool characters(const QString &str); bool fatalError(const QXmlParseException &exception); private: QListView *listView; QListViewItem *currentItem; QString currentText; };Класс SaxHandler порожден от класса QXmlDefaultHandler и перекрывает четыре метода родителя: startElement(), endElement(), characters() и fatalError(). Первые три функции объявлены в классе QXmlContentHandler, последняя функция -- в QXmlErrorHandler.
SaxHandler::SaxHandler(QListView *view) { listView = view; currentItem = 0; }Конструктор получает указатель на QListView, который будет заполняться информацией из XML-файла.
bool SaxHandler::startElement(const QString &, const QString &, const QString &qName, const QXmlAttributes &attribs) { if (qName == "entry") { if (currentItem) { currentItem = new QListViewItem(currentItem); } else { currentItem = new QListViewItem(listView); } currentItem->setOpen(true); currentItem->setText(0, attribs.value("term")); } else if (qName == "page") { currentText = ""; } return true; }Функция startElement() вызывается, когда парсер встречает новый открывающий тег. Третий аргумент -- это имя тега. Четвертый -- список атрибутов. В данном примере мы будем игнорировать первый и второй аргументы. Они предназначены для работы с XML-файлами, которые используют механизм пространств имен.
Если это тег <entry>, создается новый элемент списка QListView. Если анализируемый тег вложен в другой тег <entry>, создается вложенный подэлемент списка -- QListViewItem. В противном случае создается элемент списка верхнего уровня. Функция setOpen(true) вызывается для того, чтобы открыть вложенные подэлементы данного элемента. Функция setText() записывает текст (значение атрибута term), который будет отображаться на экране в первой колонке списка.
Если это тег <page>, то в currentText записывается пустая строка. Переменная currentText служит своего рода аккумулятором для текста, размещаемого между тегами <page> и </page>.
В заключение, в вызывающую программу возвращается true, чтобы сообщить парсеру SAX о том, что он может продолжить разбор файла. В случае неопознанного тега, можно вернуть false, чтобы известить парсер об ошибке. В этом случае необходимо тогда перекрыть метод errorString(), унаследованный от QXmlDefaultHandler, чтобы вернуть соответствующее сообщение об ошибке.
bool SaxHandler::characters(const QString &str) { currentText += str; return true; }Функция characters() вызывается для передачи символьных данных из XML-файла. В нашем случае мы просто добавляем их в конец переменной currentText.
bool SaxHandler::endElement(const QString &, const QString &, const QString &qName) { if (qName == "entry") { currentItem = currentItem->parent(); } else if (qName == "page") { if (currentItem) { QString allPages = currentItem->text(1); if (!allPages.isEmpty()) allPages += ", "; allPages += currentText; currentItem->setText(1, allPages); } } return true; }Функция endElement() вызывается, когда парсер встречает закрывающий тег. Аналогично функции startElement(), третьим аргументом ей передается имя тега.
Если это тег </entry>, то текущим назначается элемент более высокого уровня. Таким образом восстанавливается значение переменной, которое предшествовало открывающему тегу <entry>.
Если это тег </page>, производится добавление номеров в список страниц, которые отображаются во второй колонке списка.
bool SaxHandler::fatalError(const QXmlParseException &exception) { qWarning("Line %d, column %d: %s", exception.lineNumber(), exception.columnNumber(), exception.message().ascii()); return false; }Функция fatalError() вызывается, когда парсер не может продолжить разбор XML-файла. Тогда мы просто выводим сообщение, с указанием номера строки и позиции в строке, где была обнаружена ошибка.
На этом мы завершаем обзор реализации класса SaxHandler и переходим к демонстрации практического его применения:
bool parseFile(const QString &fileName) { QListView *listView = new QListView(0); listView->setCaption(QObject::tr("SAX Handler")); listView->setRootIsDecorated(true); listView->setResizeMode(QListView::AllColumns); listView->addColumn(QObject::tr("Terms")); listView->addColumn(QObject::tr("Pages")); listView->show(); QFile file(fileName); QXmlSimpleReader reader; SaxHandler handler(listView); reader.setContentHandler(&handler); reader.setErrorHandler(&handler); return reader.parse(&file); }Сначала создается виджет QListView с двумя колонками. Затем создаются объект QFile, посредством которого будет выполняться работа с файлом XML-документа, и QXmlSimpleReader -- сам парсер. У нас нет необходимости открывать файл -- за нас это сделает сама библиотека Qt.
В заключение создается объект SaxHandler. Мы передаем его парсеру, как обработчик событий разбора и как обработчик ошибок. И наконец запускаем процесс разбора, вызовом parse().
DOM -- это стандарт API, для разбора XML-документов, разработанный в недрах World Wide Web Consortium (W3C). Qt предоставляет реализацию DOM Level 2 для чтения, изменения и записи XML-документов.
DOM представляет XML-файл в памяти, в виде древовидной структуры. У приложения имеется возможность перемещаться по этой структуре, как ему заблагорассудится. Программа может изменить содержимое дерева и сохранить его обратно в файл.
Рассмотрим следующий XML-документ:
<doc> <quote>Errare humanum est</quote> <translation>To err is human</translation> </doc>Соответствующее ему дерево DOM:
В Qt, имена классов узлов начинаются с префикса QDom. Таким образом, класс QDomElement представляет узел Element, а QDomText -- узел Text.
Различные типы узлов могут включать в себя различные типы дочерних узлов. Например, узел Element может содержать другие узлы типа Element, а так же Entity Reference, Text, CDATA Section, Processing Instruction и Comment. На рисунке 14.3 показано, какие типы узлов, в состав каких типов могут входить.
Рисунок 14.3. Взаимоотношения между типами узлов в DOM.
class DomParser { public: DomParser(QIODevice *device, QListView *view); private: void parseEntry(const QDomElement &element, QListViewItem *parent); QListView *listView; };Наш класс DomParser будет производить анализ XML-файла и выводить его содержимое в QListView. Этот класс не имеет предка.
DomParser::DomParser(QIODevice *device, QListView *view) { listView = view; QString errorStr; int errorLine; int errorColumn; QDomDocument doc; if (!doc.setContent(device, true, &errorStr, &errorLine, &errorColumn)) { qWarning("Line %d, column %d: %s", errorLine, errorColumn, errorStr.ascii()); return; } QDomElement root = doc.documentElement(); if (root.tagName() != "bookindex") { qWarning("The file is not a bookindex file"); return; } QDomNode node = root.firstChild(); while (!node.isNull()) { if (node.toElement().tagName() == "entry") parseEntry(node.toElement(), 0); node = node.nextSibling(); } }В конструкторе создается объект QDomDocument и вызывается его метод setContent(), чтобы прочитать XML-документ из QIODevice. Она автоматически открывает устройство. Затем вызывается documentElement(), чтобы получить корневой узел (со всеми дочерними узлами), и проверяется -- является ли этот элемент тегом <bookindex>. После этого выполняются итерации по всем дочерним узлам и если встречен тег <entry>, вызывается parseEntry() для его анализа.
Класс QDomNode может хранить узлы любого типа. Если вы собираетесь обрабатывать узлы какого-то конкретного типа, нужно сначала выполнить соответствующее преобразование. В этом примере нас интересуют только узлы типа Element, поэтому мы выполняем преобразование вызовом метода toElement(), чтобы получить узел типа QDomElement, и затем вызываем tagName(), чтобы прочитать имя тега. Если узел относится к другому типу, то функция toElement() вернет пустой объект QDomElement, с пустым именем тега.
void DomParser::parseEntry(const QDomElement &element, QListViewItem *parent) { QListViewItem *item; if (parent) { item = new QListViewItem(parent); } else { item = new QListViewItem(listView); } item->setOpen(true); item->setText(0, element.attribute("term")); QDomNode node = element.firstChild(); while (!node.isNull()) { if (node.toElement().tagName() == "entry") { parseEntry(node.toElement(), item); } else if (node.toElement().tagName() == "page") { QDomNode childNode = node.firstChild(); while (!childNode.isNull()) { if (childNode.nodeType() == QDomNode::TextNode) { QString page = childNode.toText().data(); QString allPages = item->text(1); if (!allPages.isEmpty()) allPages += ", "; allPages += page; item->setText(1, allPages); break; } childNode = childNode.nextSibling(); } } node = node.nextSibling(); } }В функции parseEntry() создается элемент списка QListView. Если тег вложен в другой тег <entry>, то создается вложенный элемент списка. В противном случае создается элемент списка верхнего уровня. Чтобы открыть элемент списка, вызывается setOpen(true) и затем в него записывается текст, отображаемый в первой колонке списка, вызовом функции setText() (содержимое атрибута term).
После инициализации QListViewItem, выполняются итерации по всем вложенным узлам, соответствующим данному тегу <entry>.
Если встречен тег <entry>, вызывается функция parseEntry(), которой, в качестве второго аргумента передается, текущий элемент списка. В результате будет создан новый элемент списка, вложенный в текущий.
Если встречен тег <page>, выполняется поиск узла Text. После того как он будет найден, выполняется преобразование узла, функцией toText(), в QDomText и из него извлекается текст в виде QString. Полученный таким образом текст добавляется в список номеров страниц, который отображается во второй колонке QListViewItem.
Теперь покажем, как можно использовать полученный класс DomParser:
void parseFile(const QString &fileName) { QListView *listView = new QListView(0); listView->setCaption(QObject::tr("DOM Parser")); listView->setRootIsDecorated(true); listView->setResizeMode(QListView::AllColumns); listView->addColumn(QObject::tr("Terms")); listView->addColumn(QObject::tr("Pages")); listView->show(); QFile file(fileName); DomParser(&file, listView); }Сначала создается и настраивается QListView. Затем создаются QFile и DomParser. Во время создания, DomParser выполняет разбор XML-документа и заполняет список.
Как показывает пример, навигация по DOM-дереву может оказаться весьма громоздкой. Простое извлечение текста, заключенного между тегами <page> и </page> потребовало от нас выполнения итераций по всему списку дочерних узлов, с помощью функций firstChild() и nextSibling(). Программисты, которые часто сталкиваются с необходимостью выполнения синтаксического анализа XML-документов, нередко пишут свои высокоуровневые классы-обертки, упрощающие выполнение наиболее часто используемых операций, таких как извлечение текста, заключенного между тегами.
Существует два основных подхода создания XML-файлов в приложениях Qt:
Можно построить дерево DOM и затем вызвать метод save().
Можно вручную создать XML-файл
const int Indent = 4; QDomDocument doc; QDomElement root = doc.createElement("doc"); QDomElement quote = doc.createElement("quote"); QDomElement translation = doc.createElement("translation"); QDomText quoteText = doc.createTextNode("Errare humanum est"); QDomText translationText = doc.createTextNode("To err is human"); doc.appendChild(root); root.appendChild(quote); root.appendChild(translation); quote.appendChild(quoteText); translation.appendChild(translationText); QTextStream out(&file); doc.save(out, Indent);Вторым аргументом функции save() передается размер отступов. Ненулевое значение обеспечивает более удобочитаемый вид файла:
<doc> <quote>Errare humanum est</quote> <translation>To err is human</translation> </doc>Другой вариант применим в приложениях, которые используют структуру DOM-дерева для внутренней организации данных. Такие приложения, как правило, читают XML-документы с помощью DOM-парсера в память, затем изменяют содержимое дерева и сохраняют изменения с помощью save() в XML-файл.
В примерах выше использовалась кодировка UTF-8, однако существует возможность сохранения данных в других кодировках. Для этого достаточно добавить в начало XML-файла соответствующую кодировку:
<?xml version="1.0" encoding="ISO-8859-1"?>Следующий отрывок кода показывает, как это можно сделать:
QTextStream out(&file); QDomNode xmlNode = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"ISO-8859-1\""); doc.insertBefore(xmlNode, doc.firstChild()); doc.save(out, Indent);Создание XML-файлов вручную выполняется ничуть не сложнее. Для этого можно воспользоваться классом QTextStream и записать в него строки, как в обычный текстовый файл. Самое сложное в этом случае -- выполнить правильное экранирование служебных символов и значений атрибутов. Сделать это можно с помощью отдельной функции:
QString escapeXml(const QString &str) { QString xml = str; xml.replace("&", "&"); xml.replace("<", "<"); xml.replace(">", ">"); xml.replace(" ", "'"); xml.replace("\"", """); return xml; }Ниже приводится пример использования этой функции:
QTextStream out(&file); out.setEncoding(QTextStream::UnicodeUTF8); out << "<doc>\n" << " <quote>" << escapeXml(quoteText) << "</quote>\n" << " <translation>" << escapeXml(translationText) << "</translation>\n" << "</doc>\n";В ежеквартальнике Qt Quarterly вы найдете статью "Generating XML" (по адресу: http://doc.trolltech.com/qq/qq05-generating-xml.html, в которой приводится пример простого класса, создающего XML-файлы. Класс сам обслуживает экранирование служебных символов, вставляет отступы и задает кодировку символов, предоставляя нам возможность сконцентрироваться на содержимом XML-файла.
|