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

PHP logo

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

  1. Přehled podpory XML v PHP5 5.10.2009
  2. PHP a XML: SAX – čteme pěkně popořádku 12.10.2009
  3. XMLReader – když se zamotáme do SAX 19.10.2009
  4. DOM – načteme to do paměti 26.10.2009
  5. XPath – rychle to najdeme 2.11.2009
  6. XSLT – jazyk budoucnosti 9.11.2009

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.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 15

Přehled komentářů

Jakub Vrána Alternativa
Martin Malý Re: Alternativa
Jakub Vrána Re: Alternativa
Logik Re: Alternativa
Jiří Kosek Re: Alternativa
Jakub Vrána Re: Alternativa
Jiří Kosek Re: Alternativa
Jakub Vrána Re: Alternativa
Jiří Kosek Re: Alternativa
Logik Re: Alternativa
Jiří Kosek Re: Alternativa
Jiří Kosek Re: Alternativa
Jakub Vrána Re: Alternativa
gilhad DOM_over_SAX :)
Jakub Vrána Re: DOM_over_SAX :)
Zdroj: https://www.zdrojak.cz/?p=3098