О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 14. XML" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Глава 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().


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

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:

Дерево состоит из узлов различного типа. Например, узел Element соответствует открывающему и парному закрывающему тегам. Все, что находится между ними, отображается в виде дочерних узлов.

В Qt, имена классов узлов начинаются с префикса QDom. Таким образом, класс QDomElement представляет узел Element, а QDomText -- узел Text.

Различные типы узлов могут включать в себя различные типы дочерних узлов. Например, узел Element может содержать другие узлы типа Element, а так же Entity Reference, Text, CDATA Section, Processing Instruction и Comment. На рисунке 14.3 показано, какие типы узлов, в состав каких типов могут входить.

Рисунок 14.3. Взаимоотношения между типами узлов в DOM.


Для демонстрации работы с XML-документами через API DOM, мы напишем парсер, который будет анализировать XML-файл, представленный выше.
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-документов, нередко пишут свои высокоуровневые классы-обертки, упрощающие выполнение наиболее часто используемых операций, таких как извлечение текста, заключенного между тегами.


14.3. Запись в XML-документы.

Существует два основных подхода создания XML-файлов в приложениях Qt:

Выбор того или иного метода зачастую сильно зависит от используемого парсера -- SAX или DOM. Ниже приводится отрывок кода, который создает дерево DOM и записывает его в файл, с помощью QTextStream:
  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("&", "&amp;"); 
  xml.replace("<", "&lt;"); 
  xml.replace(">", "&gt;"); 
  xml.replace(" ", "&apos;"); 
  xml.replace("\"", "&quot;"); 
  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-файла.

Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 14. XML" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz