Aus Linux-Magazin 08/2005

XML-Parser für Perl im Vergleich

XML ist ein nicht überall heiß geliebtes, aber viel genutztes Austauschformat. Für Perl gibt es sehr viele Erweiterungen, um bequem in diesem Datenmeer zu fischen. Der heutige Snapshot diskutiert Vor- und Nachteile der gängigen XML-Module — das jeweils zweckmäßigste ist dann schnell gefunden.

Seinem inoffiziellen Leitspruch bleibt Perl auch treu, wenn es um das Verarbeiten von XML-Dokumenten geht: There is more than one way to do it – es gibt mehr als einen Weg für diese Aufgabe. Die verschiedenen Module sollen anhand des Beispiels von Abbildung 1 illustriert werden. Das File enthält zwei Datensätze vom Typ »<cd>« in einem »<result>«-Tag. Jeder dieser Datensätze besteht seinerseits aus Tags für »<artists>« und »<title>« einer CD, wobei »<artists>« wiederum ein oder mehrere »<artist>«-Tags umschließen darf.

Schnell, einfach und auf einen Rutsch

Am einfachsten parst sich XML in Perl mit Hilfe des Moduls XML::Simple vom CPAN. Dieses Modul exportiert die Funktion »XMLin«, die eine komplette Datei oder einen String mit XML-Daten hereinholt und anschließend als Datenstruktur in Perl ablegt:

use XML::Simple;
my $ref = XMLin("data.xml");

Abbildung 2 zeigt einen Dump der resultierenden Datenstruktur in »$ref«. Zwei Dinge fallen auf: Abhängig davon, ob ein oder zwei Künstler in »<artists>« stehen, ist die resultierende Datenstruktur entweder ein Skalar oder ein Array. Das erschwert später die Arbeit.

Mit der Option »ForceArray« lässt sich jedoch festlegen, dass ein Feld immer als Array dargestellt wird. Der Aufruf »XMLin(“data.xml”, ForceArray => [\’artist\’]);« stellt sicher, dass »$ref->{cd}->[0]->{artists}->{artist}« ebenfalls eine Arrayreferenz zurückgibt, obwohl dort nur ein einziger Interpret steht.

Außerdem ist »->{artists}->{artist}« etwas umständlich zu schreiben, da »->{artists}« außer »->{artist}« keine weiteren Unterelemente enthält. XML::Simple bietet mit der »GroupTags«-Option die Möglichkeit, Hierarchien kollabieren zu lassen. Der Aufruf

XMLin("data.xml",
  ForceArray => ['artist'],
  GroupTags  =>
    {'artists' => 'artist'});

erzeugt die Datenstruktur in Abbildung 3, die schon sehr einfach zu handhaben ist. Beispielsweise lassen sich alle Seriennummern nun mit einer einfachen For-Schleife finden:

for my $cd (@{$ref->{cd}}) {
    print $cd->{serial}, "n";
}

XML::Simple liest die komplette XML-Datei in den Hauptspeicher ein, was oft praktisch ist, manchmal aber auch zum Problem wird: Bei riesigen XML-Dateien ist das nicht effizient oder schlichtweg unmöglich.

Verschlungene Pfade

Wer auf knapper und griffiger Notation besteht, um durch den XML-Dschungel zu navigieren, wird XPath lieben. Das Modul XML::LibXML vom CPAN hängt sich an die Libxml-2-Bibliothek des Gnome-Projekts an und bietet über die »findnodes«-Methode auch die Möglichkeit, per XPath-Notation auf XML-Elemente zuzugreifen.

Um zum Beispiel den Textinhalt aller »<title>«-Elemente zutage zu fördern, genügt die XPath-Notation »/result/cd/title/text()«, die mit »/« an der Dokumentenwurzel anfängt, in die »<results>«-, »<cd>«- und »<title>«-Elemente hinabsteigt und mit »text()« deren Textinhalt zurückgibt.

Abbildung 1: Der XML-Beispieldatensatz, der die hier vorgestellten XML-Module illustriert, zeigt ein CD-Archiv.

Abbildung 1: Der XML-Beispieldatensatz, der die hier vorgestellten XML-Module illustriert, zeigt ein CD-Archiv.

Alternativ geht es auch mit »//title/text()«, denn so stöbert XPath einfach alle »<title>«-Elemente in beliebiger Tiefe auf. Listing 1 zeigt, dass die Methode »findnodes()« eine Reihe von Textobjekten zurückgibt, deren Methode »toString()« den Titeltext liefert.

Aber mit XPath lassen sich auch wesentlich komplexere Aufgaben lösen: Listing 2 fieselt beispielsweise die Seriennummern aller CDs heraus, bei denen das Artist-Tag den String »Foo Fighters« enthält. Das leistet ein einzelnes, zunächst recht kompliziert wirkendes Konstrukt: »/result/cd/artists/artist[.=”Foo Fighters”]/../../@serial«. Der Ausdruck wird verständlich, wenn man ihn abschnittweise betrachtet.

Zuerst steigt XPath bis zu den »<artist>«-Tags hinab und prüft dann jedes mit »[.=”Foo Fighters”]«. Dieses in eckige Klammern eingeschlossene Prädikat referenziert mit ».« den aktuellen Knoten und kontrolliert, ob dessen Wert auch mit dem gesuchten String »Foo Fighters« übereinstimmt.

Trifft dies zu, fährt XPath mit »../..« anschließend zwei Etagen hoch. Auf dieser Ebene findet sich auch das CD-Tag. Dessen Parameter »serial« liest das Modul mittels »@serial« aus und gibt ihn zurück. Das Listing 2 »xpserial« muss dann nur noch die »value()«-Methode des zurückgelieferten Objekts bemühen, um den Wert des Parameters zu erhalten – in diesem Fall also die gesuchte Seriennummer der CD.

XPath erlaubt sehr prägnante und bündige Formulierungen. Doch sobald etwas nicht auf Anhieb funktioniert, kann die Fehlersuche viel Zeit verschlingen. Glücklicherweise bügelt Perl manchen Nachteil einer reinen XSLT-Umgebung wieder aus, weil es schnelle XPath-Hacks mit solider Programmlogik und seinen ausgezeichneten Debugging-Möglichkeiten kombiniert.

Listing 1:
»xptitles«

01 #!/usr/bin/perl -w
02 use strict;
03 use XML::LibXML;
04 
05 my $x = XML::LibXML->new() or
06     die "new failed";
07 
08 my $d = $x->parse_file("data.xml") or
09     die "parse failed";
10 
11 my $titles =
12     "/result/cd/title/text()";
13 
14 for my $title ($d->findnodes($titles)) {
15     print $title->toString(), "n";
16 }

Listing 2:
»xpserial«

01 #!/usr/bin/perl -w
02 use strict;
03 use XML::LibXML;
04 
05 my $x = XML::LibXML->new() or
06     die "new failed";
07 
08 my $d = $x->parse_file("data.xml") or
09     die "parse failed";
10 
11 my $serials = q{
12     /result/cd/artists/
13     artist[.="Foo Fighters"]/
14     ../../@serial
15 };
16 
17 for my $serial ($d->findnodes($serials)) {
18     print $serial->value(), "n";
19 }

XML::Parser

Einen eher klassischen Parser implementiert das Modul XML::Parser. Er beißt sich Tag für Tag durch ein XML-Dokument und ruft benutzerdefinierte Callbacks auf, sobald bestimmte Bedingungen eintreten. Um auch mit diesem Modul die Seriennummern aller CDs zu finden, deren Interpret Foo Fighters heißt, ist eine andere Strategie erforderlich. Jetzt muss der Code auf dem Weg in die Tiefen einer XML-Struktur auf jeder Etage den Status festhalten, um ihn später für Entscheidungen heranzuziehen.

Wie Listing 3 zeigt, erwartet der »XML::Parser«-Konstruktor »new()« callbacks für Ereignisse wie »Start« (der Parser trifft auf ein öffnendes Tag) oder »Char« (der Parser findet eingeschlossenen Text). Stößt der Parser auf ein öffnendes Tag wie »<cd serial=”001″>«, ruft er die ab Zeile 16 definierte Funktion »start()« mit einer Referenz auf den Parser, den Tag-Namen und eine Key/Value-Liste von Attributen auf. Im Beispiel erhält die Funktion »start()« als zweiten Parameter den String »cd« und als dritten und vierten »serial« und »001«.

Listing 3:
»xmlparse«

01 !/usr/bin/perl -w
02 use strict;
03 use XML::Parser;
04 
05 my $p = XML::Parser->new();
06 $p->setHandlers(
07     Start => &start,
08     Char  => &text,
09     );
10 $p->parsefile("data.xml");
11 
12 my $serial;
13 my $is_artist;
14 
15 ###########################################
16 sub start {
17 ###########################################
18   my($p, $tag, %attrs) = @_;
19 
20   if($tag eq "cd") {
21       $serial = $attrs{serial};
22   }
23 
24   $is_artist = ($tag eq "artist");
25 }
26 
27 ###########################################
28 sub text {
29 ###########################################
30   my($p, $text) = @_;
31 
32   if($is_artist and
33      $text eq "Foo Fighters") {
34      print "$serialn";
35   }
36 }

Der ab Zeile 28 definierte Callback »text()« bekommt von XML::Parser bei gefundenen Textstücken hingegen zwei Parameter: eine Referenz auf den Parser und einen String, in dem der gefundene Text steht.

Wo bin ich?

Damit der Parser erfährt, dass ein gefundenes Textstück der Name eines Interpreten ist, muss er prüfen, ob er sich gerade innerhalb eines »<artist>«-Tag befindet. Das weiß er nur, weil der »start«-Callback die globale Variable »$is_artist« vorher entsprechend gesetzt hat. Genauso rettet die Variable »$serial« die Seriennummer, die »start« im »serial«-Attribut des »<cd>«-Tag fand, in den Aufruf von »text()« hinüber.

So kann die »print«-Funktion dort die Seriennummer der gegenwärtig untersuchten CD ausgeben. Dieses Verfahren setzt natürlich voraus, dass jede CD ein »<serial>«-Attribut führt, aber das lässt sich in einem vorausgehenden Validierungsschritt zum Beispiel mit einer DTD überprüfen.

Das Modul XML::Parser wird meist nicht direkt, sondern als Basisklasse von selbst gezimmerten Klassen genutzt. Auch das anfangs besprochene XML::Simple greift – je nach Installationsumgebung – per »$XML::Simple::PREFERRED_PARSER = “XML::Parser”;« darauf zurück.

Für problematische Plattformen ist XML::SAX::PurePerl, eine weitere Parser-Alternative vom CPAN, eine akzeptable Wahl, wenn auch nicht die schnellste. Dafür lässt sich dieses Modul, das nur aus Perl-Code besteht, aber auch ohne einen C-Compiler zum Laufen bringen.

Die Installation von XML::Parser setzt außerdem einen zuvor ordnungsgemäß installierten »expat«-Parser voraus und kostet damit mehr Einrichtungszeit. Wer sich diese Arbeit sparen will, missbraucht einfach das Modul HTML::Parser für XML. Dessen Syntax ist nur geringfügig anders und mit gesetztem »xml_mode« schaltet es von der etwas schlampigen HTML-Interpretation in die strenge XML-Welt um.

Abbildung 2: XML::Simple übersetzt die Beispieldaten in diese Datenstruktur, auf die Perl einfach zugreifen kann.

Abbildung 2: XML::Simple übersetzt die Beispieldaten in diese Datenstruktur, auf die Perl einfach zugreifen kann.

Abbildung 3: Mit Hilfe spezieller Group-Tags verflacht XML::Simple Hierarchien und sorgt für eine übersichtliche Gliederung.

Abbildung 3: Mit Hilfe spezieller Group-Tags verflacht XML::Simple Hierarchien und sorgt für eine übersichtliche Gliederung.

Erfolg mit falschem Werkzeug

In Listing 4 fällt auf, dass der »HTML::Parser«-Konstruktor eine geringfügig andere Syntax akzeptiert als »XML::Parser«. Nachdem die Version der genutzten API festgelegt ist, setzen die Parameter »start_h« und »text_h« die Callbacks für öffnende Tags und Textstücke zwischen XML-Tags. Weiter legt der Konstruktor fest, welche Parameter der Parser an die Callbacks weitergibt: »start()« erhält den Namen des aufgehenden Tag und eine Attributliste (diesmal als Referenz auf einen Array). Die Funktion »text()« bekommt aber lediglich das gefundene Textstück.

Listing 4:
»htmlparse«

01 #!/usr/bin/perl -w
02 use strict;
03 use HTML::Parser;
04 
05 my $p = HTML::Parser->new(
06   api_version => 3,
07   start_h  => [&start, "tagname, attr"],
08   text_h   => [&text, "dtext" ],
09   xml_mode => 1,
10 );
11 
12 $p->parse_file("data.xml") or
13     die "Cannot parse";
14 
15 my $serial;
16 my $artist;
17 
18 ###########################################
19 sub start {
20 ###########################################
21   my($tag, $attrs) = @_;
22 
23   if($tag eq "cd") {
24       $serial = $attrs->{serial};
25   }
26 
27   $artist = ($tag eq "artist");
28 }
29 
30 ###########################################
31 sub text {
32 ###########################################
33   my($text) = @_;
34 
35   if($artist and
36      $text eq "Foo Fighters") {
37      print "$serialn";
38   }

Listing 5:
»twig«

01 #!/usr/bin/perl -w
02 use strict;
03 use XML::Twig;
04 
05 my $twig= XML::Twig->new(
06   TwigHandlers => {
07     "/result/cd/artists/artist" => &artist
08   }
09 );
10 
11 $twig->parsefile("data.xml");
12 
13 ###########################################
14 sub artist {
15 ###########################################
16   my($t, $artist)= @_;
17 
18   if($artist->text() eq "Foo Fighters") {
19     my $cd =
20        $artist->parent()->parent();
21 
22     print $cd->att('serial'), "n";
23   }
24       # Release memory of processed tree
25       # up to here
26   $t->purge();
27 }

Tanz den Twig

Eine erstaunlich effektive Abbildung von XML-Datenstrukturen in Perl-Code bietet XML::Twig von Michel Rodriguez. Es verarbeitet auch monströse Dokumente, bei denen XML::Simple aussteigt, denn es liest sie nur stückchenweise und nie vollständig in den Speicher ein.

XML::Twig bietet so viele verschiedene Methoden, um durch XML zu navigieren, dass es schwer fällt, die am besten geeignete zu finden. Listing 5 zeigt den Aufruf des Konstruktors »XML::Twig::new« mit dem Parameter »TwigHandlers«, der dem XML-Pfad »/result/cd/artists/artist« den ab Zeile 14 definierten Handler »artist« zuweist. Sobald XML::Twig beim Parsen des XML-Dokuments auf ein »<artist>«-Tag trifft, ruft es die Funktion »artist« mit zwei Parametern auf. Ein Parameter ist ein »XML::Twig«-Objekt, der andere ein »XML::Twig::Elt«-Objekt (Elt steht wohl für Element). Letzteres repräsentiert den XML-Baumknoten, an dem das »<artist>«-Tag hängt.

Die Methode »text()« des Artist-Objekts liefert den Text zwischen dem öffnenden und dem schließenden »<artist>«-Tag. Steht dort »Foo Fighters«, navigiert Zeile 20 zum darüber liegenden »<cd>«-Tag, indem es zweimal die »parent()«-Methode ausführt. Das so gefundene CD-Objekt fragt dann mit der Methode »att()« nach dem Wert des Attributs »serial« und gibt anschließend den ermittelten Wert aus.

Jedes Mal, wenn wieder ein Artist-Tag fertig abgearbeitet ist, ruft die Funktion »artist()« die Methode »purge()« des XML-Twig-Objekts auf, um ihm mitzuteilen, dass der Baum bis zum aktuell bearbeiteten Tag nicht mehr gebraucht wird und dieser Teil deswegen zur Freigabe ansteht. XML::Twig ist so intelligent, direkte Eltern des gerade bearbeiteten Tag nicht wegzuputzen. Bereits bearbeitete Geschwister fallen hingegen der Müllabfuhr zum Opfer. Bei einem kurzen XML-Stück ist dieses Speichermanagement witzlos, bei einem riesigen Dokument kann es aber den Ausschlag geben, ob ein Programm noch funktioniert oder nicht.

Abbildung 4: Twigfilter gibt das modifizierte XML aus.

Abbildung 4: Twigfilter gibt das modifizierte XML aus.

Mit Namensänderung

XML::Twig navigiert aber nicht nur elegant in einem XML-Dokument herum. Ein Skript kann gleichzeitig auch Tags umbenennen, den Baum mit Methodenaufrufen dynamisch verändern oder sogar Teile abstoßen, um Speicherplatz zu sparen.

Um zum Beispiel im vorliegenden XML-Dokument aus den »cd«-Tags die »serial= \’xxx\’«-Attribute der Form »<cd serial=”xxx”> … </cd>« in Unterelemente der Form »<cd><id>xxx</id> … </cd>« umzuwandeln und zugleich die Artist-Informationen zu tilgen, holt das Skript »twigfilter« in Listing 6 zunächst mit »root()« das Wurzelobjekt hervor. Danach liefert die Methode »children()« alle Kindobjekte des Wurzelobjekts, also die »cd«-Elemente. Deren »serial«-Attribute transformiert die Methode »att_to_field()« in Feldelemente mit dem Namen »id«.

Anschließend holt »first_child()« das erste und einzige »artist«-Element hervor, dessen »delete()«-Methode den Knoten selbst zerstört und aus dem Baum ausklinkt. Schließlich benennt die »set_gi()«-Methode (»gi« steht für Generic Identifier) das »cd«-Objekt des gerade durchlaufenen »<cd>«-Tag in »<Compact Disc>« um. In Abbildung 4 ist das Ergebnis dargestellt.

Wegen des auf »indent« gesetzten »PrettyPrint«-Parameters im Konstruktor gibt die in Zeile 19 aufgerufene Methode »print()« den Ergebnisbaum schön eingerückt aus.

Mit XML::Twig lassen sich unglaublich kompakte Programme schreiben, es erfordert lediglich etwas Übung, um die richtigen Methoden zu finden.

XML::XSH

Wer gerne interaktiv herumprobiert, für den gibt es die Xsh-Shell des Moduls XML::XSH. Mit »xsh« aufgerufen öffnet sich ein Kommando-Interpreter, mit dem man XML-Dokumente von der Festplatte lesen oder aus dem Web holen kann. Anschließend lassen sich beliebig komplexe XPath-Abfragen abfeuern. Die Ergebnisse liegen sofort als Kommandoausgaben vor und erlauben es, die Queries fortlaufend zu verfeinern.

Abbildung 5 zeigt, wie der Shell-Benutzer zunächst mit »open docA = “data.xml”« das XML-Dokument von der Platte einliest und dann mit dem Kommando »ls« eine XPath-Abfrage abfeuert. Deren Ergebnis zeigt eine einzelne Seriennummer mit »serial=\’002\’«.

Das waren nur einige ausgewählte Beispiele aus der Vielzahl verfügbarer XML-Module vom CPAN. XML::XPath, XML::DOM, XML::Mini, XML::Grove wären weitere Möglichkeiten aus dem schier unerschöpflichen Brunnen. Für jeden Geschmack und jede Aufgabe wird sich etwas Passendes finden. (jcb)

Abbildung 5: XPath-Abfragen können in der Xsh-Shell interaktiv eingegeben und ausgewertet werden.

Abbildung 5: XPath-Abfragen können in der Xsh-Shell interaktiv eingegeben und ausgewertet werden.

Listing 6:
»twigfilter«

01 #!/usr/bin/perl -w#
02 use strict;
03 use XML::Twig;
04 
05 my $twig= XML::Twig->new(
06     PrettyPrint => "indented");
07 
08 $twig->parsefile("data.xml") or
09     die "Parse error";
10 
11 my $root = $twig->root();
12 
13 for my $cd ($root->children('cd')) {
14     $cd->att_to_field('serial', 'id');
15     $cd->first_child('artists')->delete();
16     $cd->set_gi("CompactDisc");
17 }
18 
19 $root->print();

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2005/08/Perl]

[2] Tutorial zu XML::Twig: [http://www.xmltwig.com/xmltwig/tutorial/index.html]

Der Autor


Michael Schilli arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. Seine Homepage: [http://perlmeister.com]

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben