XMLReader – když se zamotáme do SAX

V minulém dílu seriálu o práci s XML soubory v jazyce PHP jsme probírali rozhraní SAX. V dnešním pokračování je na řadě rozhraní XMLReader, které je do určité míry podobné (zpracovává XML soubor po částech), ale používá opačný směr toku událostí a dovoluje zjednodušit kód výsledné aplikace.
Seriál: Přehled podpory XML v PHP5 (6 dílů)
- Přehled podpory XML v PHP5 5. 10. 2009
- PHP a XML: SAX – čteme pěkně popořádku 12. 10. 2009
- XMLReader – když se zamotáme do SAX 19. 10. 2009
- DOM – načteme to do paměti 26. 10. 2009
- XPath – rychle to najdeme 2. 11. 2009
- XSLT – jazyk budoucnosti 9. 11. 2009
Nálepky:
Jak jsme viděli, rozhraní SAX je sice rychlé a nenáročné na zdroje, ale práce s ním není moc pohodlná. Je to způsobeno push modelem, který SAX používá (viz obrázek 4 – „Princip push modelu přístupu k dokumentu XML“ v minulém dílu). Tuto nevýhodu SAXu překonávají novější rozhraní, která staví na tzv. pull modelu. Zatímco push parser (SAX) zahrnuje naši aplikaci proudem událostí, které zkrátka musíme obsloužit, pull parser aplikaci předá data, jen když si o to aplikace řekne. Data tedy čteme v tu chvíli, kdy je potřebujeme. Díky tomu může být kód aplikace mnohem přehlednější a přímočařejší.
Pull parser je v PHP dostupný jako třída XMLReader
. Díky této objektové obálce je práce s XMLReader
velmi jednoduchá. Nejprve si musíme vždy vytvořit novou instanci parseru a pak říci, jaký dokument se bude načítat:
$reader = new XMLReader(); $reader->open("dokument.xml");
V tomto okamžiku je dokument XML připraven ke čtení. V okamžiku, kdy náš skript bude chtít přečíst část dokumentu, stačí použít metodu read()
, která přečte další část dokumentu XML (počáteční tag, obsah elementu apod.). V případě, že jsme na konci dokumentu, vrací metoda hodnotu false
, v opačném případě true
. Chceme-li tedy postupně přečíst celý dokument, stačí metodu volat v cyklu while
:
while ($reader->read()) { … }
Chcete se naučit o PHP víc?
Akademie Root.cz pořádá školení Kurz programování v PHP5. Jednodenní kurz programování v PHP 5 je určen všem webovým vývojářům, kteří se chtějí do hloubky seznámit a sžít s programovacím jazykem PHP ve verzi 5. První část kurzu je zaměřena na nový objektový model se všemi jeho vlastnostmi, ošetření chyb pomocí výjimek a efektivní využití těchto konceptů. Druhá část je zaměřena na nové knihovny PHP 5, především pro práci s databázemi, XML a objekty. Pozornost je věnována i zajištění kompatibility s PHP 4, přechodu z této verze a výhledu na PHP 6. Máte zájem o jiné školení? Napište nám!
Na objektu XMLReader
je dostupných několik vlastností, které umožňují zjišťovat informace o části dokumentu, na kterou jsme se právě přesunuli. Můžeme například zjišťovat druh uzlu (nodeType) a jeho název (name). Jsme-li nastaveni na elementu (tedy na jeho počátečním tagu), lze pomocí metody readString()
přečíst celý textový obsah elementu. Použití těchto metod pro čtení dokumentu RSS demonstruje následující příklad.
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN'> <html lang="cs"> <head> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <title>Přehled zpráv</title> </head> <body> <?php // vytvoření nového XMLReaderu $reader = new XMLReader(); // otevření souboru pro čtení $reader->open("../data/luparss.xml"); // dokud je co, čteme další část dokumentu XML while ($reader->read()) { // obsluha odkazu (element link) if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "link") echo "<h1>Přehled aktuálních zpráv ze serveru <a href='" . htmlspecialchars($reader->readString(), ENT_QUOTES) . "'>" . $title . "</a></h1>"; // obsluha názvu kanálu if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "title") $title = htmlspecialchars($reader->readString()); // obsluha položky v kanálu if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "item") { echo "<dl>"; // dokud je co, čteme další část dokumentu XML // předpokládáme přitom, že elementy jsou uvnitř elementu item while ($reader->read()) { if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "link") echo "<dt><a href='" . htmlspecialchars($reader->readString(), ENT_QUOTES) . "'>" . $title . "</a></dt>n"; if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "title") $title = htmlspecialchars($reader->readString()); if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == "description") echo "<dd>" . htmlspecialchars($reader->readString()) . "</dd>n"; } echo "</dl>"; } } ?> </body> </html>
V příkladu je použita konstanta XMLREADER_ELEMENT
, která definuje kód uzlu, který odpovídá začátku elementu. Zároveň vidíme, že podle potřeby můžeme čtení z dokumentu XML zanořovat níže do struktury kódu – např. když chceme jinak zpracovat obsah elementu item
. Práce s XMLReader
je principem podobná jako sekvenční čtení souborů, a proto je mnoha programátorům velmi blízká. Navíc zachovává výhody rozhraní SAX, jako je rychlost a malá paměťová náročnost.
V příštím díle se podíváme na snad nejznámější rozhraní pro práci s XML, na rozhraní DOM. Toto rozhraní je webovým vývojářům dobře známé, protože stejné rozhraní se používá i v JavaScriptu pro manipulaci se stránkou načtenou do prohlížeče.
Více informací o knize naleznete na stránkách nadavatelství Grada a na stránkách autora.
Byl jsem na Webexpu a poprve slysel o XMLReader a tesil se na pokracovani clanku o XML tady, ale ted jsem trosku rozpacity.
I tenhle jednoduchy priklad ukazuje, ze to ma koncepcni chyby. Element link se poprve vyskytuje zde: /rss/channel/image/link. Takze prvni nalezeni tohoto elementu se tiskne jeste predtim nez mame title z /rss/channel/title takze H1 mame ve vysledku 2× a jako premii PHP Notice.
To by se dalo nejak opravit, ale z ceho me mrazi je ten vnoreny „while ($reader->read())“. Porad jsem marne hledal misto, kde se to opousti po ukonceni elementu item a nenasel. Ono to fakt bezi furt dal. Takze pokud bude jednomu itemu chybet title, tak se tam vesele natiskne ten predchozi.
V článku je jen primitivná ukázka využití XMLReaderu.
Nic vám nebrání udělat si dokonalejší logiku zpracování. Např. mazat $title v okamžiku, kdy narazíte na koncový tag elementu item.
Další varianta je zapnout validaci a parser pak havaruje v případě, kdy narazí na položky bez názvu – ten je v RSS povinný (pamatuju-li si to dobře).
Nicméně obecně v proudových parserech jako SAX a XMLReader si každopádně musíte držet nějakou stavovou informaci. Rozdíl je v tom, že v XMLReaderu ji nepotřebujete tolik a navíc pro obsluhu podelementů můžete do sebe cykly volající $reader->read() zanořovat (a vyskočit z nich při nalezení koncového tagu), čímž se vše zase zjednoduší.
Validace v tomto případě nepomůže, protože kód závisí na pořadí jednotlivých značek (které v RSS není určeno) a navíc na absenci povolených značek (konkrétně <image>).
Sám píšeš, že u proudových parserů je potřeba si držet informaci o stavu. Zásadní chybu příkladů (u minulého a tohoto článku) vidím v tom, že si tento stav nedrží. Příklady pak vůbec neodpovídají skutečnému použití.
Ten příklad není psán jako obecná čtečka RSS, ale jako čtečka XML formátu z prvního dílu seriálu – počítá se se pevným pořadím elementů (což obecně RSS nemá a je to přiznejme si spíše prasárna tohoto formátu).
Obecně formáty, ve kterých se přenášejí velká data a je potřeba používat proudové zpracování mají pevné pořadí podelementů, takže tam tenhle problém odpadá.
Ty příklady jsou trochu umělé a trochu zjednodušené, aby se ukázalo použití rúzných API pro řešení stejné úlohy. Samozřejmě v praxi si pro danou úlohu vyberete nejvhodnější prostředek. Konvertovat RSS do HTML čímkoliv jiným než XSLT je čirý masochismus.
Jinak obsluhu toho, aby se nezpracovával link uvnitř jiných elementů, lze jednoduše vyřešit tím, že tyto elementy se na začátku cyklu přeskočí:
// přeskočíme image a textinput, protože může obsahovat link, který nás nezajímá
if ($reader->nodeType == XMLReader::ELEMENT && in_array($reader->name, array(„image“, „textinput“)))
$reader->next();
SAX a XMLReader su v podstate skoro to iste. Osobne si myslim, ze SAX je z hladiska spracovania lepsi a z hladiska kodu prehladnejsi. Samozrejme pre dobreho programatora. :)
XMLReader mi pripada ako dorbeny neskor pre tych, ktori nevedia pochopit ako moze fungovat SAX. :)
Koncepční chyba není v XMLReaderu, ale v příkladu. Osobně mi přijde vhodnější všechno načítat v jednom while cyklu a kontext si sledovat v proměnné. Ukázka RSS importu pomocí XMLReaderu je na http://www.clipboard.cz/4ng. Má pouze to omezení, že očekává značky <item> až za <link> a <title> celého zdroje. Pokud by to tak nebylo, pak bychom výstup nemohli rovnou vypisovat, ale museli bychom si ho ukládat do dočasné proměnné.
On je cely ten XMLReader divny:
$xml->nodeType == XMLReader::ELEMENT
$xml->nodeType == XMLReader::END_ELEMENT
$xml->nodeType == XMLReader::TEXT
Takze uzavreny element je typ uzlu?
A tenhle priklad je uz natolik nepochopitelny, ze muze byt i spravne. Nicmene pouziti jednoho $xml->read() asi uplne rusi veskere vyhody XMLReaderu. Zatim rikam zlaty SAX (samozrejme ze pro pripad RSS je zlaty simplexml_load_file).
ELEMENT ~ počáteční tag
END_ELEMENT ~ koncový tag
(viz tabulka na straně 165 ve knize).
To pojmenování může vypadat trochu divně, ale všichni to okopírovali z .NETu (http://msdn.microsoft.com/…odetype.aspx) – ten použil jmenné konvence tak, aby se to hodně krylo s DOMem.
Důležitá je ještě vlastnost isEmptyElement, která umožňuje rozpoznat prázdné elementy (<x/>). Ty totiž END_ELEMENT nemají.
To držení všech předků na zásobníku a jejich porovnávání je hrozně pomalé (jasně na RSS to nevadí, ale XMLReader je určena na velké objemy dat a tam by to bylo znát).
Zásadně pomalejší to není. Kontext si nějak držet musíme, takže kromě vlastního zásobníků zbývá rekurzivní funkce. Udělal jsem ji co nejchytřeji (do rekurze vstupuje jen když potřebuje změnit kontext) a výsledky jsou tyto:
0.98 s – porovnávání pole s kontextem
0.91 s – chytrá rekurzivní funkce
Jedná se o medián ze tří pokusů při načítání titulku z exportu Wikipedie. Zdrojový kód je na http://www.clipboard.cz/5ng. Dlužno ale podotknout, že pokud bychom kontextů chtěli rozlišovat více, tak by se kód dramaticky zesložitil. Např. RSS import bych v tom psát opravdu nechtěl.