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

Тема в разделе "PHP", создана пользователем denik, 12 авг 2013.

Модераторы: latteo
  1. denik

    denik Постоялец

    Регистр.:
    1 июл 2011
    Сообщения:
    79
    Симпатии:
    43
    Добрый день.
    Пишу парсер больших 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 класс SimpleXMLIterator, однако как его связать с simplexml_load_file (экономя память) никак не могу понять.
    Подскажите...
     
  2. latteo

    latteo Эффективное использование PHP, MySQL

    Moderator
    Регистр.:
    28 фев 2008
    Сообщения:
    1.403
    Симпатии:
    1.183
    simplexml_load_file сразу распарсит весь файл в объект и, соответственно, в оперативку, foreach тут погоды уже не сделает.

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

    PHP:
    $file_name "zip://$file#xl/worksheets/sheet1.xml";
    $handle fopen($file_name"r");
    if (
    $handle) {
        while ((
    $buffer fgets($handle4096)) !== 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.
    Или вообще всю задачу парсингом строк решать, часто это даст выигрыш по времени парсинга.
     
    denik нравится это.
  3. BDSG

    BDSG

    Регистр.:
    28 фев 2009
    Сообщения:
    203
    Симпатии:
    109
    PHP:
    $xml simplexml_load_file$path_to_xml_file'SimpleXMLIterator' );
     
  4. denik

    denik Постоялец

    Регистр.:
    1 июл 2011
    Сообщения:
    79
    Симпатии:
    43
    ок, как потом получить допустим вторую строку $this->_XML->sheetData->row ?
     
  5. BDSG

    BDSG

    Регистр.:
    28 фев 2009
    Сообщения:
    203
    Симпатии:
    109
    Код:
    <?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$xmlRecursiveIteratorIterator::SELF_FIRST  );
     
    foreach( 
    $xml_iterator as $k => $v ){
     
        
    $val $xml_iterator->hasChildren() ? 'parent' : (string)$v;
     
        
    var_dump$k$val );
    }
     
    denik нравится это.
  6. denik

    denik Постоялец

    Регистр.:
    1 июл 2011
    Сообщения:
    79
    Симпатии:
    43
    ай спасибо тебе. Вот никак не мог догадаться что можно обращаться как к обычному массиву [$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 формат - стандартизирован и данные идут именно этими тегами.
     
  7. BDSG

    BDSG

    Регистр.:
    28 фев 2009
    Сообщения:
    203
    Симпатии:
    109
    сдается это не 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$xmlRecursiveIteratorIterator::SELF_FIRST  );
     
    foreach( 
    $xml->foo->bar as $v ){
     
        
    var_dump( (string)$v->baz );
    }
    зы.. вообще не видя картины полностью что-то советовать трудно.. вполне возможно есть ещё 100500 гораздо более эффективных решений..
     
  8. esche

    esche

    Регистр.:
    9 авг 2009
    Сообщения:
    360
    Симпатии:
    243
    Старый добрый SAX parser с возможностью навешивать обработчики на распарсенное (по мере получения данных) будет кушать гораздо меньше памяти.. Однако, "ручной" работы будет чуть больше
    Ещё можно XMLReader посмотреть http://php.net/manual/en/book.xmlreader.php