Публикации FreePascal

Работа с XML-файлами

21.03.2008
Вадим Исаев

1. Чтение XML-файлов с помощью Document Object Model (DOM).



Авторское право Thomas Zastrow.
Перевод на русский язык Вадим Исаев.

Загрузить код примера


Document Object Model (DOM) - официальная спецификация W3C. Она описывает как разобрать XML-структуру, элементы адреса, аттрибуты и содержание. По сравнению с SAX-подходом DOM более удобна для пользователя, но нуждается в хорошем процессоре и бОльшем количестве оперативной памяти.

FPC поставляется с модулем dom.pp (fcl-xml/dom.pp). Эта статья-справка помогает разбирать XML-файл, вместе с несколькими строчками кода.

Создайте новое приложение в Lazarus и поместите на форму некоторые компоненты:

На форме есть три TEdit-поля, один TMemo, две кнопки и OpenFile-диалог.

В первом TEdit поле (названом XMLFile) помещается имя XML-файла, который должен быть разобран. Кнопка около него для того, чтобы открыть XML-файл через окно диалога выбора файла. OnClick-процедура кнопки имеет код:

procedure TForm1.Button1Click(Sender: TObject);
begin
  if opendialog1.execute then
    XMLFile.text := opendialog1.filename;
end;

Во втором TEdit-поле вводится название XML-элемента, который ищется в XML-файле, Третье TEdit-поле хранит название аттрибута, который нужно найти. Эти компоненты названы element2find и attribut2find.

TMemo (названный output) - чтобы хранить вывод анализатора. И последнее - кнопка с заголовком "Начать", которая запустит парсинг.

В строке Uses Вашей программы должны быть указаны модули XMLRead и dom.

OnClick-процедура кнопки "Начать":

procedure TForm1.Button2Click(Sender: TObject);
var doc:txmldocument;
    temp:TDomNode;
begin
  output.lines.clear;
  ReadXMLFile(doc, XMLFile.text);
  temp := doc.DocumentElement;
  ParseXML(temp);
end;

ReadXMLFile открывает XML-документ и присваивает его переменной doc. Затем корневой элемент присваивается переменной temp. Процедура ParseXMLM сделает должным образом парсинг.


Отношения узлов в XML-структуре

Итак, ParseXML начинается с корневого элемента XML-файла (Root). Этот элемент будет проанализирован и, если будут какие-нибудь дочерние узлы, то процедура вызовет себя снова, но теперь с параметром - дочерним элементом.

Шаг за шагом - процедура ParseXML:

  E := knoten as TDomElement;
  N := E.FirstChild;

Текущий XML-элемент - давайте называть его контекстный узел - назначен на E как TDOMElement. N берет первый дочерний элемент (Children) контекстного узла.

Посмотрим на цикл-петлю:

  While (N <> NIL) Do
    ...
  N := N.NextSibling; 

Цикл пробегает всех узлов-братьев (соседей :) ) контекстного узла. В конце цикла следующий узел-брат (сосед) контекстного узла становился контекстным узлом.

Снова в начале петли:


  addToOutput(N);

Процедуру addToOutput вызывают с контекстным узлом как параметром. Эта процедура используется, чтобы построить строку, которая добавляется в конец TMemo-компонента, названному "output".
Сначала, строка s получает название узла, который в параметре, чтобы вывести его:

  s := 'Name:' + N.NodeName;

Помните, что на нашей форме есть TEdit-компонент по имени element2find. Если название текущего узла - то же самое что и свойство Text этого компонента, значит мы нашли элемент, который искали:

  if N.NodeName = form1.element2find.text then
  begin
    s := s + ' Found element!';
  end;

Поиск Аттрибута (текстовое свойство TEdit-компонента attribut2find) немного более сложен:

if N.NodeName <> '#text' then
begin
  E := N as TDOMElement;
  i := E.attributes.count;
  s := s + ' Count Attribute ' + inttostr(i);
  if E[form1.attribut2find.text]<>'' then
    s := s + ' Found Attribut';
end; {nodename <> text}

Если содержание узла только текст, и нет больше узлов-братьев или дочерних узлов, то его название равно #text. И текст не может иметь никаких признаков. Затем, TDOMElement E назначен на этот узел. Теперь признаки могут являться признаками E.

Количество признаков присоединяется к строке s. Если условие if-запроса истинно, то элемент имеет признак, название которого тождественно свойству attribut2find.Text.

Последняя часть процедуры ParseXML проверяет, имеет ли контекстный узел дочерние узлы:

if N.HasChildNodes then
begin
  I := N.childnodes.count;
  sub := N.FirstChild;
  addToOutput(sub);

Если дело обстоит именно так, то первый дочерний узел контекстного узла назначается переменной sub. Снова вызывается процедура addToOutput, но теперь с параметром sub.

Следует начало рекурсии - если sub снова имеет какой-нибудь дочерний узел, процедура ParseXML вызывается снова с sub как параметр:

if sub.haschildnodes then
begin
  ParseXML(sub);
end;

В этом пункте помните, что sub был первым дочерним узлом текущего контекстного узла. Его узлы-братья присвоены переменной sub - таким образом они обрабатываются for-петлей:

for zaehler := 1 to i-1 do
begin
  sub := sub.NextSibling;
  addToOutput(sub);
  form1.refresh;
  if sub.HasChildNodes then
  begin
    ParseXML(sub);
  end;
end; {for zaehler}

Парсинг файла примера test.xml производит следующий вывод, с поиском элемента, названным two и аттрибутом a:

Name: one Count Attribute 0
Name: two Found element! Count Attribute 0
Name: #text
Name: three Count Attribute 2 Found Attribut
Name: child_of_three Count Attribute 0
Name: #text

Вот и все.


2. Создание XML-структур с помощью Document Object Model (DOM).


Авторское право Thomas Zastrow.
Перевел на русский язык Вадим Исаев

Загрузить код примера


Эта обучающая программа демонстрирует создание XML-структур.
Создайте новое Lazarus-приложение с TMemo-компонентом и кнопкой на форме.

Модули dom и XMLWrite должны быть включены в строку Uses Вашей программы.

Щелчок по кнопке создаст простые XML-структуры, сохранит их в файл и отобраит XML-код в TMemo.

Давайте взглянем на процедуру OnClick кнопки:

После создания документа (новый XML-документ), N и sub (два TDOMNodes), будет создан корневой элемент (названный "root") XML-файла, который, в свою очередь будет назначен документу:

  N := doc.CreateElement('root');
  doc.Appendchild(N);

Далее элемент N назначается корневым элементом. sub будет элементом, подчиненным элементу N ("RootsFirstChild"). Добавляется аттрибут к sub с помощью E приведенного к типу TDOMElement (AttributeName = "SomeValue"). Теперь sub можно добавить к N:

  N := doc.DocumentElement;
  sub := doc.CreateElement('RootsFirstChild');
  E := sub as TDOMElement;
  E['AttributeName']:='SomeValue';
  N.AppendChild(sub);

Другой подчиненный элемент ("RootsSecondChild") добавлен к корневому:

  sub := doc.CreateElement('RootsSecondChild');
  N.AppendChild(sub);

Теперь к последнему элементу можно добавить какой-нибудь текст. Текст, как содержание XML-узла, обрабатывается как дочерний узел: Элементу N присваивается sub, а sub теперь будет текстовым содержанием (CreateTextNode('Text-content')). После этого sub добавляется к N:

  N := sub;
  sub := doc.CreateTextNode('Text-content');
  N.AppendChild(sub);

Окончание процедуры.
XML-структура записывается в файл ("xml-file.xml") и файл загружается в компонент TMemo:

  writeXMLFile(doc, 'xml-file.xml');
  memo1.lines.clear;
  memo1.lines.loadfromfile('xml-file.xml');
  doc.free;

Полученый XML-файл:



  
  Text-content



3. Использование XML для файлов конфигурации


Авторское право Thomas Zastrow.
Перевод на русский язык Вадим Исаев.

Загрузить код примера


Во FreePascal есть модуль xmlcfg.pp. Этот модуль специально разработан для того, чтобы читать и писать данные конфигурации в форме пары ключ-значение. Основной формат файла конфигурации - XML, но использующий xmlcfg.pp программист не должен иметь дело со сложностью синтаксического анализа XML-данных.

Следующее небольшое Lazarus-приложение покажет Вам, как использовать модуль xmlcfg.

Создайте новое Lazarus-приложение и поместите на форму компоненты:

Включите в строку Uses Вашего приложения модуль xmlcfg!

На форме лежат три TEdit-компоненты с названиями:

  • KeyName2write - для записи названия ключа.
  • Value2write - для записи значения ключа.
  • KeyName2read - название ключа для чтения.
И компонент TLable - для сообщений.

Компоненты в левой группе, названые "Записать ключзначение", создают новое значение в файле конфигурации. Правая группа ("Прочитать ключзначение") - читают пару ключзначение из файла конфигурации.

Теперь, остается только ввести что-нибудь как пару ключзначение в двух TEdit-полях слева и щелкнуть левой кнопкой "OK". Например:

  • Название ключа: MyName
  • Значение: Tom

В файле myconfig.xml будет что-то вроде этого:


Здесь файл содержит ключ "MyName" со значением "Tom".

При создании формы создается переменная cfg типа TXMLConfig, которая отвечает за открытие или создание файла конфигурации с именем myconfig.xml.
При закрытии формы файл закрывается и переменная освобождается.

Посмотрим на метод OnClick левой кнопки "OK" :

Procedure TForm1. Button1Click (Sender: TObject);
Var 
  s: string;
Begin
 cfg.SetValue(KeyName2write.text, Value2write.text);
 cfg.Flash;
 Messages.caption: = 'Ключзначение были сохранены!';
End;

Пара ключзначение (Keyname2write / Value2write) будет записана в файл, используя метод SetValue() объекта cfg. После этого происходит принудительный сброс буфера в файл с помощью метода Flash

Все очень просто, не так ли?

Столь же проста, как запись в файл конфигурации и чтение из него.

Щелчек по второй кнопке "OK" прочитает значение указанного в KeyName2read ключа из файла конфигурации "myconfig.xml". Значение будет написано в метке Messages. Процедура OnClick второй (правой) кнопки "OK":


Procedure TForm1.Button2Click (Sender: TObject);
Var 
  s: string;
Begin
  s := cfg.GetValue(Keyname2read.text, ");
  Messages.caption := 'Значение' + KeyName2read.text + ':' + s;
End;

В переменную s заносится значение найденого ключа с помощью метода GetValue(). Посде этого выводим значение в Messages.

Замечание 1

Вы можете также писать/читать целочисленные и логические значения.

Замечание 2

Файл конфигурации может иметь иерархическую структуру. Разделяя названия ключей и значения к ним символами "/" можно получить более детализированные упорядоченные категории значений.

Например, создавая следующие пары ключзначение:

author/tom := chief
author/tom/email := chef@thomas-zastrow.de

мы получаем такой XML-файл:



  
    

  


4. Практический пример от переводчика


Загрузить код примера


Поскольку примеры Томаса носят несколько академический характер, то было решено создать пример более практический, который выполняет некую полезную функцию. Пример будет получать курс валют (доллар, евро) с сайта Центробанка России и выводить полученые значения в табличку.

В строке Uses Вашей программы должны быть указаны модули XMLRead и dom.

На форме размещаются компоненты TPanel, TStringGrid, TButton. TPanel выравнивается по нижнему краю, а TStringGrid по оставшейся клиентской области.
В процедуру OnClick кнопки заносится процедура получения курса валют.
Для связи с интернетом был выбран компонент TIdHTTP из комплекта Indy для Lazarus.

Сначала составляется правильная ссылка, чтобы получить курс доллара на текущее число из набора констант и отформатированной даты:


  XMLURLD:= URL + FormatDateTime('dd''/''mm''/''yyyy', Now)+
           '&date_req2='+
           FormatDateTime('dd''/''mm''/''yyyy', Now)+
           DOLL;

Получаем строку с курсом доллара с сайта:

  s:=IdHTTP1.Get(XMLURLD);

Внимание!!А вот дальше я наситупил на первые грабли. :)
Оказывается FPC не работает с кодировками XML-файлов отличными от UTF. А Центробанк нам выдает строку с кодировкой в заголовке 1251. Поэтому перед парсингом упоминание о кодировке приходится удалять:

  i:=Pos(es, s);
  Delete(s, i, Length(es)+1);

Далее присваиваем строку компоненту TStringList, сохраняем текстовое содержимое в файл "dollar.xml" и загружаем этот файл в компоненту типа TXMLDocument для разбора:

  st.Text:=s;
  st.SaveToFile(GetCurrentDir+'dollar.xml');
  ReadXMLFile(doc, GetCurrentDir+'dollar.xml');

Теперь получаем корневой элемент, находим у него дочерний узел и в цикле ищем дочерний узел, имя у которого "Nominal", в котором хранится номинал валюты:

  dn:=doc.DocumentElement;
  n:=dn.FirstChild;
  While n.NodeName<>'Nominal' Do
    n:=n.FirstChild;

Потом займемся поисками брата-узла номинала по имени "Value" где и хранится значение курса валюты.
Внимание!! Здесь я наступил на вторые грабли. :) Причем из-за того, что ленился прочитать спецификацию XML. Хотел сразу же у найденых братьев получить значение узла, но оказывается все что содержится в XML-документе - это узлы, а значения находятся как бы внутри узла. Поэтому у найденых братьев следует взять дочерние узлы и только в них уже содержаться значения номинала и курса:

  sub:=n.NextSibling;
  stg1.Cells[1,1]:=n.FirstChild.NodeValue;
  stg1.Cells[2,1]:=sub.FirstChild.NodeValue;

Ну вот и все. Для получения курса Евро в ссылке код доллара меняется на код Евро.

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links