Методы работы с «тяжёлыми» XML

    image

    На работе попросили провести исследование какими средствами лучше разбирать объёмный XML файл (более 100Mb). Предлагаю сообществу ознакомиться с результатами.

    Рассмотрим основные методы работы с XML:

    1. Simple XML (documentation)
    2. DOM (documentation)
    3. xml_parser (SAX) (documentation)
    4. XMLReader (documentation)

    Simple XML


    Минусы: работает очень медленно, собирает весь файл в память, дерево составляется в отдельных массив.
    Плюсы: простота работы, работа «из коробки» (требует библиотеки libxml которая включена практически на всех серверах)

    Пример использования Simple XML
    $xml = simplexml_load_file("price.xml");
    echo "<table border='1'>\n";
    
    foreach ($xml->xpath('/DocumentElement/price') as $producs) { ?> 
        <tr>
            <td><?php echo $producs->name; ?></td>
            <td><?php echo $producs->company; ?></td>
            <td><?php echo $producs->city; ?></td>
            <td><?php echo $producs->amount ?></td>
        </tr> 
    <?
    }
    echo "</table>\n";


    DOM


    Минусы: работает очень медленно, как и все предыдущие примеры собирает весь файл в память.
    Плюсы: На выходе привычный DOM с которым очень легко работать.

    Пример использования DOM
    $doc = new DOMDocument();
    $doc->load( 'books.xml' );
     
    $books = $doc->getElementsByTagName( "book" );
    foreach( $books as $book )
    {
    $authors = $book->getElementsByTagName( "author" );
    $author = $authors->item(0)->nodeValue;
     
    $publishers = $book->getElementsByTagName( "publisher" );
    $publisher = $publishers->item(0)->nodeValue;
     
    $titles = $book->getElementsByTagName( "title" );
    $title = $titles->item(0)->nodeValue;
     
    echo "$title - $author - $publisher\n";


    xml_parser и XMLReader.


    Предыдущие 2 нам не подходят из-за работы с целым файлом, т.к. файлы у нас бывают по 20-30 Mb, и во время работы с ними некоторые блоки образуют цепочку (массив) в 100> Mb

    Оба способа работают чтением файла построчно что подходит идеально для поставленной задачи.

    Разница между xml_parser и XMLReader в том что, в первом случае вам нужно будет писать собственные функции которые будут реагировать на начало и конец тэга.

    Проще говоря, xml_parser работает через 2 триггера – тэг открыт, тэг закрыт. Его не волнует что там идёт дальше, какие данные используются и т.д. Для работы вы задаёте 2 триггера указывающие на функции обработки.

    Пример работы xml_parser
    class Simple_Parser 
    {
        var $parser;
        var $error_code;
        var $error_string;
        var $current_line;
        var $current_column;
        var $data = array();
        var $datas = array();
        
        function parse($data)
        {
            $this->parser = xml_parser_create('UTF-8');
            xml_set_object($this->parser, $this);
            xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
            xml_set_element_handler($this->parser, 'tag_open', 'tag_close');
            xml_set_character_data_handler($this->parser, 'cdata');
            if (!xml_parse($this->parser, $data))
            {
                $this->data = array();
                $this->error_code = xml_get_error_code($this->parser);
                $this->error_string = xml_error_string($this->error_code);
                $this->current_line = xml_get_current_line_number($this->parser);
                $this->current_column = xml_get_current_column_number($this->parser);
            }
            else
            {
                $this->data = $this->data['child'];
            }
            xml_parser_free($this->parser);
        }
    
        function tag_open($parser, $tag, $attribs)
        {
            $this->data['child'][$tag][] = array('data' => '', 'attribs' => $attribs, 'child' => array());
            $this->datas[] =& $this->data;
            $this->data =& $this->data['child'][$tag][count($this->data['child'][$tag])-1];
        }
    
        function cdata($parser, $cdata)
        {
            $this->data['data'] .= $cdata;
        }
    
        function tag_close($parser, $tag)
        {
            $this->data =& $this->datas[count($this->datas)-1];
            array_pop($this->datas);
        }
    }
    
    $xml_parser = new Simple_Parser;
    $xml_parser->parse('<foo><bar>test</bar></foo>');


    В XMLReader всё проще. Во первых, это класс. Все триггеры уже заданы константами (их всего 17), чтение осуществляется функцией read() которая читает первое вхождение подходящее под заданные триггеры. Далее мы получаем объект в который заносится тип данных (аля триггер), название тэга, его значение. Также XMLReader отлично работает с аттрибутами тэгов.

    Пример использования XMLReader
    
    <?php
    <?php
    Class StoreXMLReader
    {
    	
    	private $reader;
    	private $tag;
    	
    	// if $ignoreDepth == 1 then will parse just first level, else parse 2th level too
    	
    	private function parseBlock($name, $ignoreDepth = 1) {
    		if ($this->reader->name == $name && $this->reader->nodeType == XMLReader::ELEMENT) {
    			$result = array();
    			while (!($this->reader->name == $name && $this->reader->nodeType == XMLReader::END_ELEMENT)) {
    				//echo $this->reader->name. ' - '.$this->reader->nodeType." - ".$this->reader->depth."\n";
    				switch ($this->reader->nodeType) {
    					case 1:
    						if ($this->reader->depth > 3 && !$ignoreDepth) {
    							$result[$nodeName] = (isset($result[$nodeName]) ? $result[$nodeName] : array());
    							while (!($this->reader->name == $nodeName && $this->reader->nodeType == XMLReader::END_ELEMENT)) {
    								$resultSubBlock = $this->parseBlock($this->reader->name, 1);
    								
    								if (!empty($resultSubBlock))
    									$result[$nodeName][] = $resultSubBlock;
    								
    								unset($resultSubBlock);
    								$this->reader->read();
    							}
    						}
    						$nodeName = $this->reader->name;
    						if ($this->reader->hasAttributes) {
    							$attributeCount = $this->reader->attributeCount;
    							
    							for ($i = 0; $i < $attributeCount; $i++) {
    								$this->reader->moveToAttributeNo($i);
    								$result['attr'][$this->reader->name] = $this->reader->value;
    							}
    							$this->reader->moveToElement();
    						}
    						break;
    					
    					case 3:
    					case 4:
    						$result[$nodeName] = $this->reader->value;
    						$this->reader->read();
    						break;
    				}
    				
    				$this->reader->read();
    			}
    			return $result;
    		}
    	}
    
    	public function parse($filename) {
    		
    		if (!$filename) return array();
    		
    		$this->reader = new XMLReader();
    		$this->reader->open($filename);
    		
    		// begin read XML
    		while ($this->reader->read()) {
    			
    			if ($this->reader->name == 'store_categories') {
    			// while not found end tag read blocks
    			while (!($this->reader->name == 'store_categories' && $this->reader->nodeType == XMLReader::END_ELEMENT)) {
    				$store_category = $this->parseBlock('store_category');
    				
    				/*
    					Do some code
    				*/
    				
    				$this->reader->read();
    			}
    			
    			$this->reader->read();
    		}
    			
    		} // while
    	} // func
    }
    
    $xmlr = new StoreXMLReader();
    $r = $xmlr->parse('example.xml');
    


    Тест производительности


    Код генератора example.xml
    <?php
    $xmlWriter = new XMLWriter();
    $xmlWriter->openMemory();
    $xmlWriter->startDocument('1.0', 'UTF-8');
    $xmlWriter->startElement('shop');
    for ($i=0; $i<=1000000; ++$i) {
        $productId = uniqid();
    
        $xmlWriter->startElement('product');
        $xmlWriter->writeElement('id', $productId);
        $xmlWriter->writeElement('name', 'Some product name. ID:' . $productId);
        $xmlWriter->endElement();
        // Flush XML in memory to file every 1000 iterations
        if (0 == $i%1000) {
            file_put_contents('example.xml', $xmlWriter->flush(true), FILE_APPEND);
        }
    }
    $xmlWriter->endElement();
    // Final flush to make sure we haven't missed anything
    file_put_contents('example.xml', $xmlWriter->flush(true), FILE_APPEND);


    Результаты тестирования (чтение без разбора данных)

    Характеристики тестовой среды
    Ubuntu 16.04.1 LTS
    PHP 7.0.15
    Intel® Core(TM) i5-3550 CPU @ 3.30GHz, 16 Gb RAM, 256 SSD

    Метод Время выполнения (19 Mb) Время выполнения (190 Mb)
    Simple XML 0.46 сек 4.56 сек
    DOM 0.52 сек 4.09 сек
    xml_parse 0.22 сек 2.25 сек
    XML Reader 0.26 сек 2.18 сек

    P.S. Советы и комментарии с удовольствием выслушаю. Прошу сильно не пинать
    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 53

      +4
      Ваш тест производительности опровергает ваши же утверждения о медлительности DOM и SimpleXML по сравнению с XMLReader. И еще бы не плохо показать объем используемой памяти.
        0
        да, последнюю цифру не правильно скопировал.
        ок, добавлю данные по используемой памяти
          0
          тогда было бы неплохо и на CPU глянуть.
        –10
        какими средствами лучше разбирать объёмный XML

        Во первых надо отказаться от формата и переходить на json в любом случае.
        Во вторых получать ленту со стороны сервера уже обработанную, по параметрам.
        ?part=snippet,contentDetails&maxResults=15&order=date
          +2
          К сожалению наши поставщики прайс-листов не хотят переходить на параметры и отдают нам XML от 500 до 1500 Mb.
            –1
            В принципе даже simplexml_load_file можете разбить на блоки, как вот товарищ пишет, он через curl/
            Но вот как обрабатывает ошибки json.

            Есть же какие то программы, которые конвертируют файл на стороне клиента, только конвертация уменьшит его размер на треть.
            $json = curl_exec($ch);
              if ($json !== false) { 
            //решаем проблему ошибок
            $json = preg_replace("#(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|([\s\t]//.*)|(^//.*)#", '', $json);
            setlocale(LC_ALL, 'ru_RU.utf8');
             Header("Content-Type: text/html;charset=UTF-8");
             curl_close($ch);
             
            $json = json_decode($json, true) ; 
            

              0
              Не так давно пришлось изменять целый модуль после старого программиста, именно из-за его идеи разбивать файл на кучу новых по 30 Mb и разбирать всё это дело через simplexml. Такой утечки памяти добился он что сервер не мог выдержать 100 человек онлайн.
                +1
                Можно парсить на сущности (теги), используя XMLReader, а затем эту сущность разбирать уже через DomDocument. Таким образом, памяти будет кушать не много, а DomDocument куда более приятен и расширяем, хотя, конечно, по скорости будет чуть медленнее.

                А вообще, такие парсеры лучше писать на более шустрых комилируемых языках, к примеру go. Там это в разы будет быстрее работать и можно использовать конкурентные вычисления, писать порциями напрямую в базу. Вы удивитесь насколько это будет шустрее работать и памяти жрать будет в разы меньше
                  –1
                  Ну так я и пишу, в принципе, если очень надо. XMLлом собаку уже съел, искренне не понимаю, кто наминусовал за «Во первых надо отказаться от формата и переходить на json в любом случае.».

                  Какие то странные кадры пошли в хабрахабре.

                  curl тут прописал, ибо раз 5 спрашивали в тостере как сделать чтобы кодировка была русская и ошибок не было. Им удобно смотреть и разбирать.

              • UFO just landed and posted this here
                  +1
                  :) берём что дают
                  • UFO just landed and posted this here
                    0
                    Возможно тем, что поставщики его не предоставляют?
                  +1
                  А как можно обработать большой json (к примеру > 100 мб)? Если вот нужно, и никак иначе? Буду признателен за ответ
                    0

                    В java как-то помнится тупо использовал gson.

                      +1
                      <старая шутка про java и память/>
                      0
                      использовать streaming json parser
                      Например: https://www.mkyong.com/java/jackson-streaming-api-to-read-and-write-json/
                        0
                        Если JSON вида [{}, {}, {}, ...] или похожий, где структура несложная, но есть большой кусок, который надо обрабатывать потоком, можно сделать так. Читаем посимвольно с учетом вложенности скобок, то есть определяем ситуации «начало массива», «конец массива», «начало объекта», «конец объекта». Задаем уровень вложенности или ключ, которые определяют одну единицу данных. Читаем текст для этой единицы в буфер, буфер передаем в json_decode(). Делаем обработку результата, читаем дальше.
                          0
                          И снова гуглинг по словам «JSON stream php» может навести на мысль что велосипедить — это тратить время.
                      +3

                      Помимо времени выполнения следовало бы замерить использованную память.

                        +2
                        У меня тоже есть поставщики фидов по 2-4 gb на xml

                        Мне пришлось написать безумные вещь для этой задачи.

                        — Открываю файл и читаю его построчно, пропуская открытие и закрытие, беру около 300 000 строк
                        — При помощи CURL рассылаю строки на эту же машину + на удаленные машины (24 запроса = 300 000/24)
                        — Все 24 запроса конвертируют XML в key/value массив
                        — Система собирает с каждого curl один массив

                        И так по кругу пока не кончится файл

                        Из сложного — надо правильно открывать и закрывать xml, чтобы он был валидный.

                        PS.
                        24 — это не сервера, это запросы, сервернов 4 по 6 (6 ядрен * на 4 офисные машины)

                        Проверьте htop, ваш php для этой задачи берет скорее всего только 1 ядро, заставить брать все ядра можно мультизапросами.
                          0
                          Уточняю

                          — Все 24 запрсоа не только конвертируют XML -> В массив, естественно они сначала конвертируют строки в XML объект. Но так как по XML объект ходить сложно (особенно, когда структура постоянно разная), то я написал функцию конвертации всех параметрво и аттрибутов в key/value массив, который возращается как json

                          — 24, 300 000 — все эти цифры условные, постоянно перенастраиваются и тд
                            0
                            Почитайте мой верхний комментарий.
                            Из-за таких способов происходит дикая утечка памяти, т.к. вы грузите по 300 000 строк, в то время когда воспользовавшись XMLReader или xml_parser вы будете тратить максимум 1 мегабайт памяти.
                            P.S. на разбор файла в 1.5 Gb уходит 7 Mb памяти и около 1.5 часа. Это учитывая что кроме разбора файла идёт множество mysql запросов
                              +4
                              За что минусуют не могу понять, у меня на разбор уходет от 5 до 15 минут на 7gb xml данных, а у вас 1.5 часа на 1.5

                              Дикая утечка памяти? Авто же жаловался на скорость, зачем мне экономить память, и это не утечка, а использование. Моя задача скорость и я разбил файл на 300 000 кусков и разослал на обработку 6 серверам всем их ядрам под 100%.

                              Зачем это делать на сервере с онлайн пользователями — я не понимаю )

                              XMLReader или xml_parser — я жду их выполнения, что мне толку от их потребления памяти, памяти в избытке, а 300 000 строк по кругу не позволяют ей выйти из того объема, который меня устраивает. Если их же вызвать 24 раза одновременно — будет в 24 раза быстрее, а 300 000/24 — парсер не каждую строчку конвертирует, а все строчки как 1 файл на ядре.
                                0

                                А завтра поменяют разбиение XML-файла на строки и все у вас упадет.

                                  0
                                  Нет, я не конвертирую строки, я на 1 ядре объединяю их и делаю из них валидный xml, потому не важно как именно разбит файл, идея считывать группу строк, а не весь файл целиком.

                                  Все упадет если файл будет 1 строковый. Ну под это тоже можно написать альтернативу, но по задаче не требуется.
                                    0

                                    А как вы будете разбивать файл вот такого формата?


                                    <root><item>
                                        <name>Item 1</name>
                                    </item><item>
                                        <name>Item 2</name>
                                    </item><item>
                                        <name>Item 3</name>
                                    </item><item>
                                        <name>Item 4</name>
                                    </item><item>
                                        <name>Item 5</name>
                                    </item></root>
                                      0
                                      По строкам, а потом допишу в разрыве xml, чтобы превратить в валидный, чтобы не потерять данные, следующий цикл начнется на пару строк выше разрыва, есть вероятность повтороного попадания данных (дубли), но они пропускаются при импорте

                                      Нужно просто очистить хвост, чтобы в конце остался только закрывающийся тег
                                        0
                                        А завтра поменяют разбиение XML-файла на строки и все у вас упадет.
                                          +1
                                          Вы это уже говорили, я не знаю как бы вам внятно объяснить. Есть открывающие и закрывающие теги, какая разница что там со строками и где что лежит, после получения данных для одного ядра строки соединяются и проверяются на валидность, все обрывистые куски отсеиваются и закрываются те теги, которые нужны, затем передается в xml reader
                                            0

                                            Как вы собираетесь отслеживать закрывающие тэги </item> второго уровня в файле произвольного формата?


                                            Да вы сами признали, что костыль, изготовленный для конкретного формата файла, упадет если в файле вся информация окажется в одну строку.

                                  +1
                                  Это время и память на работу на 1 сервере.
                                  По факту вы делаете кучу лишней работы которая не нужна.
                                  Возможно конкретно в вашем случае это помогло решить какие-то другие задачи, но в целом даже на обработке 300 000 строк я бы использовал xml_reader, а не simplexml
                                    0
                                    Ну это моя проблема выбора, конвертора, я лишь описал как я ускорил, если xml_reader лучше, то в данном случае он тоже отработает хорошо.

                                    –1
                                    Да, заметил, набежит группа и минусует одновременно. Выбивают в других комментариях, а не по теме срача, кто хоть что то знает, а остаются полит. психи…
                                    Скоро будет жежечка вторая.

                                    Может нервничают от старого железа или перехода на PHP7, где SimpleXML extension были проблемы с установкой.

                                    99% будет ошибка где то в XML, особо при таких гигантских файлах, не пойму как они работают.

                                    –1
                                    Долго. Хотя, если такое время выполнения никому неудобств не доставляет — ну ладно.
                                    Но, в любом случае, насилие БД… Вы при разборе каждого узла делаете запросы? У вас при разборе файла какие запросы превалируют: INSERT или UPDATE? Что, если собирать данные и отправлять их в БД пачками (bulk insert), т.е., вместо 100/200/500 запросов сделать 1? Или возможно, если вы используете innodb и у вас autocommit=1 (по умолчанию), а вы и не знали?
                                      0
                                      Оптимизация базы проведена.
                                      Используются транзакции.
                                      Большие запросы вынесены в отдельный файл который работает с очередью RabbitMQ
                                +1
                                Была задача распарсить многогиговые XML файлы, делал очень просто, зная структуру XML файла читал построчно, последовательно заполняя и обрабатывая объектную модель. При незнакомом формате, можно в два прохода, на первом строит схему XML, на втором уже зная ее обрабатываем последовательно блоки.
                                  –6
                                  А что, регулярками уже никто не парсит? :)
                                  Кстати, вполне вариант для файла вида
                                  <root><item>
                                      <name>Item 1</name>
                                  </item> ... </root>
                                  

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

                                  Еще придумался такой способ (не для всех типов данных) — конвертировать снаружи XML в нечто табличное (CSV / TSV), через XSLT, а затем вычитывать построчно (или тот же MySQL может такое съесть через LOAD DATA INFILE). Да, колонок будет овер много в общем случае. Конвертировать можно например не в файл, а в пайп, чтоб место поберечь на диске.
                                    0
                                    Ну почему же никто :)
                                    Тоже была задача в своё время распарсить xml на сотню-другую мегабайт. При этом структура была заранее известна и размер item-а был вменяемый, не больше сотен килобайт.
                                    Комбинация регулярок с построчным чтением файла вполне спасала от перерасхода памяти.
                                    +2
                                    Читаю и ужасаюсь — чего только не придумают люди, у которых нет StAX
                                      0
                                      Хм, а у меня xml_parse был быстрее на порядок. :)
                                        0
                                        Уф… XML в несколько гигов это жесть)) Мне пока максимум доводилось работать с 30-35Мб, использую Simple XML и пару раз DOM за карьеру. Про другие библиотеки не знал, спасибо.
                                          0
                                          Вы не пробовали использовать xslt преобразования? С их помощью из многомегабайтного XML документа можно вычленить только интересующую вас информацию, а уже её зачитывать в сущности.
                                            0
                                            У нас вся интересующая информация в xml )
                                            Именно весь xml и нужен был.
                                            +1
                                            По запросу «php stream xml» вроде бы в первых же результатах вылезают ссылки на XMLReader, SAX и другие полезные слова?
                                            (это коммент не к статье, а к товарищам, которые велосипедят)
                                              0
                                              но почему-то до сих пор 80% <=middle разработчиков используют simplexml )
                                                0
                                                С каких пор использование simplexml — вилосипедописание?
                                                  0
                                                  велосипедописание если идет работа с файлом более 50 Mb
                                              • UFO just landed and posted this here
                                                • UFO just landed and posted this here
                                                  0
                                                  Как насчет использования eXist? Он как раз и создавался для удобства в оперировании большими XML файлами.
                                                    0
                                                    Используйте SAX парсер, но не используйте его главную фишку — парсить в потоке?
                                                    У вас поэтому такая смешная разница между DOM и SAX.
                                                    Этот парсер легко переваривает многогиговые эксельки (любой zip архив с xml), которые офис не в состоянии открыть.
                                                      –1
                                                      https://www.sitepoint.com/functional-programming-phunkie-building-php-json-parser/
                                                      — A JSON Parser.
                                                      По опыту, большее количество кода это обнаружение и устранение ошибок, вот как работает по новому. См. выше.

                                                      Only users with full accounts can post comments. Log in, please.