Přejít k navigační liště

Zdroják » Různé » PHP a XML: SAX – čteme pěkně popořádku

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

Články Různé

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í.

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.

Obrázek 3. SAX reprezentuje dokument XML jako proud událostí

Kosek

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:

  1. událost začátek elementu (startElement) – v ní bude předán název počátečního tagu  title;

  2. událost znaková data (characters) – v ní bude předán obsah elementu Rychlost je na nic, následuj instinkt;

  3. 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 XMLKosek

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.

Komentáře

Subscribe
Upozornit na
guest
15 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Jakub Vrána

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 jako  if ($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.

Martin Malý

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).

Jakub Vrána

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.

Logik

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…)

Jakub Vrána

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.

Jakub Vrána

Však ano, tohle byla reakce na Logika. V názoru se shodneme, já jsem ho pouze podpořil čísly.

Logik

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ší.

Jakub Vrána

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.

gilhad

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.

Jakub Vrána

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é.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.