freeX: Komplexe Datenstrukturen und XML in Perl, Teil 7
|
Perl hier, Perl da, Perl ist immer da!
| |
Wenn Daten aus dem tatsächlichen Leben mit dem Computer verarbeitet
werden sollen, müssen teilweise sehr komplexe Datenstrukturen
aufgebaut werden. Früher wurde dazu oftmals C, C++ oder Pascal mit
komplexen, größtenteils jedoch statischen, Datenstrukturen
verwendet. Mit Hilfe von Referenzen, anonymen Arrays und Hashes
sowie den daraus gebildeten komplexen Datenstrukturen läßt sich
dieses auch mit Perl erreichen - bei erheblich geringerem Aufwand.
|
|
1. Referenzen |
Referenzen
In der letzten Ausgabe der freeX wurden Referenzen auf Variablen eingeführt. Bei Referenzen handelt es sich um Zeiger auf Variablen, Arrays oder Hashes. Zusammen mit anonymen Arrays und Hashes ist dieses die Voraussetzung dafür, komplizierte und verschachtelte bzw. hierarchische Datenstrukturen aufzubauen. Dabei kommt natürlich auch die Eigenschaft von Perl zum Tragen, Datenstrukturen bei Bedarf automatisch zu vergrößern.
Eine Referenz selbst ist wiederum eine skalare Variable, die anderen Variablen zugewiesen werden kann. Genauso wie statische Daten (z.B. Zahlen oder Zeichenketten) dürfen Referenzen auch in Arrays und Hashes verwendet werden. So lassen sich leicht Arrays von Referenzen bilden, dessen Referenzen sich zu Arrays, Hashes oder skalaren Variablen auflösen lassen, und so weiter.
Am Ende dieses Artikel sollen XML-Dateien geparst werden. XML wird von vielen aktuellen Programmen als Dateiformat oder zum Austausch von Daten verwendet. XML selbst beschreibt dabei nur die generelle Struktur der Daten, die Programme haben weiterhin viele Freiheiten, wie sie organisiert werden. Abiword und Gnumeric verwenden so beispielsweise XML als Dateiformat, teilweise zusätzlich mit »gzip« komprimiert.
Wenn kein selbst geschriebener Parser für die spezielle Struktur der Daten verwendet wird, der einen Teil der Struktur erst gar nicht auf die Datenstruktur abbildet, erzeugt selbst ein kleines Beispiel recht komplexe Datenstrukturen. Die hier verwendeten Beispiele sind recht klein, erzeugen jedoch schon ziemlich komplexe Baumstrukturen, die aus Arrays und Hashes mit aktuellen Daten bestehen.
Verschachtelte Datenstrukturen
Zur Einführung in die Arten des Zugriffs auf Elemente dieser Baumstrukturen soll folgendes Beispiel dienen. Das Zeitschriftenregister aus der letzten Ausgabe wird dabei erweitert. Jetzt soll nicht nur zu jeder Seite, auf der ein Artikel angefangen wird, der Titel gespeichert werden, sondern gleichzeitig der Name des Autors und die Anzahl der Seiten dieses Artikels. Die nebenstehende Abbildung gibt einen Eindruck davon.
Im Gegensatz zur vorherigen Version wird jetzt ein assoziatives Array (Hash) verwendet, dessen Elemente wieder Assoziative Arrays sind. Aufgebaut wird die Datenstruktur ähnlich wie in der letzten Ausgabe, allerdings wird für jeden Artikel ein zweites anonymes Hash verwendet.
$hash = {7 => { title => "SMS mit Linux",
author => "Jens Maske",
pages => 4 },
94 => { title => "Netzwerkverbindungen mit Perl",
author => "Martin Schulze",
pages => 6 }
};
push (@freeX, $hash);
Abbildung 1: Das Zeitschriftenregister grafisch dargestellt
Mit dem Konstrukt »{}« wird eine Referenz auf ein neues Hash, ein sogenanntes anonymes Hash, erzeugt. Es ist nicht an eine Variable gebunden und kann beliebig zugewiesen werden. In diesem Fall wird ein Hash mit 7 und 94 als Indizes angelegt und die Referenz dazu in »$hash« gespeichert. Als Wert für jeden Index wird erneut ein Hash angelegt, mit Titel, Autor und Seitenzahl als Index. Zur Verdeutlichung der so erzeugten Datenstruktur, soll die Datenstruktur mit dem Data-Dumper ausgegeben werden. Der nebenstehende Screenshot zeigt die Ausgabe. Sie wird mit folgenden Anweisungen erzeugt:
use Data::Dumper; print Dumper (\@freeX);
Um gleich eine Warnung auszusprechen: Wenn XML-Daten gelesen werden, erzeugen die Parser erheblich größere Datenstrukturen, wie später zu sehen sein wird.
Komplexe Datenstrukturen
Komplexe Datenstrukturen wären nutzlos, wenn im Programm nicht auf jede einzelne Komponente zugegriffen werden könnte. Während in einer Hochsprache wie C wilde Konstruktionen mit Zeigern erstellt werden und Speicherplatz behutsam wieder freigegeben werden muß, geschieht es in Perl etwas einfacher. Speicherplatz muß nicht mehr alloziiert und freigegeben werden, das erledigt Perl von sich aus. Die Referenzen in den Datenstrukturen müssen jedoch aufgelöst werden. Dabei muß allerdings genau darauf geachtet werden, ob es sich um ein Array, ein Hash oder eine skalare Variable handelt. Einen Fehler an dieser Stelle beantwortet Perl mit einem Laufzeitfehler, der das Programm terminieren läßt.
Um z.B. auf die Informationen zu einem bestimmten Artikel zuzugreifen, muß zuerst das assoziative Array gefunden werden, das die Seitenzahlen als Index verwendet. Die Hauptstruktur ist ein herkömmliches Array, das als Elemente (abgesehen vom nullten) Referenzen auf assoziative Arrays enthält. Um das Hash der ersten Ausgabe zu erhalten, wird die Referenz einer skalaren Variablen zugewiesen und anschließend dereferenziert:
$ausgabe = $freeX[1]; %ausgabe = %$ausgabe;
Als Index dieses assoziativen Arrays werden Seitenzahlen verwendet, die Elemente sind wieder Refernzen auf Hashes. Um die Daten zu einer Ausgabe zu erhalten, muß also erneut wie oben dereferenziert werden, allerdings ist die Basis jetzt selbst ein Hash:
$artikel = $ausgabe{94};
%artikel = %$artikel;
Wenn diese Anweisungen geklappt haben, befinden sich jetzt in »%artikel« alle Informationen zu dem Artikel aus der freeX 1/01, der auf Seite 94 beginnt. In dem Fall müßte das Hash Indizes »title« und »author« haben, deren zugehörigen Elemente skalare Variablen sind. Sie werden wie folgt ausgegeben:
printf "Titel: %s\n", $artikel{"title"};
printf "Autor: %s\n", $artikel{"author"};
Diese Art, auf die Komponenten der Datenstruktur zuzugreifen, ist recht umständlich und für die automatische Verarbeitung in Schleifen unpraktisch. Wie in anderen Sprachen, kann auf die Komponenten auch direkt zugegriffen werden. In dem Fall werden wie in anderen Sprachen die Komponenten mit Pfeilen (»->«) verbunden und alle Indizes hintereinander geschrieben. Im obigen Fall sind die Indizes »[1]«, »{94}« und »{"title"}«. Damit kann die erste Zeile auch wie folgt geschrieben werden:
printf "Titel: %s\n", $freeX[1]->{94}->{"title"};
Zwischen den Indizes dürfen die Pfeile sogar entfallen, allerdings nur zwischen Indizes. Die zweite Zeile von oben könnte man daher auch so schreiben:
printf "Autor: %s\n", $freeX[1]{94}{"author"};
Beide Schreibweisen sind erheblich übersichtlicher als die weiter oben verwendete. Aus diesen geht automatisch die Struktur der Daten hervor. Die gleiche Schreibweise darf auch für Zuweisungen verwendet werden. Perl merkt automatich, daß anonyme Arrays oder Hashes erzeugt werden müssen, legt sie an und verbindet sie entsprechend.
Etwas trickreicher ist die Schreibweise, wenn nicht auf Elemente einer solchen Datenstruktur zugegriffen werden soll, sondern auf Teilstrukturen wie Arrays oder Hashes. In dem Fall müssen die einzelnen Komponenten nicht nur dereferenziert werden, sondern die Teilstruktur muß auch dementsprechend angesprochen werden.
Dazu benötigt Perl etwas Unterstützung. Es muß ein Block um das Resultat gebildet werden, damit Perl es als Hash oder Array ansprechen kann. Ein Block wird mit geschweiften Klammern »{}« gebildet, erst dann darf das Array- oder Hash-Zeichen geschrieben davor werden. Das Ergebnis eines Blockes ist das Ergebnis des zuletzt ausgewerteten Ausdrucks. Wenn »$freeX[2]« eine Referenz auf ein Hash ist, dann liefert »{$freeX[2]}« somit eine Referenz auf einen Hash zurück. Das assoziative Array wird daher mit »%{$freeX[2]}« angesprochen.
Mit diesem Konstrukt läßt sich die anfangs aufgebaute Datenstruktur automatisch bearbeiten. Auf das so erhaltene assoziative Array kann wie bisher zugegriffen werden, z.B. können die Indizes mit der Funktion »keys« extrahiert werden, um anschließend sortiert in einer Schleife verwendet zu werden. Wer den bisherigen Artikel verstanden hat, für den mutet das folgende Codefragment nicht mehr kryptisch an. So werden z.B. alle Artikel der Ausgabe 2/01, nach Seitenzahl sortiert, ausgegeben.
foreach $page (sort keys %{$freeX[2]}) {
printf "S. %d-%d: %s - %s\n", $page, $page + $freeX[2]{$page}{"pages"},
$freeX[2]{$page}{"author"}, $freeX[2]{$page}{"title"};
}
Beispiel: Security-Check
In diesem Beispiel wird das bisher erlangte Wissen über Perl angewendet. Das Programm soll herausfinden, welche Dateien sich in einem Datei-Archiv innerhalb des letzten Tages geändert haben. Normalerweise werden dort keine Dateien gelöscht, sondern nur hinzugefügt. Daher muß kein besonderes Augenmerk auf geänderte Dateien gelegt werden. Wenn sich Dateien ändern, ist es ein Fehler, dem ggf. nachgegangen werden muß.
Bei diesem Archiv könnte sich z.B. um ein Archiv mit Sicherheits-Updates handeln, in das nur neue Updates gelegt werden, in dem sich daher nichts unkontrolliert ändern darf. Jeden Tag wird zur Kontrolle eine Auflistung aller Dateien im Archiv mit Checksummen erstellt. Dieses wird automatisch auf einen anderen Rechner übertragen und dort analysiert bzw. mit der Datei vom Vortag verglichen. Änderungen sollen per Mail verschickt werden.
Im Archiv sind die Dateien in verschiedene Verzeichnisse aufgeteilt. Da sie für verschiedene Architekturen (intel, sparc, alpha etc.) zur Verfügung stehen, beschreibt das letzte Unterverzeichnis die jeweilige Architektur. Das Archiv ist also wie folgt organisiert:
main/i386/datei1 main/i386/datei2 main/sparc/datei3 main/sparc/datei4 ...
Die täglich erstellte Auflistung aller Dateien wird mit folgendem Befehl erstellt:
find main/ -type f | sort | xargs md5sum
Das Programm »md5sum« berechnet dabei eine Checksumme aus den angegebenen Dateien und gibt sie aus. Diese Checksummen sind relativ zuverlässig eindeutig. Mit ziemlich großer Wahrscheinlichkeit läßt sich ein "Trojanisches Pferd" nicht in einer Datei mit der gleichen MD5-Summe verstecken. Der obige Befehl erstellt somit folgende Liste:
0b91fc5ad9a1ba38d06eaeafdedd6103 main/sparc/beispiel1.xwd 7a5893c613f06c47d8d62832db108a1d main/sparc/beispiel2.xwd
Auf dem Rechner, der zwei solcher Dateien untersuchen soll, liegen die Dateien als »md5sums« und »md5sums.0« vor. Unterschiede werden der Einfachheit halber mit dem Programm »diff« herausgefunden und nicht mit Perl selbst. Zur Kontrolle soll am Ende der Mail die Ausgabe von »diff« angefügt werden, jedoch nur die geänderten Zeilen. Dazu wird das Programm aufgerufen und die Ausgabe von Perl gelesen. Das geschieht mit mit dieser Anweisung:
if (open (PIPE, "diff -u md5sums.0 md5sums |")) {
while (<PIPE>) {
Wenn das Programm nicht aufgerufen werden kann oder einen Fehler vermeldet, bricht das Perl-Programm die Verarbeitung an dieser Stelle ab. »PIPE« muß nach dem erfolgreichen Öffnen wieder geschlossen werden, nicht jedoch, wenn der »open«-Befehl fehlgeschlagen ist.
Da nur die Zeilen von Interesse sind, die Änderungen beschreiben, werden von der so erzeugten Ausgabe nur die Zeilen gelesen, die mit einem Plus- oder Minuszeichen beginnen. Die folgende Anweisung überspringt den Rest der Schleife, bis das erste Zeichen in der Zeile ein Plus- oder Minuszeichen ist, das zweite jedoch nicht.
next unless (/^[+-][^+-]/);
Die Bedeutung von »next unless ()« besagt, den Rest des Schleifen-Rumpfes, der folgenden Anweisungen also, zu überspringen, es sei denn, die angegebene Bedingung ist erfüllt. Alternativ könnte auch folgende, negativ formulierte, Anweisung verwendet werden:
next if (!/^[+-][^+-]/);
Der zweite Teil des regulären Ausdrucks (»[^+-]«) ist wichtig, da sonst die ersten administrativen Zeilen von »diff« verarbeitet würden. Um neue oder gelöschte Zeilen später an die Mail anhängen zu können, werden die so gelesenen Zeilen im Array »@lines« gesichert:
push (@lines, $_);
Für die weitere Verarbeitung ist der Zeilenumbruch (»\n«) nicht sinnvoll, da er nicht zum Dateinamen gehört, der weiterverarbeitet werden soll. Daher soll er abgeschnitten werden, was der folgende Befehl übernimmt:
chomp;
In diesem Fall könnte auch »chop« verwendet werden, da die Eingabe zeilenweise gelesen wird und die letzte Zeile immer einen Zeilenumbruch am Ende der Zeile beinhaltet. Im Gegensatz zu »chop« schneidet »chomp« nur dann das letzte Zeichen einer Zeichenkette ab, wenn es sich um ein Newline-Zeichen handelt bzw. genauer um eines der in »$/« (bzw. »$INPUT_RECORD_SEPARATOR«) angegebenen Zeichen.
Sowohl bei der »next unless«-Abfrage als auch bei dem Aufruf von »chomp« wurde nicht angegeben, worauf sich die Aktion beziehen soll. Perl unterstützt die Faulheit der Entwickler bis zu einem gewissen Grad. Wenn es nicht anders angegeben wurde, beziehen sich Aktionen implizit auf »$_«. Das ist in diesem Fall die zuletzt gelesene Zeile vom File-Handle »PIPE«.
Als nächstes muß die gelesene Zeile aufgeteilt werden in MD5-Summe, Pfad der Datei und Architektur. Dazu muß die Zeile zweimal aufgesplittet werden. Zuerst dienen zwei Leerzeichen als Trennzeichen (zwischen MD5-Summe und Pfad), anschließend sind es einzelne Schrägstriche, die Pfadkomponenten voneinander trennen. Das folgende Fragment teilt die Zeile auf. Der Pfad wird dazu in ein Array überführt, von dem die letzten beiden Elemente mit »pop« abgetrennt werden, die Architektur wird dabei in »$arch« gespeichert, der Dateiname in »$file«.
($md5, $path) = split (/ /); @path = split (/\//, $path); $file = pop @path; $arch = pop @path;
Der Rest von »@path« wird nicht mehr benötigt. Nachdem alle Informationen gesammelt sind, müssen sie jetzt in die gewünschte Datenstruktur aufgenommen werden. Die Ausgabe soll zwischen neuen und gelöschten Dateien sowie zwischen Architekturen unterscheiden. Daher werden zwei Hashes eingeführt: »%new« und »%removed«. Als Index wird die jeweilige Architektur verwendet. Um zusätzlich eine Sortierung zu ermöglichen, wird ein weiteres Hash mit Dateinamen als Indizes erstellt. Erst dann folgen die eigentlichen Daten: der Pfad der Datei sowie die Checksumme. Diese werden einfach in einem Array gespeichert. Daraus entsteht folgendes Code-Fragment:
Abbildung 2: Die Datenstruktur vom Security-Checker
if ($md5 =~ /^\+/) {
$new{$arch}{$file} = [ $path, substr ($md5, 1) ];
} elsif ($md5 =~ /^-/) {
$removed{$arch}{$file} = [ $path, substr ($md5, 1) ];
} else {
print STDERR "Unknown line: $_\n";
}
Zu Anfang wird entschieden, ob es sich um eine neue oder eine gelöschte Datei handelt, abhängig davon werden unterschiedliche Variablen verwendet. Anschließend wird die Datenstruktur gefüllt. Die gesamte Index-Struktur wird dabei von Perl erzeugt. Als Element wird schließlich eine Referenz auf ein anonymes Array zugewiesen, an dessen 0ter Stelle der Pfad und an erster Stelle die Checksumme steht. Mit der Funktion »substr()«, die Teile aus Zeichenketten ausschneidet, wird sichergestellt, daß von der MD5-Summe das nullte Zeichen nicht übernommen wird.
Auswertung der Daten
Die Daten sind jetzt komplett eingelesen und müssen nur noch für die Ausgabe aufbereitet werden. Da eine Mail verschickt werden soll, muß ein Datei-Handle mit einer Verbindung zu »/usr/sbin/sendmail« aufgebaut werden. Wird dieses Program mit dem Parameter »-t« aufgerufen, dann erwartet es als Standard-Eingabe eine komplette Mail samt aller Headerzeilen. Der Empfänger wird dann aus den »To:«-, »Cc:«- und »Bcc:«-Zeilen berechnet. Um die Standard-Eingabe für ein Programm liefern zu können, wird mit dem »open«-Befehl die Verbindung zum Programm geöffnet. Das dem Programmnamen vorangestellte Pipe-Symbol »|« bedeutet Perl, daß die Standard-Eingabe für das Programm vom Perl-Skript geliefert wird. Die nachfolgenden »print«-Anweisungen schreiben direkt auf das so erhaltene Datei-Handle.
Bevor die Verbindung zu Sendmail jedoch geöffnet wird, muß überprüft werden, ob überhaupt eine Mail geschrieben werden soll. Leere Mails verwirren eher als daß sie der Kontrolle dienen. Daher muß zuerst geprüft werden, ob die assoziativen Arrays »%new« oder »%removed« überhaupt Elemente enthalten. Ist es nicht der Fall, soll sich das Programm sang- und klanglos beenden. Die einfachste Möglichkeit, herauszufinden, ob ein Hash Elemente enthält oder nicht, besteht darin, sich auf Perl zu verlassen. Perl wandelt Datentypen intern automatisch passend um, so daß man »%new« auch in einer Bedingung verwenden darf, in der »wahr« oder »falsch« erwartet wird. Das folgende Fragment übernimmt die beschriebenen Aktionen:
if (%new || %removed) {
if (open (OUT, "| /usr/sbin/sendmail -t")) {
printf OUT "To: %s\n", $mailto;
printf OUT "Subject: %s\n", $subject;
print OUT "\n";
} else {
open (OUT, ">-");
}
}
Falls das Öffnen der Verbindung zu Sendmail nicht klappen sollte, soll das Programm nicht abgebrochen werden, sondern die gesamte Ausgabe soll auf dem normalen Ausgabestrom erfolgen. So kann es zum einen direkt ohne Sendmail aufgerufen werden; wenn es per »cron« automatisch aufgerufen wird, kann »cron« so andererseits den Inhalt der Mail verschicken. Dazu wird die spezielle Datei »-« für die Ausgabe (gekennzeichnet mit »>«) geöffnet. Als Folge davon, erscheint alles, was auf das Datei-Handle »OUT« geschrieben wird, als normale Programmausgabe.
Es fehlt jetzt nur noch die formatierte Ausgabe der Hashes »%new« und »%removed«, falls sie Elemente enthalten. Da beide Hashes die gleiche Datenstruktur enthalten, soll dieses in eine Funktion ausgelagert werden, der das Hash als Referenz übergeben wird. In der Funktion muß diese Referenz daher zu Beginn aufgelöst werden. Die Routine »report« formatiert die Daten. Dabei muß Perl erneut mit einem zusätzlichen Block unterstützt werden, um auf das Hash der Architekturen zuzugreifen. Die komplette Routine sieht wie folgt aus.
sub report
{
my $hash = shift;
my %hash = %$hash;
my $arch;
my $file;
for $arch (sort keys %hash) {
printf OUT " %s\n %s\n\n", $arch, "-" x length($arch);
for $file (sort keys %{$hash{$arch}}) {
printf OUT " File: %s\n MD5 : %s\n\n",
$hash{$arch}{$file}[0], $hash{$arch}{$file}[1];
}
}
}
Es ist in dieser Funktion noch ein weiteres Konstrukt vorhanden, das bisher nicht erläutert wurde. Die Anweisung
printf OUT " %s\n %s\n\n", $arch, "-" x length($arch);
erzeugt die Unter-Überschrift für die neuen bzw. gelöschten Dateien. Genaugenommen wird folgende Ausgabe erzeugt:
sparc -----
Der Trick befindet sich im Teilausdruck »"-" x length($arch)«. Dort kommt der Replikationsoperator »x« zum Einsatz. Er ist auf Zeichenketten anwendbar und hat zur Folge, daß die Zeichenkette auf der linken Seite des Operators so oft wiederholt wird wie auf der rechten Seite angegeben ist. »"1" x 4« erzeugt somit »"1111"«. Auf der rechten Seite darf auch eine Funktion stehen, die eine Zahl zurückliefert, wie z.B. »length($arch)«, die die Länge der in der Variablen »$arch« gespeicherten Zeichenkette zurückliefert. Dadurch wird der Name der Architektur von der korrekten Anzahl Bindestriche unterstrichen. Das vollständige Programm befindet sich auf der CD (»securitycheck«).
Extensible Markup Language
Die Erweiterbare Markup-Sprache (XML) beschreibt ein universelles Format für strukturierte Dokumente und Daten. Die wichtigste Spezifikation ist XML 1.0 vom World-Wide-Web-Consortium (W3C) von Februar '98. XML ist heutzutage in aller Munde und wird von mehreren aktuellen Programmen als Datenformat verwendet.
XML selbst ist keine eigene Markup-Sprache, sondern eine Metasprache, mit der ihrerseits andere Sprachen beschrieben werden, so daß Sie Ihr eigenes Markup definieren können. Eine vordefinierte Markup-Sprache wie HTML beschreibt eine Möglichkeit, Informationen in einer speziellen Art von Dokumenten darzustellen. XML erlaubt es, eigene angepaßte Markup-Sprachen für grenzenlose und verschiedene Arten zu erstellen. Dieses geschieht üblicherweise in eigenen DTD's, auf die dieser Artikel jedoch nicht eingeht.
Auf die Struktur von XML soll an dieser Stelle nur sehr oberflächlich eingegangen werden. Am Ende dieses Artikels befinden sich mehrere Web-Adressen, die weitere Informationen zu XML bieten. Die Verwendung von XML wird für Entwickler insofern erleichtert als daß es für viele Programmiersprachen bereits Parser in Form von Bibliotheken gibt, so daß dieses Rad nicht immer wieder neu erfunden werden muß.
XML-Dateien erinnern stark an HTML-Dateien. In der Tat ist HTML ebenfalls eine Markup-Sprache mit ähnlicher Syntax. Wer ein Beispiel von XML sehen möchte, legt mit Gnumeric oder Abiword ein einfaches Dokument an. Wenn es binäre Daten sind, handelt es sich wahrscheinlich um komprimierte XML-Dateien, die mit »zcat datei|more« angezeigt werden. Dieser Artikel beschränkt sich im folgenden aus Gründen der Einfachheit jedoch auf einfache HTML-artige Beispiele. Um die Komplexität der Datenstrukturen zu demonstrieren, die von den XML-Parsern angelegt werden, können die Daten der Beispiele durch eine von Gnumeric oder Abiword erzeugte Dateien ersetzt werden.
XML-Beispiel
Als erstes Beispiel soll das folgende Konstrukt dienen. Es sieht recht simpel aus, wird jedoch umgesetzt in Perl-Datenstrukturen mittelmäßig komplex.
<title><head>Hello</head></title>
Generell ist es für die Arbeit mit XML-Daten ratsam, den Data-Dumper griffbereit zu haben. Wenn die Datenstrukturen offensichtlich nicht so aufgebaut sind, wie man anhand der Quellen gedacht hat, besteht so eine einfache Möglichkeit, zu überprüfen, wie der Parser sie stattdessen aufgebaut hat. Im folgenden soll neben dem bereits bekannten Data-Dumper ein weiteres Module beispielhaft verwendet werden: XML::Parser.
Auf einem Debian-System wird es wie folgt installiert.
apt-get install libxml-parser-perl
Das Modul ist natürlich nicht auf Debian-Systeme beschränkt. Auf anderen Systemen ist es teilweise lediglich nicht als vorkompiliertes Paket vorhanden. In dem Fall muß das Modul direkt vom Quellcode übersetzt werden, was bei CPAN-Modulen extrem einfach ist und im kurz skizziert werden soll. Als erstes wird das Archiv vom CPAN-Server geholt, die URL dazu befindet sich am Ende dieses Artikels.
Die Dateien sind Unix-übliche komprimierte Tar-Archive, die mit »tar xfz« ausgepackt werden. Anschließend gibt es im aktuellen Verzeichnis ein Unterverzeichnis für das Modul. In diesem müssen als nächstes die Compilier-Anweisungen erzeugt werden, und zwar automatisch mit dem Befehl »perl Makefile.PL«. Der nächste Schritt übersetzt das Paket mit »make«. Erst jetzt muß als »root« gearbeitet werden, um das Paket zu installieren. Das geschieht mit dem einfachen Befehl »make install«.
Parsen von XML-Daten
Die einfachste Art, XML-Daten in einem Perl-Programm zu parsen, bietet das Modul XML::Parser mit dem Stil »Tree«. Das Modul unterstützt verschiedene Quellen. Die XML-Daten müssen nicht in einer eigenen Datei vorliegen. Wenn Daten im XML-Format ausgetauscht werden, z.B. über eine Netzwerkverbindung, dann wäre es recht unpraktisch, wenn nur zum Parsen eine Datei angelegt werden müßte.
Dieses Beispiel parst den angegebenen Text und gibt eine Referenz auf die erzeugte Datenstruktur zurück. Wichtig ist das Erzeugen des Parsers über den »new«-Befehl. Dabei muß der Stil des Parsers eingestellt werden. Der Stil »Tree« erzeugt eine Baumstruktur. Eine andere Baumstruktur, die gleich Objekte in Perl anlegt, wenn sie ausgewertet wird, legt der Parser mit dem Stil »Objects« an. Wer verstehen möchte, wie der Parser arbeitet, der trägt jedoch stattdessen »Debug« als Stil ein.
use XML::Parser;
use Data::Dumper;
$p=new XML::Parser(Style => 'Tree');
$tree=$p->parsestring("<title><head>Hello</head></title>");
print Dumper($tree);
Um die Ausgabe besser zu verstehen, soll eine andere Zeichenkette gelesen werden, die zusätzliche Attribute enthält:
<title mag="freeX" edition="2/01"> <head>Hello</head><body>Text</body></title>
Beim Aufruf der Funktion »parsestring« muß jedem Anführungszeichen ein Backslash vorangestellt werden und die Zeichenkette sollte keinen Zeilenumbruch enthalten. Die Ausgabe vom Dumper zeigt jetzt mehr Struktur und vermittelt einen besseren Einblick darüber, wie der Parser die Datenstruktur aufbaut. Um zu zeigen, wie auf die Inhalte der Datenstruktur im eigenen Programm zugegriffen wird, soll der folgende Ausschnitt dienen:
print "Attributes:\n";
foreach $i (keys %{${${$tree}[1]}[0]}) {
printf " %s: %s\n", $i, ${${${$tree}[1]}[0]}{$i};
}
printf "Subelements: %d\n", $#{${$tree}[1]}/2;
for ($i=1; $i < $#{${$tree}[1]}; $i+=2) {
printf " %s\n", ${${$tree}[1]}[$i];
}
Zuerst werden die Attribute von »title« ausgegeben, anschließend alle Komponenten, also »head« und »body«. Auf die zusätzlichen Klammern kann übrigens nicht verzichtet werden. Der in den Blöcken enthaltene Ausdruck wird ausgewertet, so daß Perl das Ergebnis wieder als Array oder Hash auffassen kann. Aus diesem extrem kleinen Beispiel wird bereits deutlich, wie komplex die Datenstrukturen werden können und mit welchem Aufwand man bei der Weiterverarbeitung von XML-Daten rechnen muß.
Anstelle einer Zeichenkette läßt sich genauso gut auch eine Datei einlesen und parsen. Dafür ist es nicht erforderlich, daß der Entwickler sich um die Datei kümmert, sondern kann dieses getrost dem Modul überlassen. In diesem Fall wird die Funktion »parsefile« anstelle von »parsestring« zusammen mit dem Dateinamen verwendet:
$tree = $p->parsefile("simple.xml");
Diese Routine kann nicht verwendet werden, wenn ein externes Programm die XML-Datei erst erstellen muß, oder wenn die XML-Datei komprimiert ist (wie z.B. im Fall von Gnumeric). Das ist jedoch auch kein Problem, in dem Fall wird erneut eine andere Funktion verwendet, die auf Datei-Handles beliebiger Art arbeitet. Somit ist es möglich, die Datei mit dem Programm »zcat« auszupacken und das zugehörige Datei-Handle an die XML-Funktion zu übergeben. Die Funktion »parse« liest die Daten selbständig vom Handle ein, welches anschließend vom Programm geschlossen wird.
open (IN, "zcat debian.gnumeric |"); $tree=$p->parse(*IN); close (IN);
Ausblick
Für die Verarbeitung von XML-Daten stehen dem Perl-Entwickler eine große Anzahl weiterer Module zur Verfügung. Im CPAN-Archiv befinden sich ca. 70 unterschiedliche Module, die sich mit XML beschäftigen. Teilweise sind es Parser, teilweise Generatoren, teilweise Module zur Weiterverarbeitung u.s.w. Wer sich intensiver mit XML beschäftigen möchte, der sollte auf jeden Fall das Verzeichnis der CPAN-Module (s.u.) überprüfen.
Ressourcen
- Datenstrukturen mit Perl, Martin Schulze, freeX 3/01, S. 74ff.
- Extensible Markup Language
- XML in 10 Schritten
- Verzeichnis aller CPAN-Module
- Quellcode zu XML::Parser
- Document Object Model, Level 1