Elemente mit System

Perl und das Web gehören zusammen wie Oper und die Callas. Damit das auch in Zukunft so bleibt, arbeiten Perl-Entwickler an neuen Werkzeugen wie dem hier vorgestellten XML-Parser. Mit ihm lassen sich XML-Dokumente analysieren und in andere Formate wandeln.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 10 Min.
Von
  • Christian Kirsch
Inhaltsverzeichnis

Stylesheets sind eine Möglichkeit, die Extensible Markup Language (XML) heute schon im Web und anderswo zu nutzen. Eine freie Implementierung dieser Technik basiert auf James Clarks jade und nutzt DSSSL, das wiederum eine Implementierung des Lisp-Dialekts Scheme ist - nicht für jedermann die einfachste Lösung.

Perl-Anhänger können sich zur Verarbeitung von XML-Texten des ursprünglich von Larry Wall geschriebenen Moduls XML::Parser bedienen. Inzwischen arbeitet Clark Cooper an dessen Weiterentwicklung. Je nach Bedarf läßt es sich auf unterschiedliche Weise einsetzen. Im folgenden sollen einige Beispiele die Verwendungsmöglichkeiten illustrieren.

Am Anfang jedes Perl-Scripts zur XML-Verarbeitung ist ein Parser zu erzeugen. Dabei muß sich der Programmierer entscheiden, wie er sein Dokument analysieren will. Die new-Methode bietet zwei Einstellungen, die dies beeinflussen:

  • Mit Style wählt man eins der Standardverfahren aus, die Elemente des XML-Dokuments und den eigentlichen Text liefern.
  • Handlers erlauben die Verarbeitung sämtlicher Sprachelemente, also auch von Entities, DOCTYPE-Deklarationen und XML-Kommentaren.

Style bietet einige vorgefertigte Standardparser, die per Parameter beim Erzeugen wählbar sind. Einfachster Fall ist Debug, das lediglich die Struktur des Dokuments zeigt:

use XML::Parser;
$p = new XML::Parser(Style => ’Debug’);
$p->parsefile($ARGV[0]);

Als debug.pl benchmark.xml aufgerufen, gibt dieses Script etwa aus:

benchmarks \\ (date 10031999 tester hb \
manufact Hersteller machine Notebook \
cputype celeron mhz 266 l2cache 512 ram 64 \
os Linux: Red Hat 5.2 ccomp gcc fcomp g77)

benchmarks bench ixssba rechenwert mpdhry || #9;
benchmarks bench ixssba rechenwert mpdhry \\ ()
benchmarks bench ixssba rechenwert mpdhry nonr || 380

Hier sind alle Elemente der momentanen Hierarchie zu erkennen, angefangen mit benchmarks. Den Beginn eines neuen Elements kennzeichnet das nachgestellte \\, dem in Klammern die angegebenen Attribute folgen. Diese Darstellung ist etwas gewöhnungsbedürftig, beispielsweise lautet der Anfang der hier analysierten XML-Datei

<benchmarks>
<bench date="10031999" tester="hb"
manufact="Hersteller" machine="Notebook" ….

Was in der Debug-Ausgabe hinter benchmarks steht, sind also die Attribute des nächsten Elements, nämlich von bench. Zwei senkrechte Striche kennzeichnen den Beginn von Text innerhalb eines Elements. Im obigen Beispiel kommt direkt vor dem Element nonr ein Tabulator (#9;), innerhalb von nonr findet sich der Text "380". Druckbare ASCII-Zeichen tauchen immer als solche auf, alles andere erscheint als Dezimal- oder UTF-8-Code. Deshalb sieht man ein "schließlich" im XML-Dokument als schlie#xC3;#x9F;lich in der Ausgabe des Debug-Parsers. Die vollständige Benchmark-Datei ist mit DSSSL-Programm, Erklärungen und HTML-Ausgaben unter http://www.heise.de/ix/raven/Web/xml/cebit99 zu finden.

Geeignet ist dieser Parsertyp vor allem zur Kontrolle von XML-Dokumenten - allerdings nur in engen Grenzen, denn wie alle XML::Parser-Varianten validiert er das Dokument nicht. Man kann also nicht kontrollieren, ob es der angegebenen DTD (Document Type Definition) entspricht.

Ergiebiger als Debug ist der Stil Subs. Um ihn zu benutzen, ist für jedes interessierende Element ein Paar von Routinen zu schreiben. Die erste heißt genau wie das Element und wird vom Parser gerufen, wenn er das Start-Tag entdeckt. Die zweite Routine ruft er beim Eintreffen des Ende-Tags, sie trägt den Namen des Elements mit angehängtem "_". Eine Einsatzmöglichkeit dieses Parsertyps könnte so aussehen:

use XML::Parser;
$s = 0;
$p = new XML::Parser(Style => ’Subs’);
$p->parsefile($ARGV[0]);
print "$s Kapitel in $ARGV[0]\n";
sub chapter {
$s++;
}

Dieses Codestückchen zählt nur die im Dokument enthaltenen chapter-Elemente. Geht’s um mehr als um das Erstellen einer Statistik, müssen andere Style-Varianten her. Beispielsweise erzeugt Tree einen Baum des Dokuments. Diese Form der Darstellung kostet einerseits relativ viel Platz, da ja das gesamte Dokument gleichzeitig im Speicher gehalten wird. Andererseits ist der so erzeugte Baum relativ unhandlich, da er als mehrfach geschachteltes Array implementiert ist.

Einfacher gestaltet sich die Benutzung des Stream-Parsers. Hier muß der Entwickler eine Reihe von Perl-Routinen schreiben, die das XML::Parser-Modul ruft, wenn es entsprechende Elemente im Dokument entdeckt hat - ein typischer Fall von Callback. Die wichtigsten dieser Routinen sind StartTag, EndTag und Text. Die ersten beiden sorgen für die Behandlung aller Start- und End-Tags, die dritte kümmert sich um Text dazwischen. Findet XML:: Parser also <TAG> in seiner Eingabe, ruft es StartTag() auf, bei </TAG> analog EndTag() und für (fast) alles andere Text().

Wie man einen Stream-Parser benutzt, zeigt das Script in Listing 1. Es erzeugt aus einer einfachen Adressendatei in XML das unter anderem von Netscape genutzte vCard-Format [2]. Das Vorgehen ist denkbar simpel: StartTag() schreibt den Elementnamen in die globale Variable $key, Text() speichert den jeweiligen Eintrag im $key-Feld des Hash %vcard. Einziger Stolperstein: XML::Parser ruft Text() für alle Zeichen außerhalb von Tags auf - also auch für Leerzeichen und Zeilenvorschübe zwischen Elementen. Das Beseitigen von $key in EndTag() per undef hilft zusammen mit der Prüfung auf defined $key in Text(), diese Falle zu umgehen. adressen. xml zeigt einige Adressen im hier benutzen XML-Format.

Mehr Infos

LISTING 1

#!/usr/bin/perl
#
# Adressen in XML nach vCard-Format
# konvertieren
#
use XML::Parser;
use strict;
use vars qw($key %vcard);

my $p= new XML::Parser(Style => ’Stream’);
$p->parsefile($ARGV[0]);

sub StartTag {
my $p = shift;
$key = shift;
}

sub EndTag {
my $p = shift;
my $t = shift;
undef $key;
if ($t eq "vcard") {
printVcard(%vcard);
}
}

sub Text {
return if (! defined($key) || $key eq "vcard" || $key eq "adressen");
$vcard{$key} = $_;
}

sub printVcard {
my (%hash) = @_;
print "begin:vcard\n";
print "n:$hash{’nachname’};$hash{’vorname’}\n";
print "fn: $hash{’vorname’} $hash{’nachname’}\n";
if (exists $hash{’strasse’} && exists $hash{’plz’} &&
exists $hash{’ort’}) {
print "adr:;;$hash{’strasse’};$hash{’ort’};;$hash{’plz’};Germany\n";
}
print "tel;work:$hash{’telgesch’}\n" if (exists $hash{’telgesch’});
print "tel;fax;work:$hash{’faxgesch’}\n" if (exists $hash{’faxgesch’});
print "email;internet:$hash{’mail’}\n" if (exists $hash{’mail’});
print "end:vcard\n";
}
Mehr Infos

LISTING adressen.xml

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!-- Simple Adressendatei in XML -->
<!DOCTYPE adressen SYSTEM "adressen.dtd">

<adressen>
<vcard>
<vorname>Christian</vorname>
<nachname>Kirsch</nachname>
<strasse>Helstorfer Str. 7</strasse>
<plz>30625</plz>
<ort>Hannover</ort>
<telgesch>+49-511-5352-590</telgesch>
<telpriv>+49-30-xxxxxx</telpriv>
<faxgesch>+49-511-5352-361</faxgesch>
<faxpriv>+49-30-xxxxxx</faxpriv>
<mail>ck@ix.heise.de</mail>
</vcard>
<vcard>
<vorname>Henning</vorname>
<nachname>Behme</nachname>
<strasse>Helstorfer Str. 7</strasse>
<plz>30625</plz>
<ort>Hannover</ort>
<telgesch>+49-511-5352-374</telgesch>
<faxgesch>+49-511-5352-361</faxgesch>
<mail>hb@ix.heise.de</mail>
</vcard>
</adressen>

Ähnlich läßt sich die XML-Datei in HTML wandeln. Ein Beispiel zeigt Listing 2. Es fischt aus den Adressen lediglich den Vor- und Nachnamen sowie die EMail-Adresse heraus. Den Namen gibt es fett aus, für die EMail erzeugt es einen mailto-Link. Dabei sind im wesentlichen dieselben Routinen aktiv wie beim vorangehenden Script. Als Neulinge finden sich StartDocument() und EndDocument(), die das Parsermodul am Anfang beziehungsweise Ende des Dokuments aufruft. Sie erzeugen hier die Elemente, die ein HTML-Browser um eine gültige Datei herum erwartet: HTML, HEAD und BODY.

Mehr Infos

LISTING 2

#!/usr/bin/perl
#
# Vorname, Nachname und EMail-Adresse
# aus XML-Datei extrahieren, in HTML
# wandeln und EMail als mailto: Link eintragen
#
use XML::Parser;
use strict vars;
use vars qw($text %html);

%html = ( "vorname" => ["<b>", " </b>"],
"nachname" => [ "<b>", "</b>"],
"mail" => [ "<a href=mailto:", "> EMail</a>" ],
"vcard" => [ "", "<br>"],
);

my $p= new XML::Parser(Style => ’Stream’);
$p->parsefile($ARGV[0]);

sub StartDocument {
print "<HTML><HEAD><TITLE>Beispiel ".
"</TITLE></HEAD>\n<BODY>";
}

sub EndDocument {
print "</BODY></HTML>\n";
}

sub StartTag {}

sub EndTag {
my $p = shift;
my $key = shift;
return unless (exists $html{$key} );
print $html{$key}->[0] . $text . $html{$key}->[1] ;
$text="";
}


sub Text {
$text = $_;
}

Anders als beim vCard-Exempel ist die Funktion StartTag() diesmal untätig. Weglassen darf man sie allerdings nicht, da XML::Parser dann mit seiner Default-Routine jedes XML-Anfangs-Tag ausgeben würde. Die Entscheidung, welche XML-Elemente überhaupt interessieren, trifft diesmal der Hash %html. Er enthält einen Schlüssel für jedes zu berücksichtigende Element und ein Feld mit dem dafür auszugebenden Start- und End-Tag für das HTML-Dokument.

Für das Schreiben des eigentlichen HTML-Codes ist EndTag() zuständig. Falls diese Funktion kein passendes Element in %html findet, kehrt sie sofort tatenlos zurück. Andernfalls gibt sie das in diesem Feld definierte Start-Tag, die von Text() aufgesammelten Zeichen und das ebenfalls in %html festgelegte End-Tag aus. Da diese in einem anonymen Array gespeichert sind, greift die Routine per -> auf die jeweiligen Indizes zu.

Wem die Möglichkeiten des Stream-Parsers nicht genügen, kann auf den Style-Parameter verzichten und durch eigene Handler-Funktionen das gewünschte Verhalten erreichen. Das folgende Scriptchen beispielsweise gibt alle Kommentare einer XML-Datei samt Zeilennummer aus:

$p = new XML::Parser();
$p->setHandlers(Comment => \&comments);
$p->parsefile($ARGV[0]);

sub comments {
my $p = shift;
my $s = shift;
print "Zeile: " . $p->current_line . "’" . $s . "’\n";
}

Zur Verarbeitung von XML-Kommentaren wird hier die Subroutine comments vereinbart. Bei ihrer Definition kommt das erste Argument eines Handlers ins Spiel. Es ist eine Referenz auf den benutzten XML-Parser, mit deren Hilfe bei Bedarf nützliche Informationen über den jeweiligen Kontext zu erfahren sind - in diesem Fall die Zeilennummer des XML-Dokuments.

Um beispielsweise die Titel aller sect1-Elemente eines DocBook-Dokuments auszugeben, könnte man in EndHandler() folgendermaßen vorgehen:

my $p = shift;
my $el = shift;
my $con = $p->{’Context’};
if ($el eq ’title’ && $con->[-1] eq ’sect1’) {
print $p->current_line . ": $s\n";
}

$p ist eine Referenz auf einen Hash, dessen Schlüssel Context wiederum die Referenz auf ein Feld mit der aktuellen Elementhierarchie enthält. Auf das letzte Element dieser Hierarchie greift man wie in Perl üblich mit [-1] zu. Das ganze Feld könnte so aussehen:

$con[0]:  ’book’
$con[1]: ’chapter’
$con[2]: ’sect1’

Um nur die Überschriften von chapter-Elementen zu finden, würde man im obigen if-Statement sect1 durch chapter ersetzen und so weiter.

Bliebe noch zu klären, wie XML::Parser Attribute bereitstellt. Das folgende XML-Element

<bench date="10031999" tester="hb" \
manufact="Hersteller" machine="Notebook" \
cputype="celeron" mhz="266" l2cache="512" \
ram="64" os="Linux: Red Hat 5.2" ccomp="gcc" \
fcomp="g77">

ist in StartTag() so zugänglich:

while ( ($key, $val) = each %_) {
print "$key: $val\n"
}

da der Parser alle Attribute im Hash %_ speichert. Auf einzelne Attribute greift man beispielsweise per $_ {"machine"} zu. Eine nach Attributen sortierte Ausgabe ließe sich etwa so erreichen:

foreach $k (sort keys %_) {
print "$k: $_{$k}
}

Zum Schluß noch eine Warnung: Wer deutsche XML-Texte schreibt, wird den einen oder anderen Umlaut nicht vermeiden können. Zumindest unter Unix sind diese Zeichen in der Regel entsprechend ISO8859-1 kodiert, und dies läßt sich am Anfang des Dokuments festlegen:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>

Die Style-Variante von XML::Parser respektiert diese Einstellung und liefert etwa in Text() die Zeichenketten im ISO-8859-1-Code ab. Anders jedoch bei den per setHandler definierten Callbacks. Ein Char-Handler bekommt Text außerhalb von Elementen unabhängig von der Kodierung des XML-Dokuments stets als UTF-8 zu sehen. Hier ist also gegebenenfalls ein wenig Korrekturarbeit nötig. Mit dem Perl-Modul Unicode::String ist das schnell erledigt:

use Unicode::string qw(latin1 utf8) 
Unicode::String->stringify_as(’utf8’);

am Anfang des Scripts stellt das Encoding für neue Strings auf UTF-8 ein. Im Char-Handler sorgt dann

$s = new Unicode::String($d); 
$s = $s->latin1();

dafür, daß der Text $d in der Variablen $s als ISO-8859-1 steht.

[1] Henning Behme; Kunst der Stunde; Wozu die Extensible Markup Language gut ist; iX 2/99; S. 36 ff.

[2] vCard-Spezifikation (http://www.imc.org/pdi/)

Mehr Infos

iX-TRACT

  • XML::Parser liest und analysiert XML-Dokumente. Sogenannte Styles bieten den Zugriff auf Elemente per Array oder Callbacks, sie eignen sich vor allem zur Konvertierung in andere Formate wie HTML.
  • Für spezielle Anwendungen, etwa die Verarbeitung von Kommentaren, kann man unter Verzicht auf Styles eigene Routinen vorgeben, die der Parser beim Finden des entsprechenden Objekts aufruft.
  • Beim Verzicht auf Styles liefert XML::Parser den eigentlichen Text UTF-8-kodiert. Mit dem Perl-Modul Unicode::String läßt er sich in Latin1 konvertieren.

(ck)