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

XML ещё жив и иногда его приходиться парсить. Особенно если вы работаете со СМЭВ (привет всем ребятам для которых "ФОИВ" не пустой звук :) ).

Цели у такого парсинга могут быть самые разные, от банального ответа на вопрос какое пространство имён используется в xml-документе, до необходимости получить структурированное представление для документа вцелом.

Инструмент для каждой цели будет свой. Пространство имён можно найти поиском подстроки или регулярным выражением. Что бы сделать из xml-документа структурированное представление (DTO) - придётся писать парсер.

Для работы с XML в PHP есть пара встроенных классов. Это XMLReader и SimpleXMLElement.

XMLReader

С помощью XMLReader парсинг будет выглядеть примерно так :

$reader = (new XMLReader());
$reader->XML($content);
while ($reader->read()) {
    $this->parse($reader);
}

Внутри метода parse(XMLReader $xml) будут бесконечные:

$name = $xml->name;
$value = $xml->expand()->textContent;
$attrVal = $xml->getAttribute('attribute');
$isElem = $xml->nodeType === XMLReader::ELEMENT;

Для небольших документов или когда нам из всего документа надо только пару элементов, это приемлемо, на больших объёмах - начинает в глазах рябить от однообразного кода, плюс совесть грызёт за оверхэд от перебора всех элементов документа.

SimpleXMLElement

Провести анализ только нужных элементов помогает SimpleXMLElement. Этот класс из XML-документа делает объект, у которого все элементы и атрибуты становятся свойствами, то есть появляется возможность работать только с определёнными элементами, а не со всеми подряд, пример:

$document = new SimpleXMLElement($content);
/* имя корневого элемента */
$name = $document->getName();

/* получить произвольный элемент */
$primary = $document
	->Message
	->ResponseContent
	->content
	->MessagePrimaryContent ?? null;

/* получить элементы определённого пространства имён */
$attachment = $primary
	->children(
		'urn://x-artefacts-fns-zpvipegr/root/750-08/4.0.1'
	)
	->xpath('tns:Вложения/fnst:Вложение')[0];

/* получить значение элемента */
$fileName = $attachment
	->xpath('//fnst:ИмяФайла')[0]
	->__toString();

Удобно, да не совсем. Если имя элемента на кириллице, то обратиться к нему через свойство не получиться, придётся использовать SimpleXMLElement::xpath(). С множественными значениями так же приходиться работать через SimpleXMLElement::xpath(). Кроме того SimpleXMLElement имеет свои особенности и некоторые вещи далеко не очевидны.

Converter

Есть способ проще. Достаточно XML-документ привести к массиву. В работе с массивами нет ни каких подводных камней. Массив из XML делается в пару строчек кода:

$xml=<<<XML
    <b attr4="55">
        <c>ccc
            <d/>
        </c>
        0000
    </b>           
XML;
$fabric = (new NavigatorFabric())->setXml($xml);
$converter = $fabric->makeConverter();
$arrayRepresentationOfXml = $converter->toArray();

Каждый XML-элемент будет представлен массивом, состоящим в свою очередь, из трёх других массивов.

Соответственно:

  • массив с индексом '*value' содержит значение элемента,

  • '*attributes' - атрибуты элемента,

  • '*elements' - вложенные элементы.

/*
'b' =>
  array (
	'*value' => '0000',
	'*attributes' =>
	array (
	  'attr4' => '55',
	),
	'*elements' =>
	array (
	  'c' =>
	  array (
	  ),
	),
  ),
*/

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

$xml=<<<XML	
<doc>
    <qwe>first occurrence</qwe>
    <qwe>second occurrence</qwe>
</doc>
XML;

/*
'doc' =>
array (
  'qwe' =>
  array (
	  '*multiple' =>
	  array (
  	  0 =>
	    array (
		  '*value' => 'first occurrence',
	    ),
	    1 =>
	    array (
		  '*value' => 'second occurrence',
	    )
	  )
  )
)
*/

Но и это ещё не всё.

XmlNavigator

Если от работы с XML-документов как с массивом, у вас в глазах рябит от квадратных скобочек, то XmlNavigator - это ваш вариант, создаётся так же в две строки кода.

/* документ */
$xml = <<<XML
<doc attrib="a" option="o" >666
    <base/>
    <valuable>element value</valuable>
    <complex>
        <a empty=""/>
        <b val="x"/>
        <b val="y"/>
        <b val="z"/>
        <c>0</c>
        <c v="o"/>
        <c/>
        <different/>
    </complex>
</doc>
XML;
$fabric = (new NavigatorFabric())->setXml($xml);
$navigator = $fabric->makeNavigator();

XmlNavigator делает, то же самое что и Converter, но предоставляет API, и с документом мы работаем как с объёктом.

Имя элемента, метод name()

/* Имя элемента */
echo $navigator->name();
/* doc */

Значение элемента, метод value()

/* Значение элемента */
echo $navigator->value();
/* 666 */

Список атрибутов, метод attribs()

/* get list of attributes */
echo var_export($navigator->attribs(), true);
/*
array (
  0 => 'attrib',
  1 => 'option',
)
*/

Значение атрибута, метод get()

/* get attribute value */
echo $navigator->get('attrib');
/* a */

Список вложенных элементов, метод elements()

/* Список вложенных элементов */
echo var_export($navigator->elements(), true);
/*
array (
  0 => 'base',
  1 => 'valuable',
  2 => 'complex',
)
*/

Получить вложенный элемент, метод pull()

/* Получить вложенный элемент */
$nested = $navigator->pull('complex');

echo $nested->name();
/* complex */

echo var_export($nested->elements(), true);
/*
array (
  0 => 'a',
  1 => 'different',
  2 => 'b',
  3 => 'c',
)
*/

Перебрать все вхождения множественного элемента, метод next()

/* Получить вложенный элемент вложенного элемента */        
$multiple = $navigator->pull('complex')->pull('b');

/* Перебрать все вхождения множественного элемента */
foreach ($multiple->next() as $index => $instance) {
    echo " {$instance->name()}[$index]" .
        " => {$instance->get('val')};";
}
/*
b[0] => x; b[1] => y; b[2] => z;
*/

Все методы класса XmlNavigator

Класс XmlNavigator реализует интерфейс IXmlNavigator.

<?php

namespace SbWereWolf\XmlNavigator;

interface IXmlNavigator
{
    public function name(): string;

    public function hasValue(): string;

    public function value(): string;

    public function hasAttribs(): bool;

    public function attribs(): array;

    public function get(string $name = null): string;

    public function hasElements(): bool;

    public function elements(): array;

    public function pull(string $name): IXmlNavigator;

    public function isMultiple(): bool;

    public function next();
}

Из названий методов очевидно их назначение. Не очевидные были рассмотрены выше.

Как установить?

composer require sbwerewolf/xml-navigator

Заключение

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

$document = new SimpleXMLElement($content);
$primary = $document
    ->Message
    ->ResponseContent
    ->content
    ->MessagePrimaryContent;
$attachment = $primary
    ->children(
        'urn://x-artefacts-fns-zpvipegr/root/750-08/4.0.1'
    )
    ->xpath('tns:Вложения')[0];

$fabric = (new NavigatorFabric())->setSimpleXmlElement($attachment);
$navigator = $fabric->makeNavigator();

Желаю вам приятного использования.

Эпилог

Конечно у вас могут быть свои альтернативы для работы с XML. Предлагаю поделиться в комментариях.

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

Если вам важен порядок следования элементов, то придётся пользоваться XMLReader. Потому что SimpleXMLElement приводит документ к объекту, а у объекта нет такого понятия как порядок следования элементов.

Комментарии (15)


  1. iMedved2009
    02.01.2022 07:38
    +7

    на больших объёмах

    Ну вроде как XMLReader и предназначен для работы с БОЛЬШИМИ документами. Ибо работает с потоком, а тот же SimpleXML грузит и парсит весь файл сразу.


    1. SbWereWolf Автор
      04.01.2022 23:16

      XMLReader  работает на очень низком уровне, буквально лексический разбор.

      В одной из первых иттераций парсинга, была мысли результат $reader->read() отдавать последователь во все парсеры и таким образом документ бы парсился за один проход.

      Но когда другие команды стали писать свои парсеры, они эту идею не оценили, пришлось один и тоот же докумнет передавать в кучу парсеров.

      Для XML, в котором вас интересует больше 10-ти элементов, на код страшно смотреть, либо если всё завернуть в приватные методы, то получается слишком много микроскопических методов с шабонным кодом, хотя эту проблему тоже можно решить, но всё равно на выходне много букв, устаёшь скролить и читать.

      Почему я стал искать альтернативу XMLReader  ? Все входящие сообщения у нас парсились на примерно 10-ке парсерах, и вот один из парсеров выполнялся по 2 секунды, при том что этот парсер не находил ни чего для себя интересного, то есть работал в холостую, другие аналогичные парсеры отрабатывали документы за 5-50 мс максимум.

      По идее этот неудачный парсер за каждый цикл чтения $reader->read() выполнял около 20-ти раз оператор IF, ине понятно чему там тормозить, когда я переписал его на SimpleXMLElement , время работы сократилось до приемлимых 50мс.

      Как мне кажестя SimpleXMLElement не вычитывает весь документ за раз и не строит DOM, SimpleXMLElement вычитывает один элемент (все атрибуты и "иннер текст") за раз и строит только его объектную модель.

      Если бы оно выжирало гигабайты памяти (какие то XML у нас реально гигабайты весят), мне бы настучали по голове и по рукам, но этого не произошло: или этого ни разу не было, или админы не успели заметить :)

      Сейчас после последней оптимизации на каждый документ вызывается парсер соответствующий его пространству имён. Поэтому сейчас на каждый документ не создаётся по 10+ джоб, создаётся одна или не одной.

      Но осадочек остался, с SimpleXMLElement код очень приятно писать, снова чувствуешь себя человеком, а не студентом второго курса пишущем на ассембелере.

      Если у бизнес команд горит и им не когда ждать, что бы им написали парсер, то было решено отдавать массив (с помощью Converter).

      Кроме того, разным командам в одном и том же документе могут быть интересны разные куски, что бы не заморачиваться, поэтому мы решили всегда отдавать массивом, когда парсинг станет слишком прожорливым - придумаем что ни будь ещё.

      Позже я написал объектно ориентированную обёртку над этим массивом (XmlNavigator).

      Такая история есл икоротко, и промолчать пр о100500 нюансов.


      1. iMedved2009
        04.01.2022 23:50

        Я нисколько не обсуждал нужность или полезность вашего парсера. Вполне возможно что он был необходим. Мне трудно понять почему кому то хочется работать с xml как с массивом, или какие подводные камни работы есть в работе с xml, но возможно конетекст задачи таков - что это наиболее подходящее решение.

        Я лишь увидел что про XMLReader у вас написаны неправильные вещи. XMLReader работает с потоком - уперся в ноду, передал решение вам - идти вглубь, идти в следующую ноду или чего еще. Это ровно сделано для больших файлов, которые нельзя загрузить в память и распарсить в обьект. И все то что вам в нем не нравится - следствие этого.

        Как мне кажестя SimpleXMLElement не вычитывает весь документ за раз и не
        строит DOM, SimpleXMLElement вычитывает один элемент (все атрибуты и
        "иннер текст") за раз и строит только его объектную модель

        Боюсь вам не правильно кажется. Там вроде как есть лимит для библиотеки libxml, но скорее всего вы раньше уткнетесь в память пыха.



  1. tommyangelo27
    02.01.2022 10:41
    +2

    Пользуюсь XMLReader, потому что грузить в память 4Gb документы как-то не очень...


  1. Armleo
    02.01.2022 11:21
    +5

    Как написать эту статью:

    Шаг 1: Не разбиратся в теме, и так сойдет.

    Шаг 2: Придумать причину почему вам именно нужен масив

    Шаг 3: Обосновать логически (или просто накидать рандомную причину, которой нету)

    Шаг 4. Написать пустую статью


    1. SbWereWolf Автор
      02.01.2022 12:09
      +1

      Очень смешно читать такие коменты.

      Я написал что пользуюсь разными инструментами, в том числе и XMLReader , и SimpleXMLElement, судя по опросу многие делают так же, это называется не разобраться в теме ?

      Вы знаете почему надо из документа сделать DTO и отдать на обработку дальше, другой команде ?

      Вы знаете что другая команда отказывается иметь дело с XML и ждёт от вас только DTO, максимум на что согласны - на массив ?

      Вы знаете что документ который надо распарсить имеет не тривиальную XSD, и уникальных элементов в нём до 300 ?

      Времени на задачу у вас два дня от силы. Ваши действия ?

      Вы разобрались в теме или и так соёдёт ?

      Я не знаю каких в вашей жизни причин нет, а в моей жизни причин хватает :)


  1. dopusteam
    02.01.2022 12:42
    +2

    плюс совесть грызёт за оверхэд от перебора всех элементов документа

    А как работает создание массива из xml? Не в память всё пишет?

    Непонятно, зачем нужен *multiple, почему не сделать просто массив или что там у Вас?

    attribs() - что Вы делаете с сэкономленным временем? Ну, правда, attributes же гораздо лучше

    Кстати, тут немного проблема в терминологии, у Вас тут ассоциативный массив, а не просто массив. В php всё одно вроде, не знаю, но немного сбивает с толку

    Плюс, проблема обозначена недостаточно прозрачно


  1. Szhukov
    02.01.2022 14:05
    +10

    К любому элементу на кириллице можно обратиться не используя xpath. $parent->{'какой то элемент XML'}


  1. arokettu
    02.01.2022 18:58
    +1

    Использую sabre/xml на основе XMLReader. Киллер-фича в том что можно через коллбэки распарсить элементы сразу в массивы. Еще одна фича, которая для меня оказалась не менее важной - если распарсить просто в массив и сохранить фрагмент как документ, все пространства имен останутся на месте, SimpleXML имеет свойство их терять


  1. Katrychenko
    02.01.2022 18:58
    +2

    $xml = simplexml_load_string($xml_string);
    $json = json_encode($xml);
    $array = json_decode($json,TRUE

    wow?


    1. kvadrokot
      03.01.2022 11:38

      так неинтересно


    1. mrcoldil
      04.01.2022 22:38

      wow конечно, но лучше не пользоватся таким если нет желания потерять аттрибуты:

      <?xml version="1.0" encoding="UTF-8"?>
      <GetItemResponse xmlns="urn:ebay:apis:eBLBaseComponents">
          <Item>
              <AutoPay>true</AutoPay>
              <ListingDetails>
                  <ConvertedBuyItNowPrice currencyID="USD">0.0</ConvertedBuyItNowPrice>
                  <ConvertedStartPrice currencyID="USD">313.6</ConvertedStartPrice>
                  <ConvertedReservePrice currencyID="USD">0.0</ConvertedReservePrice>
              </ListingDetails>
          </Item>
      </GetItemResponse>
      array(1) {
        ["Item"]=>
        array(2) {
          ["AutoPay"]=>
          string(4) "true"
          ["ListingDetails"]=>
          array(3) {
            ["ConvertedBuyItNowPrice"]=>
            string(3) "0.0"
            ["ConvertedStartPrice"]=>
            string(5) "313.6"
            ["ConvertedReservePrice"]=>
            string(3) "0.0"
          }
        }
      }
      


  1. OkunevPY
    03.01.2022 10:39

    Хм. Велосипедостроение.

    Есть три варианта работат с xml, никто пока ничего нового не придумал.

    1. SAX, самый быстрый способ однонаправленного чтения/записис, даёт хорошую производительность при минимальном расходе ресурсоа. Не удобен для чтения и построения сложных обектов, код плохо читаемый.

    2. DOM, модель документа материализуються и работа идёт с верхнеуповневой абстракцией, удобно, просто, легко читаемо. Очень медленно работает, ест память.

    3. Разбор XML как строки или массива байт, самый не удобный, но самый эффективный способ, подходит когда нужно проверить значение или найти в документе заранее известный шаблон.

      Тот или иной подход выбираеться в зависимости от обстоятельств, обычно к DOM приходят или на момент MVP или когда размер исходного документа не значителен, формировать XML проще и быстрее кастомной сериализацией сразу в текст.


  1. SerjV
    03.01.2022 23:25

    XML ещё жив и иногда его приходиться парсить. Особенно если вы работаете со СМЭВ

    Эм... А что, XML нынче кроме как для электрички больше ни для чего не используется?

    Или это конкретно для PHP она - основной потребитель XML?