Простая обработка больших XML файлов

Некоторое время назад была задача разбора гигабайтных размеров XML-файлов на PHP. Попробуем поискать нужные библиотеки:

SimpleXML
Первая мысль воспользоваться SimpleXML, однако её принцип работы основан на чтении всего файла целиком и разворачивании DOM-дерева в памяти. Очевидно такой код упрется в memory_limit.

DOM
Ситуация по использованию памяти такая же как и SimpleXML.

XML-анализатор
Это SAX-парсер — работает очень быстро, принцип прост и основан на «событиях», анализатор лишь сообщает «открылся элемент» либо «закрылся элемент». Однако за это придется платить, например для моей задачи пришлось бы написать некий стек, т.е. следить за вложенностью элементов, открытием/закрытием узлов и атрибутов. Из минусов еще библиотека достаточно старая и написана в процедурном стиле, также данное pecl-расширение редко встречается на хостингах.

XMLReader
Аналогично SAX-парсер, однако API удобнее и проще чем у XML-анализатора, вся логика сконцентрирована в одном классе. Расширение входит в стандартную поставку PHP и часто встречается на хостингах.

В итоге скрестил XMLReader и SimpleXML — получился SimpleXMLReader: репозиторий на github, пример использования.

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

24 комментария на «Простая обработка больших XML файлов»

  1. Антон говорит:

    Спасибо. 🙂 Сейчас попробую.

  2. Oak говорит:

    Отлично! Правда пытаюсь отловить один баг. Почему-то после парсинга 100 объетов из большого xml-файла мой локальный сервер продолжает грузить проц. Не сталкивались?

    • dkrnl говорит:

      Предположение — посмотрите код функции parse: while ($continue && $this->read()). Т.е. файл обрабатывается до самого конца всегда, что и создает нагрузку.
      Не вижу в этом ничего плохого, но если необходима оптимизация — посмотрите на функцию XMLReader::close

  3. Александр говорит:

    Спасибо за полезный класс. Сравнил по скорости с http://webi.ru/webi_articles/big_xml.html — Ваш способ работает быстрее. Причем на порядок. Единственное, что упущено из вида — это вывод ошибок валидации xml. Если дать вашему парсеру на вход невалидный xml, то работа просто прервется, причем понять, что ошибка заключается в невалидном теге xml и тем более в какоq именно строке и символе не представляется возможным. Пробовал допилить, но пока так и не пришел к рабочему варианту. Поможете?

    • dkrnl говорит:

      не тестировал на предмет валидности, хотя да — надо предусмотреть какие то внутренние механизмы обработки ошибок.

  4. Александр говорит:

    Решил проблему. Если кому-то будет полезно:


    $file = "example1.xml";
    $reader = new ExampleXmlReader1();

    libxml_clear_errors();
    libxml_use_internal_errors(true);

    $reader->open($file);
    $reader->parse();
    $arrayErrors = libxml_get_errors();
    $reader->close();

    $xml_errors = "";
    foreach ($arrayErrors as $xmlError) $xml_errors .= $xmlError->message;
    if ($xml_errors != "") {
    echo "XML not valid: ".$xml_errors;
    }

  5. Freeze говорит:

    Хотел поробовать, не разобрался что к чему:(

  6. Борис говорит:

    Спасибо! Помогла ваша библиотека!

  7. Олег говорит:


    set_time_limit(0);
    error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED);

    class parse_xml extends SimpleXMLReader {
    public function __construct() {}

    public function getAll() {
    $xml = $this->expandSimpleXml();
    $attr = (array)$xml->attributes();
    $attr = $attr['@attributes'];
    $value = (string)$xml;
    return array($xml, $value, $attr);
    }
    }

    $parser = new parse_xml();
    $parser->registerCallback('yml_catalog', function($render){
    list(,, $attr) = $render->getAll();

    $this->ozon_update = $attr['date'];
    return true;
    });
    $parser->registerCallback('offer', function($render){
    list($item, $value, $attr) = $render->getAll();
    $ozon_title = parse_filter($item->name);
    });
    $parser->open("./ozon_book.xml");
    $parser->parse();
    $parser->close();

    Поделюсь своими наблюдениями. При таком конфиге процесс запущенный в консоли умирает где-то через 2-3 минуты. Умирает как на 200мегабайтном так и на 2 гигабайтном файле примерно одинаково. На значительно меньших файлах работает шустро и удобно. Колбеки понравились.

    $ time php ./parse_ozon.php parse
    Убито

    real 2m19.644s
    user 0m18.802s
    sys 0m2.649s

    Умирает без отработки колбеков, значит модель SAX не работает.

    • dkrnl говорит:

      Олег, sax работает, просто ошибочное использование библиотеки.

      Насколько я понял yml_catalog — это корневой узел, и действительно в память DOM-дерево будет полностью, вот почему:
      1. Sax в цикле ждет открывающий тег с именем yml_catalog.
      2. Вызывается callback, в котором вызывается $reader->expandSimpleXml();
      3. А expandSimpleXml это обвертка над http://php.net/manual/ru/xmlreader.expand.php, которая будет перемещать курсор пока не встретит закрывающий тег yml_catalog.
      4. Как только встретится закрытие yml_catalog — создается DOM-объект всего файла.

      Если необходимо читать атрибуты корневого тега, не вызывайте expand-функций, лучше так:
      $reader->getAttribute("date")

      Для обработки всех вложенных узлов верным будет использовать примерно так:
      $parser->registerCallback('/yml_catalog/shop', function($render){ });

      • Олег говорит:

        Спасибо за комментарий.

        А почему /yml_catalog/shop будет более верным вариантом? Как это спасёт от всё тех же операций по проходу всего файла ведь он из одних шопов по сути и состоит.

        $reader->getAttribute(«date») — дельное замечание, похоже в этом всё дело, однако вызвать его до колбека не удалось без запуска парсера, а в колбеке он не работает.

        Сейчас переписываю вот это для более удобного рабочего вида: http://webi.ru/webi_articles/big_xml.html

        • dkrnl говорит:

          Класс SimpleXmlReader ничего нового не придумывает, только расширяется стандартные XMLReader до более удобного API.

          Например поддержка простых XPath-запросов (работает только имена через слэш), а «/yml_catalog/shop» — работать будет быстрее т.к каждый элемент shop имеет малый размер, следовательно требует меньше памяти для вызова expand

          Про чтение корневого узла

          $parser->registerCallback('yml_catalog', function($render){
          $reader->getAttribute('date'); // так можно
          $reader-> expandSimpleXml(); // а так скушает памяти много
          });

          Насчет xml_parse это все тот же XMLReader — функции вместо обьектно ориентированого подхода организации кода, и самое важно без expand — вам придется в ручном режиме контролировать закрытие тегов.

          • Олег говорит:

            Чтож, сработало. Спасибо. Теперь задумываюсь о том как заставить его бить файлы на чанки и парсить паралельно в несколько потоков. Есть мысли?:)

  8. Антон говорит:

    Дмитрий, спасибо за скрипт, работает хорошо. Только один вопрос — есть ли в планах выложить его на Packagist.org, чтобы можно было ставить через composer? Часто не очень удобно добавлять библиотеки в репозиторий проекта, гораздо проще добавить зависимость в composer.json

  9. Max говорит:

    Не справляется на файле в 450 мб. Процесс убивается. Выгрузка 1С.
    Если. я сброшу xml-ник, сможете подсказать в чем проблема?

  10. Константин говорит:

    Добрый день!
    Подскажите, пожалуйста, как можно обратиться к атрибуту родительского элемента? Причем обращение может быть не к первому родителю а к N-ому при N вложенности элемента

    • dkrnl говорит:

      Добрый!
      Нужен пример, но вероятно это можно сделать через XPATH — вроде «//нужный-узел/ancestor::*».

  11. Петр говорит:

    А как выводить содержимое к примеру СОДЕРЖИМОЕ

    Именно только само содержимое, без атрибутов.
    Если можно пример кода скинуть, очень нужно, заранее благодарю.

    • Петр говорит:

      Вырезались теги

      «СОДЕРЖИМОЕ» обернуто в тег Остатки

    • dkrnl говорит:

      незнаю без кода, но вероятно экземляр класса SimpleXMLElement просто привести к строке:
      $xml = $reader->expandSimpleXml();
      echo (string)$xml->{«СОДЕРЖИМОЕ»}
      Но уверен что var_dump($xml) и чтение официальной документации по SimpleXMLElement поможет больше.

  12. Виталий говорит:

    Так даже с этим классом упираешься в лимит 30 секунд… Как это обойти?

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

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