Monday, June 30, 2008

XML::Twig

Приветствую.

Решил заделиться имеющимися знаниями и навыками работы с перловым модулем для работы с XML документами под названием XML::Twig. В этом документе я постараюсь осветить самые основные моменты использования данного модуля. Заранее хочу сказать что я не претендую на "последнюю инстанцию" и для полной точности нужно обращаться к официальной документации

Начнемс. Для примера возьмем xml файл следующего содержания:


<document pubdate="2008-06-06">
<chapter>
<section userlevel="1"><title>Атрибуты подпрограммы:</title>
<list>
<item>locked</item>
<item>method</item>
<item>lvalue</item>
</list>
</section>
<section userlevel="2">
<para>Разыменовывающие префиксы:</para>
<list type="bullet">
<item><bold>Скаляр</bold> $ </item>
<item><bold>Массив</bold> @ </item>
<item><bold>Хеш</bold> % </item>
</list>
</section>
<para>"Выполнять линейный просмотр в ассоциативном массиве —
все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</chapter>
</document>

назовем его example.xml.

Хочу сразу напомнить что в начало скрипта нужно поместить директиву: use XML::Twig, дабы подключить сам модуль. Создадим обьект XML::Twig и откроем вышеуказаный файл:

my $twig = XML::Twig->new();

$twig->parsefile("example.xml");

возьмем корневой элемент:

my $root = $twig->root;

для радующего глаз вывода воспользуемся методом set_pretty_print со стилем indented:

$root->set_pretty_print('indented');

в данном случае элемент $root сейчас содержит весь контент содержимого файла example.xml. Чтобы посмотреть содержимое переменной $root и вообще любого обьекта XML::Twig нужно воспользоваться методом print(), т. е. $root->print. Вызывать print можно только после парсинга, т е в нашем случае после $twig->parsefile("example.xml"), ибо как не трудно догадаться выводить ему будет просто нечего.

Замена тега

Чтобы изменить тег <chapter> на <part> нужно:

my $chapter = $root->first_child('chapter');

$chapter->set_tag('part');

Метод first_child возвращает первого "ребенка" элемента $root с названием part. После используем метод set_tag чтобы задать тегу part новое название — chapter. После этих манипуляций документ примет вид:


<document pubdate="2008-06-06">
<part>
....................................................
</part>
</document>

Обработка потомков

К примеру есть задача найти все теги <section> в теге <part> и удалить их, оставив при этом их содержание. Воспользуемся методом children:

foreach my $section ( $part->children('section') ) {

$section->erase;

}

метод children возвращает список детей элемнта, в данном случае детей под названием section. Если в скобках ничего не указывать, т е $part->children(), то метод вернет список всех детей, т е (section, section, para). Метод erase более подробно описывается ниже.

после этого документ примет вид:


<document pubdate="2008-06-06">
<chapter>
<title>Атрибуты подпрограммы:</title>
<list>
<item>locked</item>
<item>method</item>
<item>lvalue</item>
</list>
<para>Разыменовывающие префиксы:</para>
<list type="bullet">
<item><bold>Скаляр</bold> $ </item>
<item><bold>Массив</bold> @ </item>
<item><bold>Хеш</bold> % </item>
</list>
<para>"Выполнять линейный просмотр в ассоциативном массиве — все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</chapter>
</document>

Работа с атрибутами

Удаление атрибутов

Предположим что нужно удалить атрибуты у root'ового документа, для этого воспользуемся методом del_atts, т е $root->del_atts, после чего тег <document pubdate="2008-06-06"> станет просто <document>.

Добавление атрибутов

Добавим к тегу атрибут attention с значением 1:

my $para = $chapter->first_child('para');

$para->set_att('attention' => '1');

Результат:


<document pubdate="2008-06-06">
<chapter>
................................
<para attention="1">"Выполнять линейный просмотр в ассоциативном массиве — все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</chapter>
</document>

Извлечение содержимого тега

Для извлечения содержимого тега <title> нужно воспользоваться методом text:

my $title = $section->first_child('title');

$title_content = $title->text;

print $title_content,"\n";

Результатом будет строка: "Атрибуты подпрограммы:"

Метод text возвращает содержимое тега исключая любые другие теги. Т е если к примеру document_title будет:


<document_title>
<title1>Title 1</title1>
<title2>Title 2</title2>
</document_title>

и выполнить $document_title->text вывод будет: Title 1Title 2

Удаление тега

Для удаления тегов есть два метода, erase и delete. Erase удаляет сам тег, оставляя все содержимое. Delete также удаляет сам тег, но в отличие от erase также удаляет все его содержимое. К примеру:

my $section = $para->first_child('section');

$section->erase;

Результат будет:


<document pubdate="2008-06-06">
<document_title>Some title</document_title>
<part>
<title>Атрибуты подпрограммы:</title>
<list>
<item>locked</item>
<item>method</item>
<item>lvalue</item>
</list>
<section userlevel="2">
..............................................................

Если использовать delete, т е:

$section->delete;

Результат будет:


<document pubdate="2008-06-06">
<document_title>Some title</document_title>
<part>
<section userlevel="2">
<para>Разыменовывающие префиксы:</para>
<list type="bullet">
<item><bold>Скаляр</bold> $ </item>
<item><bold>Массив</bold> @ </item>
<item><bold>Хеш</bold> % </item>
</list>
</section>
<para>"Выполнять линейный просмотр в ассоциативном массиве — все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</part>
</document>

Как видно от первой секции ничего не осталось.

Сохранение в файл

Для сохранения в файл в XML::Twig есть метод с незамысловатым названием print_to_file. Пример использования:

my $twig = XML::Twig->new();

$twig->parsefile("example.xml");


my $root = $twig->root;

$root->set_pretty_print('indented');


my $part = $root->first_child('part');

my $section = $part->first_child('section');

$section->delete;


$twig->print_to_file("result.xml");


Измененный контент example.xml будет сохранен в файл result.xml.

Метод get_xpath

К примеру есть необходимость заменить все теги <item> на <listitem>. Для этой цели можно использоваться методом get_xpath. Сей замечательный метод возвращает список всех тегов которые удовлетворяют значению в скобках, '//' используется для того чтобы получить всех потомков.

my @item_collector = $root->get_xpath("//item");

foreach (@item_collector) {

        $_->set_tag('listitem');

}

После последней манипуляции документ будет выглядить так:


<document pubdate="2008-06-06">
<document_title>Some title</document_title>
<part>
<section userlevel="1">
<title>Атрибуты подпрограммы:</title>
<list>
<listitem>locked</listitem>
<listitem>method</listitem>
<listitem>lvalue</listitem>
</list>
</section>
<section userlevel="2">
<para>Разыменовывающие префиксы:</para>
<list type="bullet">
<listitem><bold>Скаляр</bold> $ </listitem>
<listitem><bold>Массив</bold> @ </listitem>
<listitem><bold>Хеш</bold> % </listitem>
</list>
</section>
<para>"Выполнять линейный просмотр в ассоциативном массиве —
все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</part>
</document>

Все теги <item> содержащиеся в элементе $root были изменены на <listitem>.

Дополнительные примеры:

1) Нужно поменять все теги list в зависимости от атрибута, если list без атрибутов, то изменить его на orderlist, а если с атрибутом type и последний равен bullet, то изменить его на bulletlist при этом удалив атрибуты. Воспользуемся вышеупомятым методом get_xpath:

my @list_collector = ($root->get_xpath("//list"));

foreach my $list (@list_collector) {

if ($list->has_no_atts) {

$list->set_tag('orderlist');

}

if ($list->has_atts && $list->att('type') eq 'bullet') {

$list->set_tag('bulletlist');

$list->del_atts;

}

}

Результат:


<document pubdate="2008-06-06">
<chapter>
<section userlevel="1">
<title>Атрибуты подпрограммы:</title>
<orderlist>
<item>locked</item>
<item>method</item>
<item>lvalue</item>
</orderlist>
</section>
<section userlevel="2">
<para>Разыменовывающие префиксы:</para>
<bulletlist>
<item><bold>Скаляр</bold> $ </item>
<item><bold>Массив</bold> @ </item>
<item><bold>Хеш</bold> % </item>
</bulletlist>
</section>
<para>"Выполнять линейный просмотр в ассоциативном массиве —
все равно что пытаться забить кого-нибудь до смерти заряженным Узи" Ларри Уолл</para>
</chapter>
</document>

2)Скопировать все теги section с содержимым в отдельный файл, предварительно заключив в тег document. В этом примере воспользуемся модулем FileHandle, как результат в начале нужно прописать use FileHandle. Если сей модуль не установлен, в Linux' е его можно поставить командой: cpan install FileHandle

my @section_collector = $root->get_xpath("//section");

my $counter = 0;

foreach my $section (@section_collector) {

#Создаем новый пустой тег document

my $document = XML::Twig::Elt->new( document => '' );

$document->set_att( 'docref', "document$counter" );

#вставляем элемент $section с содержимым в тег document

$section->move( first_child => $document );

#сохраняем элемент document в файл

my $fh = FileHandle->new();

$fh->open(">result$counter.xml");

$document->print($fh,"indented");

$fh->close;

$counter++;

}

в результате в текущем каталоге появятся два файла: result0.xml и result1.xml

PS: Если вы нашли ошибки/неточности, сообщите пожалуйста. Обоснованная критика принимается :)


13 comments:

santific said...

Спасибо за описание, очень толковое! Есть пару вопросов:
1. Много ли памяти кушает этот парсер при разборе хмл документа?
2. Какие обработчики ошибок с ним лучше использовать, как получить ошибки?

Stanley Orlenko said...

Та не за что, я старался :)
1)Увы не могу сказать, ибо работать приходилось только с XML::Twig'ом
2)Да как-то не думал об этом... он и сам по себе выдает достаточно внятные сообщения об ошибках, во всяком случае у меня никаких проблем с дебагом не возникало.

Anonymous said...

Жаль, что не уделено внимание созданию файлов или частей XML.
"Чтобы что-то распарсить, надо что-то запаковать" :)

0andriy said...

Создание XML.

Создаём новый Twig:
my $twig = XML::Twig->new(
pretty_print => 'indented',
empty_tags => 'expand',
);
указав красивый вывод и завершение тегов в стиле HTML (т.е. всегда использовать закрываюший тег).

Создаём корневой элемент:
my $root = XML::Twig::Elt->new(
'entry' => {
'xmlns' => 'http://www.w3.org/2005/Atom',
'xmlns:media' => 'http://search.yahoo.com/mrss/',
'xmlns:gphoto' => 'http://schemas.google.com/photos/2007',
}
);

Связываем созданный элемент с корнем нашего Twig:
$twig->set_root($root);

Создаём другой элемент
my $title = XML::Twig::Elt->new('title' => {'type' => 'text'}, 'Title');
Добавляем его в качестве потомка корневого:
$title->paste(last_child => $root);

Печатаем на экран, что мы получили:
$twig->print();

Освобождаем память:
$twig->purge();

На выходе получим нечто типа
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gphoto="http://schemas.google.com/photos/2007" xmlns:media="http://search.yahoo.com/mrss/">
<title type="text">Title</title>
</entry>

Stanley Orlenko said...

@andy_shev
Прошу прощения что сразу не отреагировал на ваш комментарий. Есть несколько дополнений:
1) новый элемент можно создать от любого объекта твига методом new, к примеру
my $document = $title->new('document'=>{'docref'=>"document"},'example')

результатом будет тег
<document docref="document">example</document>

2) также есть метод insert_new_elt, приведу пример использования:
$document->insert_new_elt('first_child','text'=>{'align'=>'center','color'=>'grey'},'Here is the text');

результатом будет
<document><text align="center" color="grey">Here is the text</text></document>

Как только появится время - расширю статью в направлении геренации XML

0andriy said...

@Stas

Спасибо за дополнения.
Однако, не от любого объекта получается, как Вы указали, а от объекта типа XML::Twig::Elt либо я что-то не так делаю.

Stanley Orlenko said...

Вот только что написал маленький пример, XML::Twig::Elt я там не испльзовал:

#!/usr/bin/perl
use warnings;
use strict;
use XML::Twig;

my $t = XML::Twig->new();
$t->set_pretty_print('indented');
$t->parse('<document><first_element/></document>');

my $root = $t->root;

my $first_element = $root->first_child('first_element');
$first_element->insert_new_elt('after',
'second_element','Second Element');

$root->print;

результат будет:

<document>
<first_element/>
<second_element>Second Element</second_element>
</document>

и еще один:

#!/usr/bin/perl
use warnings;
use strict;
use XML::Twig;

my $t = XML::Twig->new();
$t->set_pretty_print('indented');
$t->parse('<document><first_element/>
</document>');

my $root = $t->root;

my $first_element = $root->first_child('first_element');

my $second_element = $first_element->new('second_element','Second Element text');
$second_element->move('last_child',$root);

$root->print;

результат будет:

<document>
<first_element/>
<second_element>Second Element text</second_element>
</document>

XML::Twig::Elt я нигде не использовал

0andriy said...

Мне кажется эксперимент не чистый.
Я говорю о создании документа, а не о расширении существующего.

Stanley Orlenko said...

@ andy_shev
Вы писали:
Однако, не от любого объекта получается, как Вы указали, а от объекта типа XML::Twig::Elt либо я что-то не так делаю.

Я подразумевал объекты либо XML::Twig либо XML::Twig::Elt

cheap erectile dysfunction pills online said...

Hey There. I found your blog using msn. This is an extremely well written article. I'll make sure to bookmark it and come back to read more of your useful info. Thanks for the post. I will definitely comeback.

Vitaly Sennikov said...
This comment has been removed by the author.
Vitaly Sennikov said...

Каким образом произвести перебор всех тегов? Например, есть объект типа XML::Twig::Elt
<row>
<t1>v1</t1>
<t2>v2</t2>
...
</row>

как получить хэш вида, если заранее не знаем какие тэги t1, t2,...?
%h = ( t1 => v1, t2 => v2, ...)

Vitaly Sennikov said...

Нашел решение. Может тоже кто-то ищет.

my $t = XML::Twig->new();
$t->parse($content);

my @rows;
foreach my $tag_row ($t->root->get_xpath('//row')){
my %row;
foreach ($tag_row->children){
$row{$_->name} = $_->text;
}
push @rows, \%row;
}