На работе попросили провести исследование какими средствами лучше разбирать объёмный XML файл.
Все знают что конечно же это XMLReader, но вдруг кто-то и не знает :)


В итоге получили множество вариантов:
1. Domit XML Class
2. Simple XML
3. DOM
4. xml_parser (SAX)
5. XMLReader


Domit XML Class

Минусы: работает очень медленно, собирает весь файл в память, дерево составляется в отдельных массив ( в итоге ошибка allowed memory size неизбежна)

Плюсы: распространённость (множество CMS, включая Joomle <= 2.5 включают его в набор), простота работы (на выходе объект)

Simple XML
Минусы: работает сбором всего файла (см. domit)
Плюсы: поддержка php 4

DOM
Минусы: см. simple xml
Плюсы: всем привычный DOM J

Предыдущие 3 нам не подходят из-за работы с целым файлом, т.к. файлы у нас бывают по 20-30 Mb, и во время работы с ними некоторые блоки образуют цепочку (массив) в 100> Mb

Теперь остановимся на следующих: xml_parser и XMLReader.

Оба способа работают чтением файла построчно что подходит идеально для поставленной задачи.

Разница между xml_parser и XMLReader в том что, в первом случае вам нужно будет писать собственные функции которые будут реагировать на начало и конец тэга.

Проще говоря, xml_parser работает через 2 триггера – тэг открыт, тэг закрыт. Его не волнует что там идёт дальше, какие данные используются и т.д. Для работы вы задаёте 2 триггера указывающие на функции обработки.

В XMLReader всё проще. Во первых, это класс. Все триггеры уже заданы константами (их всего 17), чтение осуществляется функцией read() которая читает первое вхождение подходящее под заданные триггеры. Далее мы получаем объект в который ханосится тип данны (аля триггер), название тэга, его значение. Также XMLReader отлично работает с аттрибутами тэгов.

Пример использования XMLReader
<?php
<?

Class QuizXMLReader
{
	
	private $reader;
	private $tag;
	
	// if $ignoreDepth == 1 then will parse just first level, else parse 2th level too
	/*
	 * Example:
	 * 			parseBlock('quiz_question'):
	 *
	 * 				<quiz_question>
						<question_text><![CDATA[]]></question_text>
						<question_image><![CDATA[]]></question_image>
						<question_info>
							<question_date></question_date>
							<question_rating></question_rating>
						</question_info>
					</quiz_question>

				$ignoreDepth = 1
				return
					array(
							[question_text]
							[question_image]
					)

				$ignoreDepth = 0
					return
						array(
							[question_text]
							[question_image]
							[question_info] = array (
												[question_date]
												[question_rating]
											)
						)
	 */
	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;
		}
	}
	
	private function quiz_categories() {
		
		if ($this->reader->name == 'quiz_categories') {
			// while not found end tag read blocks
			while (!($this->reader->name == 'quiz_categories' && $this->reader->nodeType == XMLReader::END_ELEMENT)) {
				$quiz_category = $this->parseBlock('quiz_category');
				//DO SOMETHING
				unset($quiz_category);
				$this->reader->read();
			}
			
			$this->reader->read();
		}
	}
	
	private function quiz_certificates() {
		if ($this->reader->name == 'quiz_certificates') {
			$quiz_certificates = $this->parseBlock('quiz_certificates');
			//DO SOMETHING
		}
	}
	
	public function parse($filename) {
		
		if (!$filename) return array();
		
		$this->reader = new XMLReader();
		$this->reader->open($filename);
		
		// begin read XML
		while ($this->reader->read()) {
			
			$this->quiz_categories();
			$this->quiz_certificates();
			
		} // while
	} // func
}

$xmlr = new QuizXMLReader();
$r = $xmlr->parse('export.xml');



Согласно найденным бенчмаркам с большими файлами XMLReader справляется лучше чем xml_parser.

Требования к xml_reader:
PHP 5.0 >
libxml

Требования к XMLReader:
PHP 5.1
libxml

P.S. Советы и комментарии с удовольствием выслушаю. Прошу сильно не пинать

Комментарии (8)


  1. valemak
    06.05.2016 11:33

    В XML можно, в принципе, при записи вообще не использовать переносы строк и табуляцию. Лишь бы элементы соответствовали стандарту и их вложенность была правильной. То есть теоретически файл размером в сотни мегабай будет в одну строку и такой XML будет являться корректным.

    В этом случае для больших файлов спасёт не построчное, а только посимвольное чтение.


    1. Sect0R
      06.05.2016 11:46

      Абсолютно с вами согласен.
      Даже в этом случае нас спасёт XMLReader, т.к. ему абсолютно плевать на табуляцию и переносы.


      1. valemak
        06.05.2016 12:08

        То есть xml_parser также отсеивается и абсолютным победителем является XMLReader.


  1. ZonD80
    06.05.2016 11:42

    А как же querypath?


    1. Sect0R
      06.05.2016 11:49

      Если мне не изменяет память QueryPath использует DOMDocument


  1. RainBowAM
    06.05.2016 11:45

    У Вас в конце стать, в требованиях, дважды упомянут XMLReader, хотя речь и идёт о двух разных парсерах, исправьте в статье.


    1. Sect0R
      06.05.2016 11:45

      ок, спасибо


  1. kamilsk
    06.05.2016 12:07
    +1

    Заметка очень поверхностная. «Плюсы/минусы» -> «Плюс/Минус», т.к. ограничивается в одну фразу (в одном случае учитывается распространенность, в другом поддержка PHP 4 [в 2016 это еще плюс??]), почему бы не систематизировать?

    попросили провести исследование
    если это исследование, то было бы неплохо увидеть:
    1. API, которое предоставляет каждое из перечисленных решений
    2. немного читаемых примеров под катом для каждого из перечисленных решений, демонстрирующих удобство/не удобство работы с ним
    3. возможно приведение каких-то тестов производительности (есть 1МБ файл на входе, на выходе [после полной обработки и получения нужной информации из файла] получили такой-то overhead по памяти)
    4. возможно приведение типичных кейсов, где решение лучше подходит, и анти-кейсов, где лучше это решение не использовать
    5. ссылки на официальную документация/страницу, чтобы «чайники» могли ознакомиться более подробно с решением

    Результат: чайники так и останутся чайниками.