Выпуск 2. Апрель 2013

От редактора | Содержание | Удобное логирование с Log::Any

Преобразование XML в Perl-структуры с помощью XML::Simple

XML::Simple не рекомендуется к использованию для работы с XML в современном Perl, однако бывают ситуации, когда выбор уже сделан и необходимо поддерживать имеющийся код. Для таких случаев и была написана данная статья.

Преобразование XML в Perl-структуры и обратно довольно часто встречающаяся задача. Конечно, в большинстве таких случаев XML-файлы имеют простую структуру. Однако, решение совсем не тривиально.

XML::Simple — очень популярный модуль времен, когда XML::LibXML был большим и слишком сложным. С тех пор утекло много воды, появился Modern Perl, интерфейс XML::LibXML существенно упростился, а XML::Simple, наоборот, перестал рекомендоваться к использованию, причем самим автором модуля.

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

Создание XML

Мы будем рассматривать задачу создания XML-документа, потому что с разбором заданной структуры никаких проблем нет, и результат там практически всегда логичен и понятен.

Итак, на сервер приходит запрос списка объектов и их значений. На выходе мы хотим получить следующий результат:

<?xml version="1.0" encoding="UTF8"?>
<cats>
  <cat name='Daisy'>4</cat>
  <cat name='Abby'>5</cat>
</cats>

Попробуем построить документ, воспользуясь XML::Simple:

use XML::Simple;

my $xs      = XML::Simple->new();
my $hashref = {
    cats => {
        cat => [
            {
                name  => 'Daisy',
                value => 4
            },
            {
                name  => 'Abby',
                value => 5
            },
        ]
    }
};

say $xs->XMLout($hashref);

… на выходе получилось совсем не то, что нужно. Декларации XML-документа нет, все обернуто в <opt>:

<opt>
  <cats>
    <cat name="Daisy" value="4" />
    <cat name="Abby" value="5" />
  </cats>
</opt>

После более детального ознакомления с документацией, напишем следующий код:

my $hashref = {
    cat => [
        {
            name    => 'Daisy',
            content => 4
        },
        {
            name    => 'Abby',
            content => 5
        },
    ]
};

say $xs->XMLout(
    $hashref,
    XMLDecl  => '<?xml version="1.0" encoding="UTF8"?>',
    RootName => 'cats',
);

То, что нужно!

Итак, мы принудительно указали декларацию XML-документа, задали корневой элемент, а так же изменили value на content — это ключевое слово в имени ключа заставило XML::Simple перенести значения объектов в содержимое тегов, а не в атрибуты. Как видно, довольно много опцией для такого простого файла.

Изменив value на content, мы использовали возможности преобразования данных которые нам предоставляет модуль. Однако, это не всегда удобно, хотя и, зачастую, повышает читаемость исходных данных. В большинстве же случаев можно обойтись стандартным поведением: по умолчанию модуль преобразовывает структуру вида { key => 'value' } в xml вида <key attr=>"value" />, а структуру {key => ['value']} в <key>value</key>. Т.е. каждая простая строка будет отображена в виде атрибута, а массив будет превращен во вложенные элементы.

Как же нужно изменить код, чтобы добавить новые значения?

<?xml version="1.0" encoding="UTF8"?>
<cats>
  <cat name='Daisy'>
    <age>4</age>
    <weight>3.5</weight>
  </cat>
  <cat name='Abby'>
    <age>5</age>
    <weight>6</weight>
  </cat>
</cats>

Просто добавив поля:

{
    name => 'Daisy',
    weight => 4,
    age => 3.5,
},

Но мы получим не совсем то, что ожидалось:

<cat name="Daisy" age="3.5" weight="4" />

Более логичным было бы сделать следующим образом:

{
    name => 'Daisy',
    content => {
        weight => 4,
        age => 3.5,
    },
},

Но на самом деле правильный вариант:

name => 'Daisy',
weight => {
    content => 4,
},
age => {
    content => 3.5,
},

Что в целом объяснимо — мы превращаем age в структуру с ключевым словом content, которое подставляет свое значение в содержимое тегов. Однако, из примера выше видно, что если в content мы подставим структуру — то все особенные свойства сразу исчезнут и это будет обычный тег. Но что делать, если в исходных данных есть такое название столбца и вы хотите чтобы он был атрибутом, а не проявлял свои особенные свойства в самый неподходящий момент? Делаем так:

$xs->XMLout($hashref ,
    XMLDecl => '<?xml version="1.0" encoding="UTF8"?>',
    RootName => 'cats',
    ContentKey => ''
);

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

Также можно вообще запретить использование тегов в качестве атрибутов:

$xs->XMLout($hashref , NoAttr => 1);>

<cat>
    <name>Abby</name>
    <age>5</age>
    <weight>6</weight>
</cat>

Разбор XML

После генерации XML-документа рассмотрим особенности разбора. В этом случае все несколько проще:

my $ref = $xs->XMLin($xml_file);

Если мы возьмем сгенерированный выше XML и передадим его парсеру — то получим не ту структуру из которой XML создавался. Так как тег name заставит парсер создать структуру вида:

'cat' => {
   'Daisy' => {
              'weight' => '4',
              'age' => '3.5'
            },
   'Abby' => {
             'weight' => '6',
             'age' => '5'
           }
}

Чтоб этого избежать задаем опцию KeyAttr => '':

my $ref = $xs->XMLin($xml_file, KeyAttr => '');

'cat' => [
   {
        'weight' => '4',
        'name' => 'Daisy',
        'age' => '3.5'
   },
   {
        'weight' => '6',
        'name' => 'Abby',
        'age' => '5'
    }
]

Следующий нюанс — как видно выше, список объектов представляет собой массив хешей с атрибутами. Но что будет, если объект только один? Список превратится в хеш и нам придется отслеживать этот момент для правильной обработки. К сожалению, опции для нормального решения при разборе нет. Использование ForceArray => qw/cat/ приводит к вот таким последствиям:

'cat' => [
   {
        'weight' => [
                 '4'
            ],
        'name' => [
               'Daisy'
            ],
        'age' => [
              '3.5'
            ]
    }
]

Опция GroupTags => { cats => 'cat' } тоже не приводит к нужному эффекту. Причем ни в случае одного элемента, ни в случае нескольких.

Выводы

В целом, XML::Simple реализует простой интерфейс к XML (что логично следует из названия модуля). Проблема в том, что этот интерфейс реализует большое кол-во опций и это приводит к нелогичному поведению и запутанности при работе с XML со сложной структурой. Так что, если нужно быстро создать или разобрать простой XML небольшого объема, то можно смело использовать этот модуль. Он достаточно быстр и стабилен. Но если необходимо работать со сложными структурами и большими объемами — то лучше использовать XML::LibXML и аналоги. При более высоком пороге вхождения они дадут более предсказуемое поведение и высокую скорость обработки данных.

Денис Федосеев


От редактора | Содержание | Удобное логирование с Log::Any
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье