Работаем с XML как с массивом, версия 2

от автора

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

В двух словах для быстрого парсинга файлов надо пользоваться XMLReder в связке с yield.

О моей реализации этой связки читайте ниже.

XMLReder

XMLReder это единственный класс в PHP, который умеет читать файл по частям. В PHP есть SimpleXML, есть DOMDocument, но они работают с файлом целиком.

И пока мы читаем файл с диска, наш процессор простаивает, пока мы превращаем файл в экземпляр SimpleXML — простаивает база данных, а надо сказать, что XML файл парситься именно для того что бы вставить запись в БД. И все эти простои, это время потраченное в пустую.

yield

Следующее что нам поможет это выражение yield. Прочитали один элемент из файла, распарсили, сформировали SQL команду insert, выполнили команду, читаем следующий элемент из файла, и так далее, до победного конца. Ни кто не простаивает, всё примерно одинаково загружено.

Теперь сложим всё вместе и получим FastXmlToArray.

FastXmlToArray

FastXmlToArray это класс со статическим методом convert(), на вход можно подать или ссылку на XML файл (XML URI) или собственно XML строку. На выходе будет PHP массив со всеми атрибутами и значениями корневого элемента и всех вложенных элементов.

$xml =<<<XML <outer any_attrib="attribute value">     <inner>element value</inner>     <nested nested-attrib="nested attribute value">nested element value</nested> </outer> XML; $result =     \SbWereWolf\XmlNavigator\FastXmlToArray::prettyPrint($xml); echo json_encode($result, JSON_PRETTY_PRINT);

Вывод на консоль

{   "outer": {     "@attributes": {       "any_attrib": "attribute value"     },     "inner": "element value",     "nested": {       "@value": "nested element value",       "@attributes": {         "nested-attrib": "nested attribute value"       }     }   } }

Killer feature этого класса это статический метод extractElements(), который принимает XMLReader, а выдаёт всё тоже самое: массив со всеми атрибутами и значениями этого элемента и всех вложенных элементов.

    /**      * @param XMLReader $reader      * @param string $valueIndex index for element value      * @param string $attributesIndex index for element attributes collection      * @return array      */     public static function extractElements(         XMLReader $reader,         string $valueIndex = IFastXmlToArray::VALUE,         string $attributesIndex = IFastXmlToArray::ATTRIBUTES,     ): array;

Существенная разница, в том что convert() обрабатывает сразу корневой элемент XML документа, а extractElements() работает с произвольным элементом, не обязательно с корневым, можно передать любой.

Пример использования

Будем парсить что такое:

<?xml version="1.0" encoding="utf-8"?> <CARPLACES>     <CARPLACE             ID="11361653"             OBJECTID="20326793"     />     <CARPLACE             ID="94824"             OBJECTID="101032823"     /> </CARPLACES>

Допустим нас во всё документе интересуют только элементы CARPLACE.

Переведём XMLReader на первый элемент CARPLACE

$reader = XMLReader::XML($xml); $mayRead = true; while ($mayRead && $reader->name !== 'CARPLACE') {     $mayRead = $reader->read(); }

Пройдёмся по всем элементам CARPLACE, пока не перейдём к элементу с другим именем или пока документ не кончиться

while ($mayRead && $reader->name === 'CARPLACE') {     $elementsCollection = FastXmlToArray::extractElements(         $reader,     );     $result = FastXmlToArray::createTheHierarchyOfElements(         $elementsCollection,     );     echo json_encode([$result], JSON_PRETTY_PRINT);      while (         $mayRead &&         $reader->nodeType !== XMLReader::ELEMENT     ) {         $mayRead = $reader->read();     } }

Что мы тут делаем ?

Получаем элемент со всеми его свойствами и списком вложенных элементов (в этом примере вложенных нет)

$elementsCollection = FastXmlToArray::extractElements(     $reader, );

Формируем иерархический массив (в этом примере из одного элемента и его атрибутов)

$result = FastXmlToArray::createTheHierarchyOfElements(     $elementsCollection, );

Выводим в консоль получившийся массив

echo json_encode([$result], JSON_PRETTY_PRINT);

Проматываем «файл» до следующего элемента или до конца «файла»

while (     $mayRead &&     $reader->nodeType !== XMLReader::ELEMENT ) {     $mayRead = $reader->read(); }

В консоли будет что то такое:

[     {         "n": "CARPLACE",         "a": {             "ID": "11361653",             "OBJECTID": "20326793"         }     } ][     {         "n": "CARPLACE",         "a": {             "ID": "94824",             "OBJECTID": "101032823"         }     } ]

Другой пример

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <QueryResult         xmlns="urn://x-artefacts-smev-gov-ru/services/service-adapter/types">     <smevMetadata             b="2">         <MessageId                 c="re">c0f7b4bf-7453-11ed-8f6b-005056ac53b6         </MessageId>         <Sender>CUST01</Sender>         <Recipient>RPRN01</Recipient>     </smevMetadata>     <Message             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"             xsi:type="RequestMessageType">         <RequestMetadata>             <clientId>a0efcf22-b199-4e1c-984a-63fd59ed9345</clientId>             <linkedGroupIdentity>                 <refClientId>a0efcf22-b199-4e1c-984a-63fd59ed9345</refClientId>             </linkedGroupIdentity>             <testMessage>false</testMessage>         </RequestMetadata>         <RequestContent>             <content>                 <MessagePrimaryContent>                     <ns:Query                             xmlns:ns="urn://rpn.gov.ru/services/smev/cites/1.0.0"                             xmlns="urn://x-artefacts-smev-gov-ru/services/message-exchange/types/basic/1.2"                     >                         <ns:Search>                             <ns:SearchNumber                                     Number="22RU006228DV"/>                         </ns:Search>                     </ns:Query>                 </MessagePrimaryContent>             </content>         </RequestContent>     </Message> </QueryResult>

Пример запроса сведений пришедший по СМЭВу, нас тут интересует только элемент ns:Query

Код будет аналогичным:

$mayRead = true; $reader = XMLReader::XML($xml); while ($mayRead && $reader->name !== 'ns:Query') {     $mayRead = $reader->read(); }  while ($reader->name === 'ns:Query') {     $elementsCollection = FastXmlToArray::extractElements(         $reader,     );     $result = FastXmlToArray::createTheHierarchyOfElements(         $elementsCollection,     );      echo json_encode([$result], JSON_PRETTY_PRINT);      while (         $mayRead &&         $reader->nodeType !== XMLReader::ELEMENT     ) {         $mayRead = $reader->read();     } } $reader->close();

Вывод на консоль:

[     {         "n": "ns:Query",         "a": {             "xmlns:ns": "urn:\/\/rpn.gov.ru\/services\/smev\/cites\/1.0.0",             "xmlns": "urn:\/\/x-artefacts-smev-gov-ru\/services\/message-exchange\/types\/basic\/1.2"         },         "s": [             {                 "n": "ns:Search",                 "s": [                     {                         "n": "ns:SearchNumber",                         "a": {                             "Number": "22RU006228DV"                         }                     }                 ]             }         ]     } ]

На самом деле конечно нас интересует только «Number»: «22RU006228DV», и в коде продакшена было бы while ($reader->name === ‘ns:SearchNumber’), ради наглядности я привёл получение кусочка побольше.

Замеры производительности

Прежде чем переписать свою библиотеку для работы с XML, я посмотрел что может предложить нам Open Source.

На Packagist под сотню пакетов, я посмотрел первые 30, посмотрел в исходники, везде примерно одно и тоже, но время работы отличалось, как позже оказалось это была ошибка моего бенчмарка.

На самом деле время везде примерно одно и тоже. С той разницей что функции работают быстрей статических методов, а статические методы работают быстрее методов экземпляра класса, а самые медленные это просто PHP скрипты.

За время разработки я перепробовал все 4 варианта, и один и тот же код работает в разных «форматах» с разницей в единицы микросекунд, если вам это важно, то пишите свой конвертор XML в процедурном стиле, выиграете пару микросекунд.

Мои замеры быстродействия:

91 mcs 200 ns \Mtownsend\XmlToArray\XmlToArray::convert() 82 mcs 0 ns xmlstr_to_array() 139 mcs 600 ns getNextElement 95 mcs 700 ns \SbWereWolf\XmlNavigator\Converter->prettyPrint 105 mcs 200 ns \SbWereWolf\XmlNavigator\FastXmlToArray::prettyPrint 107 mcs 0 ns \SbWereWolf\XmlNavigator\Converter->xmlStructure 91 mcs 900 ns \SbWereWolf\XmlNavigator\FastXmlToArray::convert

От запуска к запуску числа отличаются, но общая картина примерно такая.

Мой use case заключался в том, что бы 280 гигабайт XML файлов превратить в базу данных Федеральной Информационной Адресной системы (БД ФИАС).

Надеюсь теперь понятно почему меня волновало время парсинга XML файлов.

Что удивительно это то что 280 гигабайт XML файлов превратить в 190 гигабайт базы данных, такое чувство что у СУБД нет ни какой оптимизации, если из XML выкинуть имена элементов и имена атрибутов и всю разметку, то что там останется ? На мой вкус объём должен был сократиться в два раза хотя бы, но нет. 190 гигабайт это без индексов, с индексами наверное все 220 будут.

В первых версиях парсер жрал по 4 гига оперативы, грузил БД на 25% процессора, сколько процесс PHP отъедал оперативки я уже не помню. Вставка 100 000 записей занимала от 2 минут, чтение файла могло затянуться на 10 минут.

Последняя версия парсера ровно каждые 9 секунд вставляет очередные 100 0000 записей. PHP и БД каждый отъедают не больше 8 мегабайт оперативки и не больше 12% процессора.

Заключение

Если вам хочется попробовать этот парсер в деле, то вы можете установить пакет через Композер

composer require sbwerewolf/xml-navigator

Больше информации в README.md

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Чем вы парсите XML?
0% XML, что это? 0
50% XMLReader 1
0% SimpleXMLElement 0
50% DOMDocument 1
0% Сторонней библиотекой (пожалуйста напишите название в комментариях) 0
0% можно попробовать этот парсер 0
Проголосовали 2 пользователя. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/post/712106/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *