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
| DOMDocument | XMLReader | |
|---|---|---|
| Lokalny komnputer | 269 sek | 41 sek. |
| Serwer dedykowany / Hetzner | 264 sek. | 15 sek. |
| Serwer współdzielony / vipserv.org | error 500 / timeout | 15 sek. |
| Serwer współdzielony / IQ.pl | 277 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();