На работе попросили провести исследование какими средствами лучше разбирать объёмный XML файл (более 100Mb). Предлагаю сообществу ознакомиться с результатами.
Рассмотрим основные методы работы с XML:
1. Simple XML (documentation)
2. DOM (documentation)
3. xml_parser (SAX) (documentation)
4. XMLReader (documentation)
Simple XML
Минусы: работает очень медленно, собирает весь файл в память, дерево составляется в отдельных массив.
Плюсы: простота работы, работа «из коробки» (требует библиотеки libxml которая включена практически на всех серверах)
$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 с которым очень легко работать.
$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 - $publishern";
xml_parser и XMLReader.
Предыдущие 2 нам не подходят из-за работы с целым файлом, т.к. файлы у нас бывают по 20-30 Mb, и во время работы с ними некоторые блоки образуют цепочку (массив) в 100> Mb
Оба способа работают чтением файла построчно что подходит идеально для поставленной задачи.
Разница между xml_parser и XMLReader в том что, в первом случае вам нужно будет писать собственные функции которые будут реагировать на начало и конец тэга.
Проще говоря, xml_parser работает через 2 триггера – тэг открыт, тэг закрыт. Его не волнует что там идёт дальше, какие данные используются и т.д. Для работы вы задаёте 2 триггера указывающие на функции обработки.
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 отлично работает с аттрибутами тэгов.
<?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');
Тест производительности
<?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);
Результаты тестирования (чтение без разбора данных)
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. Советы и комментарии с удовольствием выслушаю. Прошу сильно не пинать
Автор: Sect0R