Всем привет. Хочу поделиться своим опытом в парсинге 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)
tommyangelo27
02.01.2022 10:41+2Пользуюсь XMLReader, потому что грузить в память 4Gb документы как-то не очень...
Armleo
02.01.2022 11:21+5Как написать эту статью:
Шаг 1: Не разбиратся в теме, и так сойдет.
Шаг 2: Придумать причину почему вам именно нужен масив
Шаг 3: Обосновать логически (или просто накидать рандомную причину, которой нету)
Шаг 4. Написать пустую статью
SbWereWolf Автор
02.01.2022 12:09+1Очень смешно читать такие коменты.
Я написал что пользуюсь разными инструментами, в том числе и XMLReader , и SimpleXMLElement, судя по опросу многие делают так же, это называется не разобраться в теме ?
Вы знаете почему надо из документа сделать DTO и отдать на обработку дальше, другой команде ?
Вы знаете что другая команда отказывается иметь дело с XML и ждёт от вас только DTO, максимум на что согласны - на массив ?
Вы знаете что документ который надо распарсить имеет не тривиальную XSD, и уникальных элементов в нём до 300 ?
Времени на задачу у вас два дня от силы. Ваши действия ?
Вы разобрались в теме или и так соёдёт ?
Я не знаю каких в вашей жизни причин нет, а в моей жизни причин хватает :)
dopusteam
02.01.2022 12:42+2плюс совесть грызёт за оверхэд от перебора всех элементов документа
А как работает создание массива из xml? Не в память всё пишет?
Непонятно, зачем нужен *multiple, почему не сделать просто массив или что там у Вас?
attribs() - что Вы делаете с сэкономленным временем? Ну, правда, attributes же гораздо лучше
Кстати, тут немного проблема в терминологии, у Вас тут ассоциативный массив, а не просто массив. В php всё одно вроде, не знаю, но немного сбивает с толку
Плюс, проблема обозначена недостаточно прозрачно
Szhukov
02.01.2022 14:05+10К любому элементу на кириллице можно обратиться не используя xpath. $parent->{'какой то элемент XML'}
arokettu
02.01.2022 18:58+1Использую sabre/xml на основе XMLReader. Киллер-фича в том что можно через коллбэки распарсить элементы сразу в массивы. Еще одна фича, которая для меня оказалась не менее важной - если распарсить просто в массив и сохранить фрагмент как документ, все пространства имен останутся на месте, SimpleXML имеет свойство их терять
Katrychenko
02.01.2022 18:58+2$xml = simplexml_load_string($xml_string); $json = json_encode($xml); $array = json_decode($json,TRUE
wow?
mrcoldil
04.01.2022 22:38wow конечно, но лучше не пользоватся таким если нет желания потерять аттрибуты:
<?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" } } }
OkunevPY
03.01.2022 10:39Хм. Велосипедостроение.
Есть три варианта работат с xml, никто пока ничего нового не придумал.
SAX, самый быстрый способ однонаправленного чтения/записис, даёт хорошую производительность при минимальном расходе ресурсоа. Не удобен для чтения и построения сложных обектов, код плохо читаемый.
DOM, модель документа материализуються и работа идёт с верхнеуповневой абстракцией, удобно, просто, легко читаемо. Очень медленно работает, ест память.
-
Разбор XML как строки или массива байт, самый не удобный, но самый эффективный способ, подходит когда нужно проверить значение или найти в документе заранее известный шаблон.
Тот или иной подход выбираеться в зависимости от обстоятельств, обычно к DOM приходят или на момент MVP или когда размер исходного документа не значителен, формировать XML проще и быстрее кастомной сериализацией сразу в текст.
SerjV
03.01.2022 23:25XML ещё жив и иногда его приходиться парсить. Особенно если вы работаете со СМЭВ
Эм... А что, XML нынче кроме как для электрички больше ни для чего не используется?
Или это конкретно для PHP она - основной потребитель XML?
iMedved2009
Ну вроде как XMLReader и предназначен для работы с БОЛЬШИМИ документами. Ибо работает с потоком, а тот же SimpleXML грузит и парсит весь файл сразу.
SbWereWolf Автор
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 нюансов.
iMedved2009
Я нисколько не обсуждал нужность или полезность вашего парсера. Вполне возможно что он был необходим. Мне трудно понять почему кому то хочется работать с xml как с массивом, или какие подводные камни работы есть в работе с xml, но возможно конетекст задачи таков - что это наиболее подходящее решение.
Я лишь увидел что про XMLReader у вас написаны неправильные вещи. XMLReader работает с потоком - уперся в ноду, передал решение вам - идти вглубь, идти в следующую ноду или чего еще. Это ровно сделано для больших файлов, которые нельзя загрузить в память и распарсить в обьект. И все то что вам в нем не нравится - следствие этого.
Боюсь вам не правильно кажется. Там вроде как есть лимит для библиотеки libxml, но скорее всего вы раньше уткнетесь в память пыха.