Perl: работаем с XML и JSON

Date September 13th, 2010 Author Vitaly Agapov

— Поверь мне, не в пирогах счастье…
— Ты, что, с ума сошёл? А в чём же ещё?

— «Малыш и Карлсон»

It's not even XML!

Не буду производить тонну выкладок в стиле “Что лучше: XML или JSON?”. Дело это неблагодарное, более девяти тысяч раз обмусоленное в статьях. Стоит только разок  обратиться в Гугл с вопросом “JSON vs XML” и окунуться в разнообразие мнений. Тем более, что, как это обычно бывает, однозначного ответа здесь нет и не будет, так как разные бывают ситуации и разные задачи. Так, например, в информационно-ориентированных web-приложениях для AJAX-обмена наиболее оптимальным выбором видится JSON , а вот для хранения больших объемов формализованных иерархических данных или для использования в качестве конфигов, наверное, всё же лучше брать XML. Так или иначе столкнуться придётся с каждым из этих форматов, и с каждым из них надо будет работать. И вот на тот случай, если работать придётся на Perl, пишу эту статейку-напоминалку.

Когда я использую Perl, всегда держу в голове золотое правило “Есть задача – стопудово есть и модуль на CPAN”.  Срабатывает безотказно. Так и здесь: для работы с JSON и XML существует множество  модулей, из которых я для себя выбрал соответственно модули JSON и XML::Twig. Модули большие, навороченные, умеют всё, о чём можно пожелать. Ну, разве что не “грабят корованы”. Но опишу только основное.

JSON

Модуль в основном предназначен для того, чтобы преобразовывать перловые структуры любой сложности и вложенности в текстовый формат JSON и обратно. Поэтому здесь основными методами являются encode и decode (и функции to_json, from_json, decode_json и encode_json).

Рассмотрим простенький пример использования модуля из статьи про календарь:

use JSON;
%json_hash;
$json_hash{month}=11;
$json_hash{year}=2011;
my @days=(4,8,15,16,23,42);
$json_hash{days}=\@days;
my $json = JSON->new->allow_nonref;
my $json_text=$json->encode( \%json_hash );

В итоге в скаляр $json_text получим результат:

{"month":"11","days":["4","8","15","16","23","42"],"year":"2011"}

В данном примере используется объектно-ориентированный интерфейс, но модуль позволяет пользоваться и традиционными функциями. Это to_json и from_json с поддержкой параметров:

$json_text   = to_json( $perl_scalar, { ascii => 1, pretty => 1 } );
$perl_scalar = from_json( $json_text, { utf8  => 1 } );

А также простая форма – encode_json и decode_json:

$utf8_encoded_json_text = encode_json $perl_hash_or_arrayref;
$perl_hash_or_arrayref  = decode_json $utf8_encoded_json_text;

Обе этих формы работают только с данными в кодировке utf8, и это надо учитывать.

В ОО-интерфейсе булевый флаг allow_nonref при объявлении объекта JSON позволяет впоследствии конвертировать не только массивы и хэши, указатели на которые передаются в качестве параметров, но и скалярные переменные.

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

$json->indent->space_before->space_after;

Здесь space_before и space_after управляют наличием пробелов вокруг разделителя “:” между ключами и их значениями в объекте JSON.

Флаг canonical позволяет включить сортировку объектов внутри текста JSON по ключам в алфавитном порядке:

$json = $json->canonical;

Этих вот знаний с головой хватает для полноценной работы с форматом JSON.

XML::Twig

Как и в предыдущем случае сразу приведу простенький вариант использования. Итак, нам надо распарсить некоторый XML-файл, преобразовав его из текстового вида в некоторую перловую структуру. Так же просто, как в случае с JSON, у нас не получится. Впрочем, и не надо – простота заменяется чрезвычайной гибкостью, что в случае с очень большими XML-файлами играет важную роль.

Пример XML:

<?xml version="1.0" encoding="windows-1251"?>
   <yml_catalog date="2010-09-08 23:15">
      <shop>
         <name>Shop name</name>
         <currencies>
             <currency id="RUR" rate="1" />
         </currencies>
         <offers>
             <offer id="83" type="a" available="true">
                 <price>185</price>
                 <currencyId>RUR</currencyId>
                 <name>Товар 83</name>
            </offer>
            <offer id="84" type="b" available="true">
                 <price>90</price>
                 <currencyId>RUR</currencyId>
                 <name>Товар 84</name>
            </offer>
            </offers>
         </shop>
   </yml_catalog>

Эта самая гибкость позволит нам добраться только до нужных тэгов, оставив за бортом всё лишнее. Например, если нам нужны тэги <offer> и их содержимое включая атрибуты:

use XML::Twig;
// создаём объект XML::Twig и парсим файл
my $t = XML::Twig->new();
$t->parsefile('shop.xml');
// берём корневой элемент
my $root=$t->root;
// после этого переменная $root содержит всё дерево XML
// далее указываем флаг читабельного вывода с переносом строк
$root->set_pretty_print('indented');
// ищем тэг-наследник <offers>
my $offers=$root->first_child('shop')->first_child('offers');
my %hash;
// пробегаем все тэги <offer> внутри <offers>
foreach my $offer ($offers->children('offer')) {
    if ($offer->has_atts) {
        my $id = $offer->att('id');
        // так мы запоминаем все пары атрибут-значение
        foreach my $a (keys %{$offer->atts}) {
            $hash{$id}->{$a}=$offer->atts->{$a};
        }
        // а так запоминаем пары тэг-значение
        foreach my $t ($offer->children) {
            my $tag = $t->tag;
            my $value = encode('utf8', $t->text);
            $hash{$id}->{$tag}=$value;
        }
   }
}

В итоге – мы получили в хэше %hash всё, что хотели. Скрипт получился чуть побольше, чем в случае с JSON, поэтому я разбавил его комментариями.

Но парсингом файлов возможности не ограничиваются. Мы легко можем всячески модифицировать XML, чтобы впоследствии обновить сам файл.

Извлечение значения тэга

Как видно из скрипта, для получения значения тэга (в том числе если у него есть потомки) используется метод text:

$offers_text = $offers->text;

В моём случае я ещё преобразую текст в кодировку utf8.

Замена тэга

$offers->set_tag(‘wares’);

Так мы заменим тэги <offers> на <wares>. Само собой предполагается, что переменной $offers мы уже присвоили указатель на соответствующий блок, как это сделано в скрипте-примере.

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

foreach my $offer ($offers->children('offer')) {
    $offer->erase;
}

Так мы удалим все тэги <offer></offer>, оставляя на месте всех их потомков.

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

$offer->del_att('type', 'available');
$offer->set_att('invisible' => 'true', 'price' => '100');
$offer->dell_atts;

Так мы удалим ненужные атрибуты, добавим новые, а напоследок вообще удалим все атрибуты тэга.

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

my $t = XML::Twig->new();
$t->parsefile("ugly.xml");
my $root = $t->root;
$root->set_pretty_print('indented');
$t->print_to_file("pretty.xml");

Так, например, мы откроем нечитабельный XML-файл и пересохраним его в красивом (pretty) виде в новый файл.

Немножко про get_xpath

Этот метод позволяет очень гибко искать нужные тэги или наборы тэгов в документе (примерно как jQuery позволяет искать элементы внутри документа DOM). Возвращается при этом массив из всех удовлетворяющих тэгов.

Вот немножко примеров:

// Получить второй тэг name
my @res=$t->get_xpath(“//name[2]“);

// Получить последний тэг currencyId
my @res=$t->get_xpath(“//currencyId[last()]“);

// Получить все тэги с атрибутом type, равным “b”
my @res=$t->get_xpath(qq~//*[@type="b"]~);

//Найти тэги <name> со значением, соответствующим регулярному выражению
my @res=$t->get_xpath(qq~//name[string()=~/Товар/]~);

В общем, множество всяких вкусностей преподносит get_xpath. Отмечу напоследок, что символы “//” в начале параметра определяют поиск по всем вложенным тэгам, а не только по непосредственным наследникам.

На этом всё. До новых встреч!

Category: Perl | No comments »

Comments

Leave a comment

 Comment Form