XML::Parser

(Auszug aus "Perl & XML" von Erik T. Ray & Jason McIntosh)

Ein anderer früher Parser ist XML::Parser, der erste schnelle und effiziente Parser im CPAN. Wir haben einige seiner Fähigkeiten bereits in XML-Grundlagen: Lesen und Schreiben vorgestellt. Speziell der eingebaute Modus »stream« lohnt aber noch eine nähere Betrachtung. Kehren wir also zu einem vernünftigen Beispiel von Eventströmen zurück.

Wir werden XML::Parser benutzen, um eine in einem XML-Dokument stehende Liste von Adressen zu lesen. Die Adressen enthalten unter anderem die Namen der Kontaktpersonen, Straßen und Telefonnummern. Während der Parser die Datei lädt, speichert unser eigener Handler die gelesenen Informationen in einer speziellen Datenstruktur, damit sie später weiterverarbeitet werden können. Wenn der Parser sein Werk getan hat, sortiert das Programm die Adressen nach den Namen der Kontaktpersonen und gibt sie in Form einer HTML-Tabelle aus.

Das Eingabedokument sehen Sie im folgenden Beispiel. Als Wurzelelement dient das äußere <liste>-Element. Es enthält seinerseits vier <kontakt>-Elemente. Jedes dieser Elemente enthält einen Namen, eine Telefonnummer und eine Adresse.

Beispiel: Eine Adreßbuchdatei

<?xml version="1.0" encoding="ISO-8859-1"?>
<liste>
    <kontakt>
        <name><vorname>Taddäus</vorname><nachname>Troll</nachname></name>
        <telefon>0711 5623723</telefon>
        <adresse>
            <strasse>Schwanstr. 8</strasse>
            <stadt>Stuttgart</stadt><plz>70432</plz>
        </adresse>
    </kontakt>
    <kontakt>
        <name><vorname>Beppo</vorname><nachname>Brem</nachname></name>
        <adresse>
            <strasse>Dahlienweg 17</strasse>
            <plz>80131</plz>
            <stadt>München</stadt>
        </adresse>
        <telefon>089 736862</telefon>
    </kontakt>
    <kontakt>
        <name><nachname>Kabel</nachname>
            <vorname>Heidi</vorname></name>
        <adresse>
            <strasse>Am Alsterwasser 11</strasse>
            <stadt>Hamburg<stadt><plz>20457</plz>
        </adresse>
        <telefon>040 342946</telefon>
    </kontakt>
    <kontakt>
        <adresse>
            <strasse>Schnabelgasse 27b</strasse>
            <stadt>Stuttgart</stadt><plz>70113</plz>
        </adresse>
        <name><vorname>Willi</vorname><nachname>Reichert</last></nachname>
            <telefon>0711 2987234</telefon>
    </kontakt>
</liste>

Diese einfache Struktur bietet sich für die Eventverarbeitung an. Jedes <kontakt>-Start-Tag signalisiert die Einrichtung einer neuen Datenstruktur, in der die Daten zu speichern sind. Ein </kontakt>-End-Tag gibt an, daß der Datensatz vollständig ist und gespeichert werden kann. Die Kinder der <kontakt>-Elemente geben offensichtlich an, welche Informationen der Handler jeweils entgegennimmt und wo er sie zu speichern hat. Die einzelnen Kontakte sind voneinander unabhängig: Es gibt keine Links nach vorne oder hinten ins Dokument.

Das eigentliche Programm finden Sie im nächsten Beispiel. Am Anfang wird zunächst der Parser initialisiert und mit Referenzen auf verschiedene Subroutinen versehen, den Handlern der einzelnen Events. Wir bezeichnen solche Subroutinen als Callback, weil sie erst von Ihnen geschrieben und dann vom Parser für die Behandlung eines Events aufgerufen werden.

Nach der Initialisierung werden einige globale Variablen definiert, in denen wir die aus den XML-Elementen gelesenen Informationen speichern wollen. Diese Variablen geben den Handlern gewissermaßen ein Gedächtnis. Die Speicherung von Informationen für die spätere Verarbeitung wird oft auch als Sicherung eines Status bezeichnet. Der Parser greift später auf die alten Statusinformationen zurück.

Nach etwas Logik zum Lesen der Daten und zum Aufruf des Parsers besteht der Rest des Programms aus den eigentlichen Handlern. Wir wollen fünf Events behandeln: Start und Ende eines Dokuments, Start und Ende eines Elements sowie Textdaten. Andere Events wie Kommentare, PIs usw. werden ignoriert.

Beispiel: Ein Programm zur Ausgabe von Adressen

# Initialisierung des Parsers mit Referenzen auf die Handlerfunktionen
#
use XML::Parser;     
my $parser = XML::Parser->new( Handlers => {
     Init => \&handle_doc_start,
     Final => \&handle_doc_end,
     Start => \&handle_elem_start,
     End => \&handle_elem_end,
     Char => \&handle_char_data,
});

#
# Globale Variablen
#
my $record; # Zeiger auf einen Hash mit Elementinhalten
my $context; # Name des aktuellen Elements
my %records; # Menge von Adreßeinträgen

#
# Daten durch Parser lesen lassen
#
my $file = shift @ARGV;
if( $file ) {
     $parser->parsefile( $file );
} else {
     my $input = "";
     while( <STDIN> ) { $input .= $_; }
     $parser->parse( $input );
}
exit;

###
### Handlerfunktionen
###
#
# Wenn die Verarbeitung beginnt, wird der Kopf einer HTML-Tabelle ausgegeben.
#
sub handle_doc_start {
     print "<html><head><title>Adressen</title></head>\n";
     print "<body><h1>Adressen</h1>\n";
}

#
# Speicherung von Elementnamen und Attributen
#
sub handle_elem_start {
     my( $expat, $name, %atts ) = @_;
     $context = $name;
     $record = {} if( $name eq 'kontakt' );
}

# Zeichendaten werden dem jeweils aktuellen Element zugewiesen
#
sub handle_char_data {
my( $expat, $text ) = @_;
# Minimale Behandlung von HTML-Sonderzeichen
$text =~ s/&/&amp;/g;
$text =~ s/</&lt;/g;
$record->{ $context } .= $text;
}

#
# Falls dies ein <kontakt>-End-Tag ist, erzeuge einen neuen Datensatz.
#
sub handle_elem_end {
     my( $expat, $name ) = @_;
     return unless( $name eq 'kontakt' );
     my $fullname = $record->{'nachname'} . "," . $record->{'vorname'};

       $records{ $fullname } = $record;
}

#
# Ende der Daten, gib die gelesenen Adressen aus.
#
sub handle_doc_end {
     print "<table border='1'>\n";
     print "<tr><th>Name</th><th>Telefon</th><th>Adresse</th></tr>\n";
     foreach my $key ( sort( keys( %records ))) {
          print "<tr><td>" . $records{ $key }->{ 'vorname' } . ' ';
          print $records{ $key }->{ 'nachname' } . "</td><td>";
          print $records{ $key }->{ 'telefon' } . "</td><td>";
          print $records{ $key }->{ 'strasse' } . ', ';
          print $records{ $key }->{ 'stadt' } . ', ';
          print $records{ $key }->{ 'plz' } . "</td></tr>\n";
     }
     print "</table>\n</div>\n</body></html>\n";
}

Um das Programm zu verstehen, müssen wir uns auf die Handlerfunktionen konzentrieren. Alle von XML::Parser aufgerufenen Handler erhalten als erstes Argument die Referenz auf ein Expat-Parserobjekt. Das gibt dem Entwickler die Möglichkeit, gegebenenfalls von diesem Objekt weitere Informationen abzurufen, zum Beispiel die aktuelle Zeilennummer in der Eingabedatei. Abhängig vom Typ des Events, werden weitere Informationen übergeben. Zum Beispiel bekommt der Handler des Start-Tag-Events einen Elementnamen als zweites Element, gefolgt von einer Liste aus Attributnamen und -werten.

Unsere Handler benutzen globale Variablen zur Speicherung von Informationen. Wenn Sie globale Variablen nicht so lieben, können Sie statt dessen ein Objekt erzeugen, das die Informationen intern speichert. Dafür gibt es gute Gründe, globale Variablen sind oft schwer zu debuggen und im Zeitalter von Threads oft gefährlich. In diesem Fall kann man als Handler anonyme Codereferenzen übergeben, die die Methoden des Objekts aufrufen und die übergebenen Daten weiterreichen. Wir bleiben für den Moment bei globalen Variablen, weil sie das Verständnis des Beispiels erleichtern.

Der erste Handler, handle_doc_start, wird aufgerufen, wenn der Parser mit seiner Arbeit beginnt. Das ist eine bequeme Möglichkeit, um noch etwas zu erledigen, bevor die eigentliche Verarbeitung losgeht. Zum Beispiel kann man den Handler initialisieren und dadurch mehrfach verwenden. In unserem Fall wird einfach der Kopf einer HTML-Tabelle ausgegeben. Dieser Handler bekommt keine speziellen Argumente.

Der nächste Handler, handle_elem_start, wird aufgerufen, wenn der Parser den Start eines Elements meldet. Abgesehen von der obligatorischen Expat-Referenz, erhält dieser Handler zwei Argumente: den Elementnamen $name sowie die Variable %atts, einen Hash aus Attributnamen und -werten. (Beachten Sie, daß in einem Hash die Reihenfolge der Schlüssel verlorengeht. Wenn die Reihenfolge der Attribute also für Sie von Bedeutung sein sollte, dann verwenden Sie statt dessen eine Variable @atts, aus der Sie in einer Schleife je zwei Elemente für Name und Wert extrahieren.) In diesem einfachen Beispiel verwenden wir die Attribute gar nicht, aber wir merken uns die Möglichkeit für später.

Der Handler initialisiert die Verarbeitung eines Elements, indem er den Namen in der Variable $context speichert. Diese Variable enthält also stets den Namen des zuletzt geöffneten Elements bzw. den Namen des aktuellen Elements, wenn dieses keine Kinder hat. Wir benötigen diese Information später bei der Verarbeitung von Textdaten. Falls das geöffnete Element den Typ <kontakt> hat, wird außerdem ein Hash %record initialisiert, den wir mit Werten wie Nachname, Straße usw. füllen wollen.

Der dritte Handler, handle_char_data, kümmert sich um Textdaten. Der Text wird im zweiten Argument des Handlers als $text übergeben. Der Handler hat eine recht einfache Aufgabe: Er speichert den Text in $record->{ $context } . Dies funktioniert nur, weil der Handler des Startelement-Events diese Variablen initialisiert hat. Beachten Sie, daß wir die Variable $text anhängen und einen eventuell bereits vorhandenen Wert nicht überschreiben. XML::Parser hat die Eigenschaft, diesen Handler bei Zeilenenden aufzurufen. Wenn ein Element einen Text enthält, der über mehrere Zeilen geht, wird der Handler für dieses Element also mehrfach aufgerufen, einmal pro Zeile.

Anmerkung: Text auf diese Weise zu lesen ist typisch für Perl. XML-Puristen sind über diesen Umgang mit Textdaten wohl eher überrascht. XML kümmert sich normalerweise nicht um Zeilenenden, tatsächlich sind sogar alle Arten von Leerzeichen bedeutungslos. Es sind einfach nur Textzeichen, die alle auf dieselbe Weise zu behandeln sind.

Würden wir den Text nicht anhängen, hätten wir am Ende nur die letzte Zeile als Wert.

Es ist nicht weiter überraschend, daß handle_elem_end beim Ende eines Elements aufgerufen wird. Dieser Handler bekommt als zweites Argument den Elementnamen, analog zum Handler des Startelement-Events. In den meisten Fällen ist hier nichts zu tun, mit Ausnahme des <kontakt>-Elements. An dieser Stelle sind nämlich die Informationen einer Adresse komplett, und wir können die Daten speichern. Das geschieht in diesem Fall durch Erzeugen eines Eintrags in einem anderen Hash. Die Schlüssel des Hashs sind die vollen Namen der Personen, damit die später erfolgende Sortierung erleichtert wird. Die Sortierung kann erst erledigt werden, wenn alle Adressen gesammelt sind. Wenn die Adressen nicht sortiert werden müßten, könnten wir bereits an dieser Stelle die Adresse in HTML ausgeben und müßten sie nicht abspeichern.

Zum Schluß unseres Beispiels sehen wir den Handler handle_doc_end. Er wird aufgerufen, nachdem der Parser das gesamte Dokument gelesen hat. Normalerweise geschehen hier Aufräumarbeiten, etwa die Freigabe von Ressourcen usw. In unserem Fall müssen wir noch etwas erledigen: Die Adressen müssen nach den Namen der Personen sortiert und in HTML ausgegeben werden. Abschließend muß die Tabelle beendet werden.

Dieses Beispiel war sehr einfach, weil wir im Prinzip nur eine flache Folge von Einträgen hatten, vergleichbar den Zeilen in einer Tabelle. Im allgemeinen ist XML aber erheblich komplexer. Es gibt sehr komplizierte Dokumentformate, in denen Abhängigkeiten vom Vater, dem Großvater oder sogar von noch weiter entfernten Ahnen eine Rolle spielen. Ein Gedächtnis für die Ahnen eines Elements erfordert aber eine etwas raffiniertere Datenstruktur, die wir in einem der nächsten Beispiele zeigen werden.

  

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Perl & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2003 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Perl & XML" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de