Глава 14. XML

XML (от англ. Extensible Markup Language -- Расширяемый Язык Разметки) -- популярный формат файлов, используемый для обмена и хранения данных в текстовом виде.

Для работы с XML документами, Qt поддерживает два различных API:

В каждом конкретном случае, при выборе того или иного API, необходимо учитывать множество факторов. SAX -- более быстрый, он больше подходит для выполнения простых задач (например, чтобы найти все вхождения заданного тега в документе), и для работы с XML-файлами огромного размера, которые могут не уместиться в памяти целиком. DOM -- более удобен, в большинстве приложений, фактор удобства перевешивает быстроту и нетребовательность SAX.

В этой главе мы покажем, как работать с XML-файлами посредством обоих API.

14.1. Чтение XML-документов с помощью SAX.

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-файл, содержимое которого отображается в QListView, на рисунке 14.2. <?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().