Некоторое время назад была задача разбора гигабайтных размеров XML-файлов на PHP. Попробуем поискать нужные библиотеки:
SimpleXML
Первая мысль воспользоваться SimpleXML, однако её принцип работы основан на чтении всего файла целиком и разворачивании DOM-дерева в памяти. Очевидно такой код упрется в memory_limit.
DOM
Ситуация по использованию памяти такая же как и SimpleXML.
XML-анализатор
Это SAX-парсер — работает очень быстро, принцип прост и основан на «событиях», анализатор лишь сообщает «открылся элемент» либо «закрылся элемент». Однако за это придется платить, например для моей задачи пришлось бы написать некий стек, т.е. следить за вложенностью элементов, открытием/закрытием узлов и атрибутов. Из минусов еще библиотека достаточно старая и написана в процедурном стиле, также данное pecl-расширение редко встречается на хостингах.
XMLReader
Аналогично SAX-парсер, однако API удобнее и проще чем у XML-анализатора, вся логика сконцентрирована в одном классе. Расширение входит в стандартную поставку PHP и часто встречается на хостингах.
В итоге скрестил XMLReader и SimpleXML — получился SimpleXMLReader: репозиторий на github, пример использования.
Спасибо. 🙂 Сейчас попробую.
и как вам?
Отлично! Правда пытаюсь отловить один баг. Почему-то после парсинга 100 объетов из большого xml-файла мой локальный сервер продолжает грузить проц. Не сталкивались?
Предположение — посмотрите код функции parse: while ($continue && $this->read()). Т.е. файл обрабатывается до самого конца всегда, что и создает нагрузку.
Не вижу в этом ничего плохого, но если необходима оптимизация — посмотрите на функцию XMLReader::close
Спасибо за полезный класс. Сравнил по скорости с http://webi.ru/webi_articles/big_xml.html — Ваш способ работает быстрее. Причем на порядок. Единственное, что упущено из вида — это вывод ошибок валидации xml. Если дать вашему парсеру на вход невалидный xml, то работа просто прервется, причем понять, что ошибка заключается в невалидном теге xml и тем более в какоq именно строке и символе не представляется возможным. Пробовал допилить, но пока так и не пришел к рабочему варианту. Поможете?
не тестировал на предмет валидности, хотя да — надо предусмотреть какие то внутренние механизмы обработки ошибок.
Решил проблему. Если кому-то будет полезно:
$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;
}
Хотел поробовать, не разобрался что к чему:(
Спасибо! Помогла ваша библиотека!
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 не работает.
Олег, 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
Класс SimpleXmlReader ничего нового не придумывает, только расширяется стандартные XMLReader до более удобного API.
Например поддержка простых XPath-запросов (работает только имена через слэш), а «/yml_catalog/shop» — работать будет быстрее т.к каждый элемент shop имеет малый размер, следовательно требует меньше памяти для вызова expand
Про чтение корневого узла
$parser->registerCallback('yml_catalog', function($render){
$reader->getAttribute('date'); // так можно
$reader-> expandSimpleXml(); // а так скушает памяти много
});
Насчет xml_parse это все тот же XMLReader — функции вместо обьектно ориентированого подхода организации кода, и самое важно без expand — вам придется в ручном режиме контролировать закрытие тегов.
Чтож, сработало. Спасибо. Теперь задумываюсь о том как заставить его бить файлы на чанки и парсить паралельно в несколько потоков. Есть мысли?:)
Дмитрий, спасибо за скрипт, работает хорошо. Только один вопрос — есть ли в планах выложить его на Packagist.org, чтобы можно было ставить через composer? Часто не очень удобно добавлять библиотеки в репозиторий проекта, гораздо проще добавить зависимость в composer.json
Добавил composer.json: https://packagist.org/packages/dkrnl/simplexmlreader
От вас жду лайков репозитория (:
Не справляется на файле в 450 мб. Процесс убивается. Выгрузка 1С.
Если. я сброшу xml-ник, сможете подсказать в чем проблема?
Большое потребление памяти напрямую связано с размером под-дерева которому Вы делаете expand.
https://github.com/dkrnl/SimpleXMLReader/issues/new — скидывайте суда xml + краткий php, разберемся.
Добрый день!
Подскажите, пожалуйста, как можно обратиться к атрибуту родительского элемента? Причем обращение может быть не к первому родителю а к N-ому при N вложенности элемента
Добрый!
Нужен пример, но вероятно это можно сделать через XPATH — вроде «//нужный-узел/ancestor::*».
А как выводить содержимое к примеру СОДЕРЖИМОЕ
Именно только само содержимое, без атрибутов.
Если можно пример кода скинуть, очень нужно, заранее благодарю.
Вырезались теги
«СОДЕРЖИМОЕ» обернуто в тег Остатки
незнаю без кода, но вероятно экземляр класса SimpleXMLElement просто привести к строке:
$xml = $reader->expandSimpleXml();
echo (string)$xml->{«СОДЕРЖИМОЕ»}
Но уверен что var_dump($xml) и чтение официальной документации по SimpleXMLElement поможет больше.
Так даже с этим классом упираешься в лимит 30 секунд… Как это обойти?