Jak parsować duże pliki XML w PHP?

Jakiś czas temu stanąłem przed problemem parsowania dużych plików XML w PHP. O ile z małymi plikami nie ma problemu i całość parsowana jest szybko, to próba obsługi większych plików często powoduje zatrzymywanie wykonywania skryptów. Tak duże pliki są jednak często wykorzystywane przy zdalnych aktualizacjach ofert (np. publikowanych przez hurtownie).

Dzieje się tak, gdyż PHP ma z góry założone ograniczenie na możliwą do wykorzystania pamięć, a parsowanie plików standardową metodą (np. DOMDocument) potrafi ją skutecznie wykorzystać.

Rozwiązaniem jest skorzystanie z klasy XMLReader, domyślnie dostępnej w standardowej konfiguracji PHP od wersji 5.1.0 .

Zrobiłem krótkie porównanie szybkości działań dla DOMDocument i XMLReader korzystając z 4 różnych komputerów

Rozmiar pliku XML: 208 MB
Liczba wpisów: 148723

DOMDocumentXMLReader
Lokalny komnputer269 sek41 sek.
Serwer dedykowany / Hetzner264 sek.15 sek.
Serwer współdzielony / vipserv.orgerror 500 / timeout15 sek.
Serwer współdzielony / IQ.pl277 sek.33 sek.

Jak widać różnica jest kolosalna (ok. 10-20 razy szybciej) i w przypadku dużych plików warto postawić na XMLReader.

Fragment kodu odpowiedzialny za parsowanie przez DOMDocument:

$doc = new DOMDocument();
$doc->load($localurl);
$items= $doc->getElementsByTagName("item");
$countItems = $items->length;
 
foreach($items as $item)
{
	$id = $item->getElementsByTagName("id")->item(0)->nodeValue;
	$url = $item->getElementsByTagName("url")->item(0)->nodeValue;
	$title = $item->getElementsByTagName("title")->item(0)->nodeValue;
	$author = $item->getElementsByTagName("author")->item(0)->nodeValue;
	$isbn = $item->getElementsByTagName("isbn")->item(0)->nodeValue;
	$image = $item->getElementsByTagName("image")->item(0)->nodeValue;
	$ean = $item->getElementsByTagName("ean")->item(0)->nodeValue;
	$published = $item->getElementsByTagName("published")->item(0)->nodeValue;
	$publisher = $item->getElementsByTagName("publisher")->item(0)->nodeValue;
	$pages = $item->getElementsByTagName("pages")->item(0)->nodeValue;
	$price = $item->getElementsByTagName("price")->item(0)->nodeValue;
	$description = $item->getElementsByTagName("description")->item(0)->nodeValue;
	$status = $item->getElementsByTagName("status")->item(0)->nodeValue;
	$count++;
  }

 

Fragment kodu odpowiedzialny za parsowanie przez XMLReader:

$reader = new XMLReader();
$reader->open($localurl);
 
while($reader->read())
{
	if($reader->nodeType == XMLReader::ELEMENT) $nodeName = $reader->name;
	if($reader->nodeType == XMLReader::TEXT || $reader->nodeType == XMLReader::CDATA)
	{
		if ($nodeName == 'id') $id = $reader->value;
		if ($nodeName == 'url') $url = $reader->value;
		if ($nodeName == 'title') $title = $reader->value;
		if ($nodeName == 'author') $author = $reader->value;
		if ($nodeName == 'isbn') $isbn = $reader->value;
		if ($nodeName == 'image') $image = $reader->value;
		if ($nodeName == 'ean') $ean = $reader->value;
		if ($nodeName == 'published') $published = $reader->value;
		if ($nodeName == 'publisher') $publisher = $reader->value;
		if ($nodeName == 'pages') $pages = $reader->value;
		if ($nodeName == 'price') $price = $reader->value;
		if ($nodeName == 'description') $description = $reader->value;
		if ($nodeName == 'status') $status = $reader->value;
		$ean = '';
	}
 
	if($reader->nodeType == XMLReader::END_ELEMENT && $reader->name == 'item')
	{
		$count++;
	}
}
$reader->close();