PHP a XML: SAX – čteme pěkně popořádku

Rozhraní SAX patří k jedněm z nejstarších rozhraní pro práci s XML. Původně bylo vyvinuto pro programovací jazyk Java, ale brzy bylo ve více či méně upravené podobě převzato i do dalších jazyků. V druhém díle seriálu Jiřího Koska se zaměříme právě na toto rozhraní.
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:
Na rozdíl od rozhraní DOM a SimpleXML se SAX hodí pro čtení i hodně velkých dokumentů XML, protože se dokument nenačítá celý do paměti, ale čte se postupně sekvenčně. Během čtení dokumentu se aplikaci průběžně předávají informace o tom, co se v dokumentu nachází za informace. SAX parser pro každý důležitý prvek dokumentu, jako je počáteční a koncový tag, znaková data, komentář apod., vyvolá událost, kterou můžeme obsloužit. Jako parametry události se přitom předávají důležité informace, jako je například název elementu pro počáteční a koncový tag, text obsažený ve znakových datech apod.
Práce s rozhraním SAX je poměrně komplikovaná, protože ke zpracování dokumentu XML dochází nepřímo v obsluze událostí. Ve skriptu proto musíme definovat funkce, které se postarají o obsluhu jednotlivých událostí. Tyto funkce je pak potřeba zaregistrovat v nově vytvořeném parseru a teprve na konec probíhá samotné čtení dokumentu XML a jeho předávání parseru ke zpracování.
Příklad 3. Čtení XML pomocí rozhraní SAX – sax.php
<!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í parseru $parser = xml_parser_create("utf-8"); // nastavení parametrů xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); // nastavení funkcí pro obsluhu elementů xml_set_element_handler($parser, "startElement", "endElement"); // nastavení funkce pro obsluhu obsahu elementu xml_set_character_data_handler($parser, "characters"); // otevření XML dokumentu $fp = fopen("../data/luparss.xml", "r"); if (!$fp) die ("Nelze otevřít soubor."); // zpracování celého souboru while ($x = fread($fp, 4096)) { if (!xml_parse($parser, $x, feof($fp))) die (sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser))); } // uvolnění paměti alokované parserem xml_parser_free($parser); // pomocné proměnné pro uchovávání stavu čtení $inItem = false; $inLink = false; $inTitle = false; $inDescription = false; $title = ""; $link = ""; $description = ""; // obsluha začátku elementu function startElement($parser, $name, $attrs) { global $inItem, $inLink, $inTitle, $inDescription, $title, $link, $description; // zjistíme, zda jsme v další položce feedu if ($name == "item") { $inItem = true; return; } // zjistíme, zda jsme v nadpisu if ($name == "title") { $inTitle = true; $title = ""; return; } // zjistíme, zda jsme v adrese if ($name == "link") { $inLink = true; $link = ""; return; } // zjistíme, zda jsme v popisu if ($name == "description") { $inDescription = true; $description = ""; return; } } // zpracování konce elementu function endElement($parser, $name) { global $inItem, $inLink, $inTitle, $inDescription, $title, $link, $description; // zjistíme, zda jsme na konci položky if ($name == "item" && $inItem) { $inItem = false; echo "<dt><a href='" . htmlspecialchars($link, ENT_QUOTES) . "'>" . htmlspecialchars($title) . "</a></dt>n"; echo "<dd>" . htmlspecialchars($description) . "</dd>n"; return; } // zjistíme, zda jsme na konci elementu a nejsme v položce // v tomto případě vypisujeme záhlaví if ($name=="link" && !$inItem) { $inLink = false; echo "<h1>Přehled aktuálních zpráv ze serveru <a href='" . htmlspecialchars($link, ENT_QUOTES) . "'>"; echo htmlspecialchars($title) . "</a></h1>n"; echo "<dl>"; return; } // ukončení seznamu if ($name=="channel") { echo "</dl>n"; } // vypnutí příznaků na koncovém tagu if ($name=="item") $inItem = false; if ($name=="title") $inTitle = false; if ($name=="link") $inLink = false; if ($name=="desciption") $inDescription = false; } // obsluha znakových dat function characters($parser, $data) { global $inItem, $inLink, $inTitle, $inDescription, $title, $link, $description; // připojení právě přečteného textu do odpovídající pomocné proměnné // podle toho, v jakém jsme právě elementu if ($inLink) $link .= $data; if ($inTitle) $title .= $data; if ($inDescription) $description .= $data; } ?> </body> </html>
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!
SAX parser v PHP pochází ještě z dob PHP3, a proto nemá objektové rozhraní. Pracuje se s ním podobně jako se soubory nebo s připojením k databázi. Nově vytvořený parser dostane přiřazený svůj identifikátor:
$parser = xml_parser_create("utf-8");
A tento identifikátor se používá v dalších funkcích pro určení parseru, na který se má funkce použít. Můžeme tak najednou pracovat s více dokumenty XML. Většinou si proto identifikátor parseru uložíme do nějaké proměnné, v našem příkladě se jedná o proměnnou $parser
.
Před dalším použitím parseru jej musíme nakonfigurovat. Při výchozím nastavení parser ignoruje velikost písmen, což je v rozporu se specifikací XML. Proto náš skript toto chování vypíná:
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
Dále parseru nastavíme, jakým funkcím má ke zpracování předávat události pro začátek a konec elementu a pro text uvnitř elementu.
xml_set_element_handler($parser, "startElement", "endElement"); xml_set_character_data_handler($parser, "characters");
Všechny tři odpovídající funkce startElement()
, endElement()
a characters()
jsou přitom definovány dále ve skriptu a k jejich vysvětlení se ještě vrátíme.
Nyní je již parser připraven na přijímání a zpracování dat. Můžeme proto otevřít soubor obsahující dokument XML:
$fp = fopen("../data/luparss.xml", "r");
Nyní v cyklu budeme ze souboru číst bloky textu o velikosti 4 KiB:
while ($x = fread($fp, 4096))
V proměnné $x
tak budeme mít vždy kus vstupního dokumentu XML. Ten musíme předat parseru ke zpracování:
xml_parse($parser, $x, feof($fp))
Funkce xml_parse()
jako první parametr očekává identifikátor parseru, dále data ke zpracování a poslední parametr určuje, zda se jedná o poslední kus dat, který parseru předáváme. Parser budeme naposledy volat, až přečteme celý soubor a funkce feof()
tedy bude vracet hodnotu true
.
V případě, že ve zpracovávané části dokumentu XML je nějaká syntaktická chyba, vrátí funkce xml_parse()
hodnotu false
, takže můžeme na chybu zareagovat a vypsat ji.
Na závěr zpracování je slušné uvolnit paměť, kterou si parser alokoval pomocí:
xml_parser_free($parser);
Kód, který jsme si dosud ukázali, je vlastně stejný pro všechny aplikace, které používají rozhraní SAX. Odlišnosti jsou až v logice zpracování dat, která je zapsaná přímo do funkcí, které obsluhují jednotlivé události.
Při bližším studiu zjistíme, že kód vytvářený pomocí rozhraní SAX není zrovna dvakrát přehledný. Je to způsobeno tím, že zpracování jedné informace je rozděleno na tři části. Dejme tomu, že chceme přečíst název položky v kanálu RSS:
<title>Rychlost je na nic, následuj instinkt</title>
Tento kousek kódu XML rozhraní SAX předá postupně jako tři události:
-
událost začátek elementu (startElement) – v ní bude předán název počátečního tagu
title
; -
událost znaková data (characters) – v ní bude předán obsah elementu „Rychlost je na nic, následuj instinkt“;
-
událost konec elementu (endElement) – v ní bude předán název koncového tagu
title
.
Obsluha události pro počáteční tag proto musí otestovat, zda se jedná o počáteční tag elementu title
. Pokud ano, pak si musíme nastavit nějaký příznak, který bude indikovat, že jsme uvnitř elementu title
. Tento příznak pak bude testovat obsluha události znakových dat, protože text nás v tomto případě zajímá pouze tehdy, pokud jsme uvnitř elementu title
. A konečně obsluha koncového tagu detekuje, zda se jedná o koncový tag elementu title
. Pokud ano, zpracuje data, která jsme si uložili během zpracování události pro znaková data, a vynuluje příznak přítomnosti uvnitř elementu title
.
Protože většinou pracujeme s více elementy než s jedním, je výše popsaný kód uvnitř obsluhy každé události přítomen několikrát pro každý element, jehož obsah chceme nějakým speciálním způsobem zpracovat.
I proto náš ukázkový skript používá několik globálních proměnných $inItem
, $inLink
, $inTitle
a $inDescription
. V nich se uchovává informace o tom, v jakém elementu se nacházíme. Funkce startElement()
obsluhující počáteční tagy testuje vždy název elementu, který začíná, a podle toho nastaví odpovídající příznak a případně vynuluje proměnnou, která se používá pro uchovávání textového obsahu elementu.
// zjistíme, zda jsme v nadpisu if ($name == "title") { $inTitle = true; $title = ""; return; }
Funkce pro obsluhu události začátku elementu přitom musí vždy akceptovat tři parametry. Prvním je identifikátor parseru, druhým název elementu a konečně třetí parametr je pole obsahující hodnoty všech atributů uvedených u elementu.
Funkce characters()
obsluhující znaková data dostane jako parametry identifikátor parseru ( $parser
) a text ( $data
), který je uvnitř elementu. Uvnitř funkce se podle příznaku rozhodneme, do jakého elementu text patří a připojíme k pomocné proměnné. Např. o postupné zjištění obsahu elementu title
se postará následující část funkce:
if ($inTitle) $title .= $data;
Poslední část logiky zpracování údajů je uložena ve funkci endElement()
, která se stará o obsluhu události pro koncový tag. V parametrech dostane předán identifikátor parseru a název ukončovacího tagu. Podle toho, jaký element je ukončen, se vypíší odpovídající údaje nashromážděné v pomocných proměnných uvnitř události pro znaková data. Nakonec se ještě zruší příznak indikující, že jsme uvnitř nějakého elementu:
if ($name=="title") $inTitle = false;
Jak je vidět, je použití rozhraní SAX poměrně pracné, protože čtení dokumentu XML jako proudu událostí není vždy úplně přehledné. Tomuto modelu pro práci s XML se také někdy říká push model, protože parser do aplikace tlačí (angl. push) informace z dokumentu XML.
Obrázek 4. Princip push modelu přístupu k dokumentu XML
Příště se podíváme na mnohem pohodlnější alternativu k rozhraní SAX na tzv. XMLReader.
Ke stažení: PHP a XML – ukázky (pro celý seriál)
Více informací o knize naleznete na stránkách nadavatelství Grada a na stránkách autora.
V rámci konference WebExpo 2009 proběhne autogramiáda knihy.
Jako alternativu doporučuji PHP 5 extenzi XMLReader, která má stejné vlastnosti (postupné načítání) a pracuje se s ní přece jenom o poznání lépe.
Při tomto způsobu zpracování XML dokumentů (ať už archaickou extenzí XML nebo novější XMLReader) doporučuji udržovat zásobník načtených značek. To je univerzální řešení schopné zpracovat libovolný XML dokument bez potřeby definovat si spoustu pomocných proměnných (zde
$in*
). Test na titulek RSS zprávy pak může být zapsán jednoduše jakoif ($kontext == array("rss", "channel", "item", "title"))
.Ukázkovému kódu by také myslím slušelo, kdyby místo globálních proměnných používal vlastnosti objektu. Čitelnosti příkladu by to myslím navíc prospělo.
Jakube, je to seriál, takže se dočkáš i XMLReaderu, a to hned příští pondělí (pro zajímavost: Čekají nás ještě díly o XMLReaderu, DOM, XPath a XSLT).
Abych byl konstruktivní: http://www.clipboard.cz/9pg
Takto upravený kód je podle mě nejen kratší, ale hlavně přehlednější, snáze rozšiřitelný a méně náchylný k chybám.
Na druhou stranu by bylo dobré říct, že ten SAX (s dobře napsanejma handlerama) bude to xml zpracovávat značně rychlejc, než todle řešení…
Vůbec, doufam že na konci bude srovnání, k čemu se hodí co (rychlost, spotřeba paměti, podpora namespace apod…)
SAX tak jak je v PHP se nehodí skoro na nic ;-)
Nicméně v jazycích Perl a PHP bývá SAX znatelně pomalejší než XMLReader, protože se spoustu času stráví marshallingem předávaných parametrů mezi nativní knihovnou (zde libxml2) a interpretem Perlu/PHP. A stejná aplikace napsaná pomocí SAX vyžaduje obvykle několikánosobně větší počet volání než napsaná pomocí XMLReader.
Zajímalo by mě, jestli to máte něčím podložené. Protože já jsem si pro zajímavost udělal srovnání a totožné zpracování 4.9 MB dokumentu (
bawiki.xml
) trvalo s extenzí XML 1.04 sekund a s XMLReader 0.55 sekund.Nějak nechápu, vaše srovnání přece potvrzuje to co jsem psal (SAX je v PHP/Perlu pomalejší než XMLReader), ne?
A ano, kdysi jsem na to dělal v PHP testy, a o Perlu to vím přímo od autora daných perlových modulů.
Však ano, tohle byla reakce na Logika. V názoru se shodneme, já jsem ho pouze podpořil čísly.
Sorry, nějak jsem se tady v těch vláknech ztratil :-(
Myslel jsem to tak, že v SAXu lze využít možnosti „přehazovat“ handlery a tim v obsluze (při složitějšim dokumentu) zjednodušit rozhodování, zatimco v XML readeru se s uvedenym přístupem se vždy musí porovnávat pole kontextu s hromadou polí „kde právě jsem“.
Ale je pravda, že jsem se na to koukal teoreticky a přesný čísla neznam (v php jsem se saxem dělal jen malý osubory, kde mě to nezajímalo) – je možný že to maršmelounování parametrů i pro dobře napsanej SAX kód bude pomalejší.
I když budete přepínat několik handlerů, bude SAX kód relativně složitější.
Navíc s XML Readerem můžete dělat něco podobného. Když najdete určitý element, můžete zavolat nějakou funkci/metodu, předat ji XMLReader a obsloužit celý element a pak se zase vrátit zpět.
I na XMLReader dojde, viz poslední věta článku <i>Příště se podíváme na mnohem pohodlnější alternativu k rozhraní SAX na tzv. XMLReader.</i>
V knize je samozřejmě i ukázka toho, jak zapouzdřit obsluhu událostí do jednoho objektu, ale nevidím přínos proč objekty v PHP míchat do nejprimtivnějšího příkladu. Narozdíl třeba od Javy nejsou v PHP objekty povinné, není vnucena ani minimální štábní kultura, takže mi přijde zbytečné čtenáře přesvědčovat o tom, zda používat nebo nepoužívat objekty, když jádrem článku mají být možnosti zpracování XML.
Omlouvám se, tu větu v článku jsem přehlédl. Navíc mi to mohlo dojít z ukázkových příkladů.
Co se objektů týče, tak také nejsem žádný jejich fanatický zastánce, ale pokud jejich použití vede ke zjednodušení a hlavně vyčištění kódu jako v tomto případě, tak bych je neváhal použít.
udelal jsem v Pythonu si mini-knihovnu ktera cte XML pomoci SAX-u a jednoduse zbuduje objektovy model toho XML. Vedlo me k tomu to, ze DOM byl z vykonostniho hlediska naprosto nepouzitelny, jak kvuli rychlosti, tak kvuli pameti. Takhle jsem snadno a rychle ziskal z XML data v pouzitelnem tvaru za prijatelnou cenu, bez komplexnosti sluzeb DOM (kterou jsem fakt nepotreboval).
Proste se vytvori objekty pro tagy, ktere maji jako polozky text tagu, seznam atributu a seznam potomku. Plus par funkci, ktere vraci hodnoty jako string/int/boolean a umozni pridat/ubrat jednotlive prvky. Pro prenos dat to je vice nez dostacujici, zpracovani je jednoduche a oproti poctivemu DOMu to je radove min narozne na zdroje.
Jde tedy o jakousi obdobu knihovny SimpleXML z PHP. Problém tohoto řešení je ten, že se celý dokument načte do paměti, což u obrovských XML dokumentů (třeba export Wikipedie) není schůdné.