НЕ циклическая итерация SimpleXML (сохраняя ОЗУ)

denik

Постоялец
Регистрация
1 Июл 2011
Сообщения
80
Реакции
40
Добрый день.
Пишу парсер больших XLSX-файлов. Как всем известно, xlsx - это ZIP архив с xml файлами данных.

В общем требования - экономить память.

Открываю XML-файлы через поток "zip://" так:
PHP:
// Открываем основной WorkSheet #1
$this->_XML = simplexml_load_file("zip://$file#xl/worksheets/sheet1.xml");
все это большой проект, по этому привожу только самое важное.

Основной вопрос: как можно организовать итерацию по объекту XML БЕЗ foreach?
Т.е. есть решение такое:
PHP:
foreach ($this->_XML->sheetData->row as $item) {
                $out[$row] = array();
                //по каждой ячейке строки
                $cell = 0;
                foreach ($item as $child) {
                    $attr = $child->attributes();
                    $value = isset($child->v)? (string)$child->v:false;
                    $out[$row][$cell] = isset($attr['t']) ? $this->_XML_str[$value] : $value;
                    $cell++;
                }
                $row++;
  }
Но мне необходимо нечто вроде rewind(); current(); next();
Есть у PHP класс Для просмотра ссылки Войди или Зарегистрируйся, однако как его связать с simplexml_load_file (экономя память) никак не могу понять.
Подскажите...
 
simplexml_load_file сразу распарсит весь файл в объект и, соответственно, в оперативку, foreach тут погоды уже не сделает.

Будь xml отформатирован переносами для каждого элемента, можно было бы использовать:

PHP:
$file_name = "zip://$file#xl/worksheets/sheet1.xml";
$handle = fopen($file_name, "r");
if ($handle) {
    while (($buffer = fgets($handle, 4096)) !== false) {
        $xml_buffer = @simplexml_load_string($buffer);
		if($xml_buffer === false) {
		  //обработка ошибок
		} else {
                                    //логика скрипта
                                }

    }
    if (!feof($handle)) {
        echo "Error: unexpected fgets() fail\n";
    }
    fclose($handle);
}

В районе fgets можно выцеплять парсингом строк те xml-теги, что тебе нужны, и на эту строку натравливать simplexml_load_string.
Или вообще всю задачу парсингом строк решать, часто это даст выигрыш по времени парсинга.
 
PHP:
$xml = simplexml_load_file( $path_to_xml_file, 'SimpleXMLIterator' );
 
Код:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
    <foo>
        <bar>
            <baz>10</baz>
        </bar>
        <bar>
            <baz>20</baz>
        </bar>
    </foo>
</root>

PHP:
/** @var SimpleXMLIterator $xml */
$xml = simplexml_load_file( $path_to_xml_file, 'SimpleXMLIterator' );
var_dump( (string)$xml->foo->bar[1]->baz );

зы.. но это всё на самом деле лажа.. если надо целиком обойти все(!) узлы дерева, какое бы оно ни было, надо делать так:
PHP:
$xml = simplexml_load_file( $path_to_xml_file, 'SimpleXMLIterator' );
$xml_iterator = new RecursiveIteratorIterator( $xml, RecursiveIteratorIterator::SELF_FIRST  );
 
foreach( $xml_iterator as $k => $v ){
 
    $val = $xml_iterator->hasChildren() ? 'parent' : (string)$v;
 
    var_dump( $k, $val );
}
 
PHP:
$xml = simplexml_load_file( $path_to_xml_file, 'SimpleXMLIterator' );
var_dump( (string)$xml->foo->bar[B][1][/B]->baz );
ай спасибо тебе. Вот никак не мог догадаться что можно обращаться как к обычному массиву [$i]. Сделал такой цикл:
PHP:
$_xml = '
<root>
    <foo>
        <bar>
            <baz>10</baz>
        </bar>
        <bar>
            <baz>20</baz>
        </bar>
    </foo>
</root>';
 
/** @var SimpleXMLIterator $xml */
$xml = simplexml_load_string( $_xml, 'SimpleXMLIterator' );
 
$total = $xml->foo->bar->count();
$i = 0;
while( $i!=$total )
{
  var_dump( (string)$xml->foo->bar[$i++]->baz );
 
}

Однако, simplexml_load_file - как оказалось, действительно съедает память. Я в это не верил, потому что memory_get_peak_usage() - этого не показывал! (кстати, никто не знает - почему?).
В результате сделал парсинг по наводке latteo - использовать fopen. Открываю файл, далее ищу начало "<row>" и конец "</row>" - в цикле получаю строки и их уже парсю через simplexml_load_string (экономя память). Такое решение устраивает, потому, что XLSX формат - стандартизирован и данные идут именно этими тегами.
 
сдается это не simplexml_load_string память жрет, а инициализация объекта ($total = $xml->foo->bar), что, впрочем, логично - посчитать то как-то надо (count()).. т.е. весь смысл создания итератора помножен на ноль..

строго говоря итераторы как раз для экономии памяти и сделаны.. т.е. если обходить xml дерево итератором именно начиная с корня (фактически с указателя на корень), и по ключу ловить узлы, то память кушаться не будет (точнее будет, конечно, но только на хранение текущего указателя и текущего узла).. если при этом надо не копируя модифицировать значение узла, можно передавать его по ссылке ( foreach( $xml_iterator as $k => & $v ) ) - так тоже память будет расходоваться по-минимуму, т.к. работа производится с указателем..

в приведенном постом выше примере я бы поступил примерно так:
PHP:
$_xml = '
<root>
    <foo>
        <bar>
            <baz>10</baz>
        </bar>
        <bar>
            <baz>20</baz>
        </bar>
    </foo>
</root>';
 
/** @var SimpleXMLIterator $xml */
$xml = simplexml_load_string( $_xml, 'SimpleXMLIterator' );
$xml_iterator = new RecursiveIteratorIterator( $xml, RecursiveIteratorIterator::SELF_FIRST  );
 
foreach( $xml->foo->bar as $v ){
 
    var_dump( (string)$v->baz );
}

зы.. вообще не видя картины полностью что-то советовать трудно.. вполне возможно есть ещё 100500 гораздо более эффективных решений..
 
Старый добрый Для просмотра ссылки Войди или Зарегистрируйся с возможностью навешивать обработчики на распарсенное (по мере получения данных) будет кушать гораздо меньше памяти.. Однако, "ручной" работы будет чуть больше
Ещё можно XMLReader посмотреть Для просмотра ссылки Войди или Зарегистрируйся
 
Назад
Сверху