Unterabschnitte


XML-Dokumente parsen

In diesem Abschnitt geht es um das Parsen von XML-Dokumenten[*]

Was ist XML?

Die Extensible Markup Language (XML) ist eine Methode, um strukturierte Daten in einer Textdatei abzulegen. Auf den ersten Blick scheint XML wie eine Weiterentwicklung von HTML. In Wirklichkeit basieren aber sowohl XML als auch HTML auf der zwar mächtigeren aber auch sehr schwer verständlichen Sprache SGML. Darüber hinaus wurde HTML nach Version 4 in XML umgesetzt und sinngemäßerweise in XHTML umgetauft.[*]Ein wichtiger Aspekt von XML ist die vollständige Plattformunabhängigkeit, da es sich nur um reine Textdaten handelt.

Parsen von XML-Daten

Unter PHP gibt es zu diesem Thema elegante Lösungen, wie zum Beispiel Expat (http://www.jclark.com/xml/). Expat ist ein SAX[*]-ähnlicher, ereignisorientierter XML-Parser.

Der Ablauf eines Parsevorgangs ist einfach:
Es werden zuerst sog. Handler (,,Behandler``) für bestimmte Ereignisse definiert. Solche Ereignisse können sein:

Findet der Parser ein startendes Element, wie
<abschnitt titel="XML-Dokumente">
, so ruft er die passende, vordefinierte Funktion auf und übergibt dieser den Namen des Elements(abschnitt) sowie die Attribute (titel = XML-Dokumente).

Beispiel 1

Die XML-Datei (tutorial.xml)

<?xml version="1.0"?>
<tutorial titel="expat">
    <abschnitt titel="Einfuehrung">
        <index keyword="expat" />
        Text...
    </abschnitt>
    <abschnitt titel="Beispiele">
        <index keyword="beispiele" />
        Wie im <ref id="expat">1. Abschnitt</ref> gezeigt, ...
    </abschnitt>
</tutorial>

Das PHP-Skript

// Zuerst definieren wir die Funktionen, die später auf
// die diversen Ereignisse reagieren sollen

/**
 * Diese Funktion behandelt ein öffnendes Element.
 * Alle Parameter werden automatisch vom Parser übergeben
 *
 * @param    parser    Object    Parserobjekt
 * @param    name      string    Name des öffnenden Elements
 * @param    atts      array     Array mit Attributen
 */
function startElement($parser, $name, $atts) {
  global $html;

  // Die XML-Namen werden in Großbuchstaben übergeben.
  // Deshalb wandeln wir sie mit strtolower() in Klein-
  // buchstaben um.
  switch (strtolower($name)) {
  case "tutorial":
    // Wir fügen der globalen Variable eine Überschrift hinzu:
    $html .= "<h1>".$atts["TITEL"]."</h1>";
    break;
  case "abschnitt";
    $html .= "<h2>".$atts["TITEL"]."</h2>";
    break;
  case "index":
    // Einen HTML-Anker erzeugen:
    $html .= "<a name=\"".$atts["KEYWORD"]."\"></a>";
    break;
  case "ref":
    // Verweis auf einen HTML-Anker:
    $html .= "<a href=\"#".$atts["ID"]."\">";
    break;
  default:
    // Ein ungültiges Element ist vorgekommen.
    $error = "Undefiniertes Element <".$name.">";
    die($error . " in Zeile " . 
        xml_get_current_line_number($parser));
    break;
  }
}

Wenn im XML-Dokument ein öffnendes Element gefunden wird, passiert folgendes:

/**
 * Diese Funktion behandelt ein abschließendes Element
 * Alle Parameter werden automatisch vom Parser übergeben
 *
 * @param  parser    Object    Parserobjekt
 * @param  name      string    Name des schließenden Elements
 */
function endElement($parser, $name) {
  global $html;

  switch (strtolower($name)) {
  case "ref":
    // Den HTML-Link schließen:
    $html .= "</a>";
    break;
  }
}

Diese Funktion schließt einen eventuell offenen HTML-Link.

/**
 * Diese Funktion behandelt normalen Text
 * Alle Parameter werden automatisch vom Parser übergeben
 *
 * @param    parser    Object    Parserobjekt
 * @param    text      string    Der Text
 */
function cdata($parser, $text) {
  global $html;

  // Der normale Text wird einfach an $html angehängt:
  $html .= $text;
}

Die Funktion cdata() wird aufgerufen, wenn normaler Text im XML-Dokument gefunden wird. In diesem Fall wird dieser einfach an die Ausgabe angehängt.

// Die XML-Datei wird in die Variable $xmlFile eingelesen
$xmlFile = implode("", file("tutorial.xml"));

// Der Parser wird erstellt
$parser = xml_parser_create();
// Setzen der Handler
xml_set_element_handler($parser,"startElement","endElement");
// Setzen des CDATA-Handlers
xml_set_character_data_handler($parser, "cdata");
// Parsen
xml_parse($parser, $xmlFile);
// Gibt alle verbrauchten Ressourcen wieder frei.
xml_parser_free($parser);

// Ausgabe der globalen Variable $html.
print $html;

Wenn man das XML-Dokument etwas abändert und ein fehlerhaftes Element an einer beliebigen Stelle einfügt, gibt das Script einen Fehler aus und zeigt an, in welcher Zeile das falsche Element gefunden wurde.

Nachteile

Das erste Beispiel hat den Nachteil, daß im Dokument keine Umlaute oder XML-spezifischen Sonderzeichen verwendet werden können. Man löst dieses Problem meistens durch Entities:
<?xml version="1.0"?>
<!DOCTYPE tutorial [
    <!ENTITY auml "&amp;auml;">
    <!ENTITY ouml "&amp;ouml;">
    <!ENTITY uuml "&amp;uuml;">
    <!ENTITY Auml "&amp;Auml;">
    <!ENTITY Ouml "&amp;Ouml;">
    <!ENTITY Uuml "&amp;Uuml;">
    <!ENTITY szlig "&amp;szlig;">
]>
<tutorial titel="expat">
    <abschnitt titel="Beispiele">
        <index keyword="beispiele" />
        Im normalen Text k&ouml;nnen wir 
        jetzt Umlaute verwenden:
        &Auml;
        &auml;
        &Uuml;
        &uuml;
        &Ouml;
        &ouml;
        Und auch ein scharfes S:
        &szlig;
    </abschnitt>
</tutorial>

Dieses Dokument ist etwas kompliziert. &auml; (ein ä) wird zu &amp;auml; wenn man &amp; wiederum auflöst, erhält man &amp;auml; wird &auml. Also den Ausgangszustand. Das ist aber gewünscht! Die Ausgabe soll nämlich in HTML gewandelt werden. Und dort verwenden wir wieder ein Entity...

Allerdings hat auch diese Methode ihre Nachteile. Zum Beispiel müssen &-Zeichen immer mit &amp; geschrieben werden. Das ist ziemlich lästig, zum Beispiel bei Code-Beispielen:

...
<tutorial ...>
    ...
        <code language="php">
if ($a &amp;&amp; $b) {
    print "\$a und \$b sind nicht '0'";
}
        </code>
    ...
</tutorial>

In diesem Fall kann man aber auch <![CDATA[ ... ]]> verwenden:

...
<tutorial ...>
    ...
        <code language="php">
        <![CDATA[
if ($a && $b) {
    print "\$a und \$b sind nicht '0'";
}
        ]]>
        </code>
    ...
</tutorial>

Das Auflösen von Entities und CDATA-Abschnitten übernimmt expat.

Beispiel 2

Erweitern wir unser Parser-Script. Es soll jetzt zusätzlich auch noch:

Außerdem sollen keine globalen Funktionen/Variablen mehr benutzt werden.

Zunächst benötigen wir ein neues XML-Dokument:

<?xml version="1.0"?>
<!DOCTYPE tutorial [
    <!ENTITY auml "&amp;auml;">
    <!ENTITY ouml "&amp;ouml;">
    <!ENTITY uuml "&amp;uuml;">
    <!ENTITY Auml "&amp;Auml;">
    <!ENTITY Ouml "&amp;Ouml;">
    <!ENTITY Uuml "&amp;Uuml;">
    <!ENTITY szlig "&amp;szlig;">
]>
<tutorial titel="Boolesche Werte">
    <abschnitt titel="Beispiele">
        Nachfolgend ein paar Beispiele zu booleschen Abfragen.
        <code language="php">
        <![CDATA[
$a = 3;
$b = 5;

if ($a && $b) {
    print '$a und $b sind nicht "0"<br />';
}

if ($a < $b) {
    print '$a ist kleiner als $b<br />';
}
        ]]>
        </code>
        Beim Ausf&uuml;ren erh&auml;lt man folgende Ausgabe.
        <?php
$a = 3;
$b = 5;

if ($a && $b) {
    print '$a und $b sind nicht "0"<br />';
}

if ($a < $b) {
    print '$a ist kleiner als $b<br />';
}
        ?>
    </abschnitt>
</tutorial>

Um globale Funktionen/Variablen zu vermeiden, bleibt uns als einziger Ausweg die Verwendung einer Klasse.

class TutorialParser {
  var $html; // Erstellter HTML-Code

  function TutorialParser($file) {
    // Überprüfen, ob die Datei vorhanden ist:
    if (!file_exists($file)) {
      $this->error('Datei '.$file.
                   ' kann nicht gefunden werden!');
    }
    else {
      $buffer = implode('', file($file));
      $p = xml_parser_create();

      // Durch das Setzen dieser Option werden nicht alle
      // Element- und Attributnamen in Großbuchstaben
      // umgewandelt.
      xml_parser_set_option($p, XML_OPTION_CASE_FOLDING, 0);
      // Wichtig, daß der Parser nicht globale Funktionen
      // aufruft, sondern Methoden dieser Klasse.
      xml_set_object($p, $this);

      xml_set_element_handler($p, 'startElement', 
                              'closeElement');
      xml_set_character_data_handler($p, 'cdataHandler');
      // Setzt den Handler für Processing Instructions.
      xml_set_processing_instruction_handler($p, 'piHandler');
      xml_parse($p, $buffer);
      xml_parser_free($p);
    }
  }
    
  function startElement($parser, $name, $a) {
    // Wie im ersten Beispiel, allerdings wird die Klassen-
    // Variable $html anstelle der globalen benutzt.
    switch ($name) {
    case 'tutorial':
      $this->html .= '<h1>'.$a['titel'].'</h1>';
      break;
    case 'abschnitt';
      $this->html .= '<h2>'.$a['titel'].'</h2>';
      break;
    case 'index':
      $this->html .= '<a name="'.$a['keyword'].'"></a>';
      break;
    case 'ref':
      $this->html .= '<a href="#'.$a['id'].'">';
      break;
    case 'code':
      $this->html .= '<pre>';
      break;
    default:
      $error = 'Undefiniertes Element &lt;'.$name.'&gt;';
      $line = xml_get_current_line_number($parser);
      $this->error($error.' in Zeile '.$line);
    }
  }

  function closeElement($parser, $name) {
    switch ($name) {
    case 'ref':
      $this->html .= '</a>';
      break;
    case 'code':
      $this->html .= '</pre>';
      break;
    }
  }

  function cdataHandler($parser, $cdata) {
    $this->html .= $cdata;
  }

  function piHandler($parser, $target, $data) {
    switch ($target) {
    case 'php':
      // Es wurde eine Codestelle gefunden, die
      // ausgeführt werden soll. Zuerst starten
      // wir den Ausgabepuffer, damit die gesamte
      // Ausgabe eingefangen werden kann.
      ob_start();
      // Ausführen des PHP-Codes
      eval($data);
      // "Einsammeln" der Ausgabe
      $output = ob_get_contents();
      // Ausgabe verwerfen
      ob_end_clean();
      // Anhängen der Ausgabe an $html:
      $this->html .= '<b>Ausgabe:</b><br />';
      $this->html .= $output;
      break;
    }
  }

  function error($str) {
    // Ausgeben einer Fehlermeldung:
    die('<b>Fehler:</b> '.$str);
  }

  function get() {
    // Gibt den erzeugten HTML-Code zurück.
    return $this->html;
  }

  function show() {
    // Gibt den erzeugten HTML-Code aus.
    print $this->get();
  }
}

Zuletzt brauchen wir noch ein kleines Script, das die Klassenfunktionen benutzt.

include_once "tp.php"; // Einbinden der Klasse

$tp = new TutorialParser('tutorial.xml');
$tp->show();

Christoph Reeg