XML

Постановка проблемы

Очень часто программам нужно хранить данные в файлах. Рассмотрим наиболее простой из вариантов: запись и чтение простых текстовых данных (plain text) в файл. Перечислим проблемы, с которыми может столкнуться программист при решении этой, на первый взгляд, тривиальной задачи:

  1. Кодировка текста. Существуют многочисленные варианты как однобайтовых, так и многобайтовых кодировок.
  2. Вид конца строк. В различных ОС используется различные способы обозначения перевода строки. Например, в Unix-подобных ОС это символ "\n" (код 10), в Windows — "\r\n" (13, 10), в MacOS (в старых версиях) — "\r" (13).

Представим теперь более содержательный пример. Пусть мы пишем программу для обработки трехмерных изображений. В какой-то момент нам потребуется сохранять их в файлы, поэтому попробуем придумать формат для представления таких картинок. Например, он может быть таким:

4                  Целое число. Количество точек
1.0 2.0 3.0        Три вещественных числа — координаты точек
2.0 3.0 1.055
0.0 0.0 0.0
1.0 1.0 1.0
6                  Целое число. Количество линий
0 1                Номера двух точек в порядке перечисления
2 4
...

У такого формата проблем еще больше:

  1. Символы перевода строк.
  2. Формат вещественных чисел. Такие числа можно записывать многими способами: с плавающей точкой, с фиксированной точкой, в экспоненциальной нотации и т.п. Да и сама «точка» может оказаться «запятой» (например, 1.23 и 1,23) в зависимости от региональных настроек.
  3. Символы между числами (например, пробел, табуляция и т.д.)
  4. Возможные ошибки в формате записи.

Последняя проблема наиболее существенная, т.к. подобных ошибок может быть сколько угодно: неверное число точек, координат, посторонние символы и т.д. Как правило, разбор всех таких вариантов может занимать существенную часть кода программы. Строго говоря, любые форматы — это проблема.

Решение

Одним из стандартных решений является XML (eXtendable Markup Language). XML это способ описания языков. Известным примером XML является язык гипертекстовой разметки XHTML. Рассмотрим основные правила XML на примере описания языка для упомянутых 3D-картинок.

Правила XML

XML файлы начинаются со стандартной строки:

<?xml version="1.0" encoding="UTF-8"?>
Структура файла состоит из вложенных тегов. Теги имеют вид <имя_тега>содержимое</имя_тега>. На самом верхнем уровне должен быть только один уникальный тег. В нашем случае описать картинку можно следующим образом.

<?xml version="1.0" encoding="UTF-8"?> Стандартный заголовок
<ddd>                                  Тег самого верхнего уровня
   <points number="4">                 Перечисление точек
      <point>                          Описание точки
         <x>1.0</x>                    Последовательное задание всех трех координат
         <y>2.0</y>
         <z>3.0</z>
      </point>
      ...
   </points>
   <lines>                             Описание линий
   ...
   </lines>
</ddd>

Как показано в примере, теги также могут иметь атрибуты. Атрибуты перечисляются через пробел после имени тега (только открывающего). Если тег не имеет содержимого, то вместо парного закрывающего тега можно написать просто <tag/>. Например, координаты точки можно записать одним тегом, указав коэффициенты в атрибутах: <point x="1.0" y="2.0" z="3.0" />.

В общем случае любой открывающий тег должен иметь соответствующий закрывающий и не иметь пересечений с другими тегами (другими словами, все теги по отношении друг к другу должны быть либо внешними, либо внутренними). Например, с точки зрения XML, в отличие от HTML, такой код некорректный: <b><i></b></i>, т.к. тег "b" закрывается раньше вложенного тега "i".

В XML есть 5 служебных символов, которые должны быть заменены служебными последовательностями (entity characters) в содержимом тегов и атрибутов (< — &lt;, > — &gt;, " — &quot;, ' — &apos;, & — &amp;). Так, вместо <less>a < b</less> следует писать <less>a &lt; b</less>.

XML файл, удовлетворяющий перечисленным условиям, называется правильно построенным (англ. well-formed).

Замечание. Можно заметить, что использование XML приводит к избыточному размеру файла. Тем не менее, в наши дни эта проблема неактуальна. Во-первых, дисковое пространство стоит очень дешево, а во-вторых, производительность современных компьютеров без труда позволяет хранить файлы в zip-папках и читать их «на лету».

Готовые библиотеки

Чем же XML лучше? Это стандартный способ. Поэтому существует огромное количество готовых библиотек для работы с XML. Такие библиотеки способны прочесть любой валидный XML файл, а если он окажется невалидным, то укажут, в каком месте. Воспользовавшись одной из них, в нашем случае автоматически снимается ряд проблем.

  1. Кодировка текста указывается в начале XML файла, что снимает первую проблему.
  2. Используемые символы перевода строк теперь неважны, т.к. в хорошей библиотеке предусмотрена ситуация встретить любой из используемых символов перевода строки и корректно его обработать.
  3. Символов-разделителей между числами больше нет, вместо них используются теги XML.
  4. Возможные опечатки, некорректные последовательности и прочие случайные искажения с высокой вероятностью приведут к не-well-formed XML файлу и поэтому будут обнаружены парсером.
  5. Среди перечисленных проблем остались только неоднозначность в представлении вещественных чисел и логические ошибки в формате записи.

DTD и Schema

Программы, использующие XML в совокупности с готовым парсером (англ. parse — разбор), гораздо менее подвержены описанным неприятностям. Важнейшим следствием этого является то, что программы занимаются только логикой представления данных, абстрагируясь от проблем чтения их из файла. Это уже очень хорошо, но XML может дать гораздо больше. Стандарт описывает средства для документирования структуры XML файла. Такими средствами являются языки DTD и Schema. DTD (англ. Document Type Definition — определение типа документа) описывает схему документа для конкретного языка разметки посредством набора объявлений, которые описывают его тип с точки зрения синтаксических ограничений этого документа. Например, при помощи DTD можно описать, что внутри тега <points> должны содержаться теги с именами x, y и z. Язык Schema предоставляет гораздо более богатые возможности, чем DTD, и в нашем примере способен полностью автоматизировать решение оставшихся проблем (к примеру, Schema позволяет сказать, что внутри <points number="4"> должно содержаться ровно столько тегов x, y, и z, сколько указано в атрибуте number). XML, который соответствует семантическим правилам DTD или Schema, называется валидным (англ. valid). Cуществуют разнообразные утилиты для работы с XML, в частности, утилита xmllint для проверки файла на валидность.

DOM и SAX

Все XML парсеры по способу организации интерфейса к данным XML файла делятся на два основных класса: DOM и SAX. Рассмотрим их реализацию.

DOM (Document Object Model) — это способ представления содержимого XML в виде дерева. Такой подход наиболее естественно согласуется с объектно-ориентированной парадигмой. В несколько упрощенном виде схема может быть следующей: на самом верхнем уровне класс Element представляет собой узел дерева и имеет два наследника — TagElement (тег) и TextElement (содержимое тега).

struct Element
{
   Element *parent; // узел родителя
   list<Element *> children; // узлы детей
};
struct TagElement : Element
{
   string name; // имя тега
   map<string, string> attribs; // атрибуты тега
};
struct TextElement : Element
{
   string value; // содержимое тега
};

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

SAX (Simple API for XML) — это способ последовательного чтения XML файла. SAX-парсер читает содержимое файла и просто сообщает о встреченных элементах XML-разметки (или ошибках). Такая модель может быть осуществлена посредством функций обратного вызова (callback function) или полиморфизма. Преимуществом SAX является то, что его реализация требует О(1) памяти. Но у него есть и недостатки, например, нельзя возвращаться назад или сразу перейти вперед. Используя полиморфизм, схематически работу SAX-парсера можно изобразить так.

struct Handler
{
   virtual void startTag(string name, map<string, string> attribs) = 0;
   virtual void endTag(string name) = 0;
   virtual void chars(string text) = 0;
};
bool SAX_Parser(string xml, Handler *handler)
{
   File file = open(xml); // открыть файл
   char buffer[1024];
   int length;
   do {
      length = read(file, buffer, 1024);
      if(встретили тег) {
         handler->startTag(name, attribs);
      }
      // ...
   } while(length == 1024);
   close(file);
   return true;
}

Теперь пользователю остается лишь создать класс, реализующий интерфейс Handler, и передать его функции SAX_Parser.

struct MyHandler : Handler
{
   virtual void startTag(string name, map<string, string> attribs)
   {
      cout << name << endl; // просто выводим на экран имя тега
   }
   // ...
};
// ...
Handler *handler = new MyHandler();
SAX_Parser(fileName, handler);

Заключение

Подведем итоги. Существует два вида XML парсеров, DOM и SAX. DOM представляет собой дерево и удобен, если нужно выполнять быструю навигацию по документу. Использует объектную модель, так что применяется преимущественно в объектно-ориентированных языках (например, C++, Java и т.д.) SAX, в отличие от DOM, требует независящего от размера файла объема памяти, но не позволяет произвольно перемещаться по файлу. Может быть реализован при помощи функций обратного вызова, что позволяет его использовать в процедурных языках программирования (таких, как C).

XML является стандартизированным языком, для работы с которым существует много библиотек. Ими нужно пользоваться, потому что готовые библиотеки — это удобно. А умение пользоваться готовыми библиотеками, написанным другими людьми, это важная часть программирования.

Примерами могут служить библиотеки Xerces (написана на C++, предоставляет DOM и SAX, а также Schema, DTD и все, что еще можно придумать) и Expat (написана на C и предоставляет только SAX).