XML::SAX: Die zweite Generation

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

Die Entwicklung der SAX-Parser warf ein Problem auf: Wie sollte man sie alle synchron mit der Standardschnittstelle halten? XML::SAX, ein hervorragendes Gemeinschaftswerk von Matt Sergeant, Kip Hampton und Robin Berjon, löst dieses Problem und unterstützt nebenbei auch noch SAX Level 2, was die anderen Module nicht taten.

"Was meinen Sie mit 'synchron mit der Standardschnittstelle halten'?" werden Sie fragen. Die ganze Zeit haben wir die Wunder einer Standardschnittstelle wie SAX gepredigt, die sicherstellen sollte, daß SAX-basierte Module wirklich austauschbar sind. Aber leider gibt es da ein kleines Problem: In Perl gibt es mehr als eine Möglichkeit, SAX zu implementieren. SAX wurde ursprünglich für Java entwickelt, eine wundervoll typsichere Sprache, in der man jedes einzelne Argument eines Handlers präzise festlegen kann. Derlei gibt es in Perl nicht.

Das war kein großes Problem für die älteren SAX-Module, über die wir bisher gesprochen haben. Sie unterstützen alle das relativ einfache SAX 1. Inzwischen bahnt sich aber eine neue Gruppe von Modulen den Weg, die allesamt SAX2 unterstützen. SAX2 ist erheblich komplizierter, weil hier die Unterstützung von Namensräume eingeführt wurde. Der Eventhandler eines Elements sollte jetzt sowohl das Präfix des Namensraums erhalten als auch den lokalen Namen. Wie soll diese zusätzliche Information in Parametern übergeben werden? In einem einzigen String, foo:bar? Oder in zwei Parametern?

Darüber entstand eine hitzige Debatte auf der Mailingliste perl-xml , bis sich einige Mitglieder entschlossen, die Spezifikation einer SAX-»Perle« zu entwickeln. (Wir werden gleich sehen, wie diese neue API für SAX2 eingesetzt wird.) Um andere zu ermuntern, sich an diesen neuen Standard zu halten, enthält XML::SAX eine Klasse namens XML::SAX::ParserFactory. Eine Factory ist ein Objekt, das nur einen einzigen Zweck hat: Objekte einer bestimmten Klasse zu generieren – in diesem Falle Parser. XML::SAX::ParserFactory ist nützlich, um Parsern ihre Hausarbeit zu erleichtern, zum Beispiel die Änderung von Optionen und andere Initialisierungsaufgaben. Sag der Factory, was für einen Parser Du möchtest, und sie erzeugt Dir eine Kopie.

XML::SAX hat das Zusammenwirken von XML und Perl deutlich verändert und geprägt. Das Modul baut auf den älteren auf, indem es die besten Eigenschaften übernimmt, vermeidet aber einige der Fehler. Um die Kompatibilität der Module sicherzustellen, gibt es zum Beispiel eine Basisklasse für Parser, die viele der eher uninteressanten Aufgaben eines Parsers abstrahiert. Der Entwickler kann sich dadurch auf die wirklich wichtigen Aufgaben konzentrieren. Auch für die Benutzer bietet diese abstrakte Schnittstelle einiges, insbesondere die Katalogisierung einer Unmenge von Modulen und ihrer Eigenschaften sowie die Möglichkeit, mit einer einfachen Query (Abfrage) zu gegebenen Eigenschaften ein Modul zu finden, das diese erfüllt. Das ist ein kühner Schritt und erfordert eine Menge neues Wissen. Seien Sie also auf viele neue Informationen und Details in diesem Abschnitt vorbereitet. Sie werden sehen: Es lohnt sich!

XML::SAX::ParserFactory

Wir beginnen mit dem Modul XML::SAX::ParserFactory, einer Schnittstelle zur Auswahl von Parsern. Wenn Sie bereits einmal mit DBI gearbeitet haben: Diese Klasse hat viel Ähnlichkeit damit. Sie ist ein Frontend für alle auf Ihrem Rechner installierten SAX-Parser. Sie verlangen einen Parser von der Factory, und diese erzeugt einen geeigneten. Nehmen wir einmal an, wir hätten ein Package XML::SAX::MyHandler erzeugt und wollten einen SAX-Parser, der die Events liefert.

Das folgende Beispiel zeigt, wie man den Parser bekommt und ihn zum Lesen einer Datei verwendet:

use XML::SAX::ParserFactory;
use XML::SAX::MyHandler;
my $handler = new XML::SAX::MyHandler;
my $parser = XML::SAX::ParserFactory->parser( Handler => $handler );
$parser->parse_uri( "foo.xml" );

Welchen Parser man genau bekommt, hängt unter anderem von der Reihenfolge ab, in der die Module installiert wurden. Der zuletzt installierte Parser (mit all den eventuell durch RequiredFeatures spezifizierten Fähigkeiten) wird standardmäßig zurückgegeben. Aber möglicherweise wollen Sie genau diesen nicht. Auch das ist kein Problem; XML::SAX betreut einen Katalog von SAX-Parsern, aus dem Sie wählen können. Jedesmal wenn Sie einen neuen SAX-Parser installieren, trägt er sich selbst in den Katalog der ParserFactory ein. Wenn Sie eine Parserklasse XML::SAX::BobsParser installiert haben, können Sie eine Instanz dieser Klasse bekommen, indem Sie die Variable $XML::SAX::ParserPackage wie folgt verwenden:

use XML::SAX::ParserFactory;
use XML::SAX::MyHandler;
my $handler = new XML::SAX::MyHandler;
$XML::SAX::ParserPackage = "XML::SAX::BobsParser( 1.24 )";
my $parser = XML::SAX::ParserFactory->parser( Handler => $handler );

Da die Variable $XML::SAX:ParserPackage den Wert XML::SAX::BobsParser(1.24) enthält, wird eine Instanz dieser Klasse erzeugt. Intern lädt die ParserFactory die Klasse mit Hilfe von require( ) und ruft dann deren new( )-Methode auf. Die Nummer 1.24 gibt die minimale Version an, die die geladene Klasse haben soll. Wenn die Klasse nicht auf Ihrem Rechner installiert ist oder nur in einer älteren Version, dann wird eine Ausnahme ausgelöst.

Um eine Liste aller bei XML::SAX registrierten Parserklassen zu bekommen, verwendet man die Methode parsers( ):

use XML::SAX;

my @parsers = @{XML::SAX->parsers( )};

foreach my $p ( @parsers ) {
   print "\n", $p->{ Name }, "\n";
   foreach my $f ( sort keys %{$p->{ Features }} ) {
      print "$f => ", $p->{ Features }->{ $f }, "\n";
   }
}

Die Methode liefert eine Referenz auf eine Liste von Hashes. Jeder Hash enthält Informationen über eine Parserklasse, unter anderem den Namen der Klasse und einen weiteren Hash mit ihren Fähigkeiten. Wenn wir das obige Programm ausführen, erfahren wir zum Beispiel, daß zwei Parserklassen bei XML::SAX registriert sind, die beide Namensräume unterstützen:

XML::LibXML::SAX::Parser
http://xml.org/sax/features/namespaces => 1

XML::SAX::PurePerl
http://xml.org/sax/features/namespaces => 1

Als diese Seiten geschrieben wurde, waren die beiden genannten Parser die einzigen, die in der XML::SAX-Distribution enthalten waren. XML::LibXML::SAX::Parser ist eine SAX-Schnittstelle, die auf der libxml2-Bibliothek aufbaut und die wir in Baumorientierte Verarbeitung noch näher kennenlernen werden. Um sie zu benutzen, benötigt man libxml2, eine kompilierte und dynamisch ladbare Bibliothek, die in C geschrieben ist. Sie ist sehr schnell, aber wenn man keine passende Binärdatei findet und keinen Compiler hat, dann wird man die mangelnde Portabilität dieses Moduls beklagen. XML::SAX::PurePerl ist dagegen komplett in Perl geschrieben, wie der Name schon sagt. Damit ist dieses Modul voll portabel, sofern ein Perl-Interpreter vorhanden ist. Damit stehen schon einmal die wichtigsten Möglichkeiten zur Verfügung.

Die explizit genannten Fähigkeiten eines Parsers sind sehr wichtig, weil sie einem Benutzer garantieren, daß ein Parser die verlangten Eigenschaften hat. Nehmen wir zum Beispiel an, Sie wollen einen validierenden Parser, der Namensräume unterstützt. Die Methode require_feature( ) der Factory sorgt dafür, daß Sie ihn bekommen:

my $factory = new XML::SAX::ParserFactory;
$factory->require_feature( 'http://xml.org/sax/features/validation' );
$factory->require_feature( 'http://xml.org/sax/features/namespaces' );
my $parser = $factory->parser( Handler => $handler );

Alternativ können Sie dieselbe Information auch im Konstruktor der Factory übergeben:

my $factory = new XML::SAX::ParserFactory(
           Required_features => {
                  'http://xml.org/sax/features/validation' => 1
                  'http://xml.org/sax/features/namespaces' => 1
           }
);
my $parser = $factory->parser( Handler => $handler );

Falls mehrere passende Parserklassen gefunden werden, wird die zuletzt installierte verwendet. Wenn die Factory allerdings keinen Parser mit den genannten Eigenschaften findet, dann löst sie eine Ausnahme aus.

Um weitere SAX-Module in den Katalog aufzunehmen, müssen Sie sie einfach per Download auf Ihr System übertragen und installieren. Die Installationsprozedur der Module muß natürlich XML::SAX kennen und die Module selbst im Katalog eintragen. Die Eintragung des Moduls geschieht durch den Aufruf der XML::SAX-Methode add_parser( ) mit einer Liste von Modulnamen als Argumente. Um sicherzustellen, daß die genannten Klassen die Konventionen eines SAX-Moduls erfüllen, implementiert man sie am besten als Subklassen von XML::SAX::Base. Wir werden Ihnen später noch ausführlicher zeigen, wie man einen Parser schreibt, installiert und in den Katalog einträgt.

Handlerschnittstelle von SAX2

Nachdem Sie einen Parser ausgewählt haben, müssen Sie als nächstes – wie gehabt – eine Klasse mit Handlermethoden schreiben. Diese Methoden haben die Aufgabe, die Eventströme eines SAX-Moduls zu verarbeiten, genau wie bei den SAX-Modulen, die wir bereits kennengelernt haben. XML::SAX spezifiziert die Events und deren Properties sehr genau. Dem Handler gewährt die Spezifikation beträchtliche Kontrolle bei gleichzeitiger strikter Einhaltung der geforderten Richtlinien.

Die unterstützten Eventhandler sind in verschiedene Gruppen aufgeteilt. Die uns am besten bekannten sind die Content-Handler , die zum Beispiel für Elemente und allgemeine Informationen über das Dokument verantwortlich sind. Kennengelernt haben wir auch schon die Entity-Resolver und die lexikalischen Handler, die CDATA-Abschnitte und Kommentare betreuen. DTD-Handler und Declaration-Handler kümmern sich um außerhalb des eigentlichen Dokuments stehenden Markup, vor allem um Deklarationen von Elementen und Entities. XML::SAX kennt darüber hinaus eine neue Gruppe namens Error-Handler . Diese werden im Falle von Fehlern aufgerufen.

Die wichtigste neue Eigenschaft dieser Art von Parsern ist die Unterstützung von Namensräumen, der wesentlichen Innovation von SAX2. Bis dato behandelten SAX-Parser einen qualifizierten Namen als einen einzelnen String: Das Präfix des Namensraums und der lokale Name waren stets miteinander verbunden. Nun bekommt man die Namensräume detailliert geliefert, wird über Beginn und Ende ihrer Gültigkeit informiert. Die Arbeit mit Namensräumen wird dadurch wesentlich erleichtert.

Content-Handler

Diese Gruppe von Handlern konzentriert sich auf den eigentlichen Inhalt eines Dokuments. Sie werden von den meisten Programmen implementiert, die SAX benutzen. Beachten Sie als nützliche Neuigkeit die Referenz auf einen Lokator, die dem Handler Einblick in wichtige Details des Parsers erlaubt. Die einzelnen Handlermethoden unterstützen außerdem neuerdings Properties für Namensräume.

set_document_locator( Lokator )
Wird zu Beginn vom Parser aufgerufen, um dem Handler Informationen über die Datenquelle zu verschaffen. Der Parameter Lokator ist eine Referenz auf einen Hash mit den folgenden Properties:

PublicID
Die Public-ID des aktuell vom Parser gelesenen Entity.

SystemID
Die System-ID des aktuell vom Parser gelesenen Entity.

LineNumber
Die Zeilennummer innerhalb des aktuell gelesenen Entity.

ColumnNumber
Die Spaltennummer innerhalb des aktuell gelesenen Entity.

Dieser Hash wird laufend aktualisiert und mit den neuesten Informationen versehen. Wenn Ihr Handler die gelesenen Daten nicht akzeptiert, kann der Lokator verwendet werden, um eine Fehlermeldung zu erzeugen, die unter anderem angibt, wo die fehlerhaften Daten stehen. Ein SAX-Parser sollte einen Lokator übergeben, muß es aber nicht. Aus diesem Grund sollte die Verwendung eines Lokators stets mit der Prüfung einhergehen, ob überhaupt einer vorhanden ist. Ferner sollten Sie den Lokator ausschließlich innerhalb eines Eventhandlers verwenden, das Ergebnis ist andernfalls nicht vorhersagbar.

start_document( Dokument )
Diese Handlermethode wird unmittelbar im Anschluß an set_document_locator( ) aufgerufen, bevor der Parser tatsächlich mit der Arbeit beginnt. Der Parameter Dokument ist eine Referenz auf einen leeren Hash, da dieses Event zur Zeit keine Properties hat.

end_document( Dokument )
Diese Handlermethode wird als allerletztes aufgerufen. Der Parser signalisiert damit ein Ende der zu lesenden Daten oder einen Fehler, der zum Abbruch des Parsens führt. Der Rückgabewert dieser Methode wird vom Parser seinerseits als Ergebnis der Methode parse( ) zurückgegeben. Der Parameter Dokument ist wiederum leer.

start_element( Element )
Wenn der Parser das Start-Tag eines Elements findet, ruft er diese Methode auf. Der Parameter Element ist ein Hash mit den Properties des Elements, unter anderem den folgenden:

Name
Der Name des Elements, einschließlich eines eventuellen Namensraum-Präfixes.

Attributes
Eine Referenz auf einen Hash mit Attributen. Jedes Attribut erscheint unter dem Schlüssel { NamespaceURI } LocalName. Der Wert ist wiederum eine Referenz auf einen Hash mit den Properties des Attributes.

NamespaceURI
Der Namensraum des Elements.

Prefix
Das Präfix des Namensraums.

LocalName
Der lokale Teil des qualifizierten Namens.

Properties von Attributen sind unter anderem:

Name
Der qualifizierte Name des Attributs (Präfix + lokaler Name).

Value
Der normalisierte Attributwert (führende und abschließende Leerzeichen werden entfernt).

NamespaceURI
Der Namensraum des Attributs.

Prefix
Das Präfix des Namensraums.

LocalName
Der lokale Teil des qualifizierten Namens.

Die Properties NamespaceURI, LocalName und Prefix fehlen jeweils, wenn der Parser Namensräume nicht unterstützt.

end_element( Element )
Nachdem der Inhalt eines Elements verarbeitet und das End-Tag gefunden wurde, ruft der Parser diese Methode auf. Diese Methode wird auch bei leeren Tags aufgerufen. Der Parameter Element ist ein Hash mit den folgenden Properties:

Name
Der Name des Elements, einschließlich eines eventuellen Namensraum-Präfix.

NamespaceURI
Der Namensraum des Elements.

Prefix
Das Präfix des Namensraums.

LocalName
Der lokale Teil des qualifizierten Namens.

Die Properties NamespaceURI , LocalName und Prefix fehlen, wenn der Parser keine Namensräume unterstützt.

characters( Text )
Der Parser ruft diese Methode auf, wenn er ein Stück Text (Zeichendaten) findet. Der Parser hat die Erlaubnis, einen solchen Text in mehrere Teile aufzubrechen und jedes Teil in einem separaten Aufruf der Handlermethode zu melden. Die Reihenfolge der übergebenen Teile muß aber mit der Reihenfolge im Text übereinstimmen. Die innerhalb eines Aufrufs übergebenen Daten müssen komplett aus derselben Datenquelle stammen. Der Parameter Text ist eine Referenz auf einen Hash mit einer einzigen Property namens Data, einem String mit dem übergebenen Text.

ignorable_whitespace( Text )
Unter ignorierbaren Leerzeichen versteht man Leerzeichen, die an Stellen stehen, wo das Inhaltsmodell eines Elements keine Textdaten vorsieht. Mit anderen Worten: Die Zeilenenden und einrückenden Leerzeichen, die gerne verwendet werden, um die Lesbarkeit eines Dokuments zu verbessern, können ignoriert werden, weil sie nicht tatsächlich als Teil des Dokuments angesehen werden. Ein Parser kann allerdings nur dann erkennen, daß Leerzeichen ignorierbar sind, wenn er eine entsprechende DTD gelesen hat. Ferner muß es sich um einen validierenden Parser handeln. (Wenn Ihnen diese Einschränkungen unklar sind, können Sie diese Details guten Gewissens vergessen, sie sind in den meisten Fällen bedeutungslos.) Der Parameter Text ist ein Parameter mit der Property Data, einem String mit den übergebenen Leerzeichen.

start_prefix_mapping( Mapping )
Diese Methode wird aufgerufen, wenn der Gültigkeitsbereich eines Namensraums beginnt. Wenn ein Parser Namensräume nicht unterstützt, wird dieses Event nicht ausgelöst, aber Element- und Attributnamen enthalten nach wie vor die Namensraumpräfixe. Dieses Event erfolgt stets vor dem Start-Tag-Event des Elements, das den Gültigkeitsbereich einleitet. Der Parameter Mapping ist eine Hashreferenz mit den folgenden Properties:

Prefix
Das Präfix des Namensraums.

NamespaceURI
Der URI des Namensraums, auf den das Präfix abgebildet wird.

end_prefix_mapping( Mapping )
Diese Methode wird aufgerufen, wenn der Gültigkeitsbereich eines Namensraums endet. Der Parameter Mapping ist eine Referenz auf einen Hash mit der folgenden Property:

Prefix
Das Präfix des Namensraums.

Das Event wird im Anschluß an das End-Tag-Event des Elements ausgelöst, mit dem der Gültigkeitsbereich endet.

processing_instruction( PI )
Diese Methode behandelt Events, mit denen der Parser eine PI meldet. Der Parameter PI ist eine Hashreferenz mit den folgenden Properties:

Target
Das Target der PI.

Data
Die Daten der PI (eventuell undef, falls Daten fehlen).

skipped_entity( Entity )
Nicht-validierende Parser können Entities ignorieren, statt sie aufzulösen. Wenn ein Parser zum Beispiel keine Deklaration des Entity kennt, kann er entweder eine Fehlermeldung auslösen oder eben die Verwendung des Entity ignorieren. Durch die Auslösung des Events erhält der Handler die Möglichkeit, seinerseits etwas mit dem Entity anzufangen, möglicherweise auch seinen eigenen Mechanismus zur Auflösung des Entity zu implementieren.

Wenn ein Parser Entities überspringt, ist eines der folgenden Features gesetzt:

  • Behandlung externer Parameterentities (die Feature-ID ist "http://xml.org/sax/features/external-parameter-entities").
  • Behandlung externer allgemeiner Entities (die Feature-ID ist "http://xml.org/sax/features/external-general-entities").

(In XML werden Features als URIs repräsentiert, die entweder vorhanden sind oder eben nicht. In Strategien des Programmierers werden Sie dazu genauere Erklärungen finden.)

Der Parameter Entity ist eine Hashreferenz mit der folgenden Property:

Name
Der Name des zu überspringenden Entity. Falls es ein Parameterentity ist, beginnt der Name mit einem Prozentzeichen (%).

Entity-Resolver

Standardmäßig lösen XML-Parser Referenzen auf externe Entities selbst auf, ohne daß Ihr Programm auch nur erfährt, daß es sie jemals gegeben hat. Gelegentlich mag es aber durchaus erwünscht sein, dieses Verhalten zu ändern. Zum Beispiel liegen die Entities vielleicht in einer Datenbank. Was auch immer Ihr Grund für die Implementierung dieses Handlers sein mag: Falls er vorhanden ist, wird ihn der Parser aufrufen, bevor er seinerseits versucht, das Entity aufzulösen.

Das Argument der Methode resolve_entity( ) ist eine Hashreferenz mit zwei Properties: PublicID, dem öffentlichen Bezeichner der Datei, und SystemID, dem systemspezifischen Bezeichner. Dabei kann es sich um einen Pfadnamen im Dateisystem oder um einen URI handeln. Falls der öffentliche Bezeichner undef ist, dann wurde keiner deklariert. Der Systembezeichner muß aber gesetzt sein.

Lexikalische Eventhandler

Es steht Ihnen drei, die Handlermethoden dieser Gruppe zu implentieren. In den meisten Fällen werden Sie kein Interesse an diesen Events haben. Aus diesem Grund ist es auch keineswegs selbstverständlich, daß ein Parser diese Events generieren kann. Einige relativ vollständige Parser werden dazu allerdings in der Lage sein. Wenn Ihnen daran gelegen ist, das ursprüngliche XML-Dokument möglichst exakt zu replizieren, einschließlich Kommentaren und CDATA-Abschnitten, dann benötigen Sie einen solchen Parser.

Zu den Methoden dieser Gruppe gehören:

  • start_dtd( ) und end_dtd( ), die Anfang und Ende der Dokumenttypdeklaration anzeigen,
  • start_entity( ) und end_entity( ), die Anfang und Ende einer aufgelösten Entityreferenz anzeigen,
  • start_cdata( ) und end_cdata( ), die Anfang und Ende eines CDATA-Abschnitts anzeigen,
  • comment( ) signalisiert einen lexikalischen Kommentar. Kommentare werden ansonsten vom Parser ignoriert.
Error-Eventhandler und Ausnahmebehandlung

XML::SAX erlaubt es, die Behandlung von Fehlern mit Hilfe dieser Gruppe von Handlern an die eigenen Bedürfnisse anzupassen. Jeder Handler erhält ein einziges Argument, das als Exception (Ausnahme) bezeichnet wird und eine Beschreibung des Fehlers enthält. Die Auswahl des jeweiligen Handlers erfolgt abhängig von der Schwere des Problems, so wie vom W3C empfohlen. Es gibt die folgenden drei Schweregrade:

warning( )
Dies ist der Handler der unwesentlichsten Exceptions, der Warnungen. Der aufgetretene Fehler erlaubt es dem Parser, seine Arbeit fortzusetzen. Kann eine ID-Referenz innerhalb des Dokuments nicht aufgelöst werden, dann kann der Parser eine Warnung ausgeben, ansonsten aber das Dokument weiterlesen, um eventuell weitere Fehler zu finden. Falls diese Handlermethode nicht implementiert ist, wird der Parser die Warnung ignorieren.

error( )
Fehler dieser Art gelten als schwerwiegend, der Parser sieht aber eine Möglichkeit weiterzumachen. In diese Kategorie fallen zum Beispiel Validierungsfehler. Der Parser kann weiterhin Events generieren. Ihre Anwendung kann aber an dieser Stelle auch entscheiden, das Programm abzubrechen. Fehlt ein entsprechender Handler, wird der Parser ebenfalls weitermachen.

fatal_error( )
Ein fataler Fehler bedeutet, daß der Parser sofort aufhören wird, das Dokument zu lesen. Der Parser ist auf keinen Fall in der Lage, die Analyse wiederaufzunehmen. Dagegen ist er bereit, weitere Fehlermeldungen zu sammeln, die die Anwendung eventuell generiert. Mögliche Ursachen sind vor allem Syntaxfehler, die die Wohlgeformtheit des XML-Dokuments verletzen. Eine andere mögliche Ursache könnte ein Entity sein, das nicht aufgelöst werden kann. In jedem Fall ist dies die schwerwiegendste Art der Fehlermeldung innerhalb XML::SAX.

Die XML-Spezifikation verlangt, daß ein Parser Verletzungen der Wohlgeformtheit oder der Gültigkeit eines Dokuments mit einem Abbruch quittiert. In Perl SAX bedeutet das einen Aufruf von die( ). Das hat aber nicht unbedingt ein Ende des Programms zur Folge. Selbst wenn ein Parser die( ) aufgerufen hat, kann man diese Ausnahme immer noch mit Hilfe von eval{} abfangen. Das sieht in etwa so aus:

eval{ $parser->parse( $uri ) };
if( $@ ) {
# Oh Schreck! Ein Fehler trat auf...
}

Die Variable $@ ist in diesem Fall ein Objekt mit einer Reihe von Properties, die Details zum Fehler enthalten.

Dazu gehören unter anderem:

Message 
Eine textuelle Beschreibung des Fehlers

ColumnNumber 
Die Spaltennummer (Zeichenzahl) innerhalb der Zeile, in der der Fehler erkannt wurde

LineNumber 
Die Zeilennummer, in der der Fehler erkannt wurde

PublicID 
Ein öffentlicher Bezeichner des Entity, in dem der Fehler erkannt wurde

SystemID 
Ein Systembezeichner des Entity, in dem der Fehler erkannt wurde

Wird eine solche Exception geworfen, bedeutet das nicht notwendigerweise, daß das gelesene XML-Dokument einen Fehler enthält. Zum Beispiel wirft der Parser auch eine Exception, wenn versucht wird, ein Feature zu setzen, das er nicht unterstützt. In diesem Fall ist vermutlich nur die Property Message gesetzt.

Die Parserschnittstelle von SAX2

Nachdem Sie eine Handlerklasse geschrieben haben, eine Instanz des Parsers erzeugt und mit den richtigen Features konfiguriert haben, sollte der Parser als nächstes seine Eingabedaten lesen. In diesem Abschnitt beschreiben wir, wie man das alles mit XML::SAX - Parsern bewerkstelligt.

Die Methode parse( ), die den eigentlichen Prozeß anstößt, erhält als Argument einen Hash mit Optionen. Hier werden Handler und Features gesetzt und die zu lesenden Daten definiert. Zum Beispiel wird in den folgenden Zeilen ein Handler gesetzt und das zu lesende Dokument angegeben:

$parser->parse( Handler => $handler,
             Source => { SystemId => "data.xml" });

Die Property Handler gibt an, daß das übergebene Objekt eine generische Menge von Handlermethoden implementiert hat. Allerdings hat jede Handlergruppe ihre eigene Property, zum Beispiel: ContentHandler, DTDHandler, EntityResolver und ErrorHandler. Die Property Handler ist die Zusammenfassung aller Gruppen, d. h., das Objekt implementiert alle Handlermethoden. Wird kein Handler für eine Gruppe angegeben, dann wird der Parser die betreffenden Events einfach ignorieren bzw. Fehler auf seine eigene Weise interpretieren.

Der Parameter Source ist ein Hash mit allen Informationen über die zu lesenden Daten. Die folgenden Properties sind in diesem Hash definiert:

CharacterStream 
Diese Art von Dateihandle funktioniert ab Perl 5.7.2 mit Hilfe von PerlIO. Der Parser wird in diesem Fall keinerlei Transformationen von einem Zeichensatz in einen anderen vornehmen. Mit der Funktion read( ) wird er statt dessen stets eine gewisse Anzahl von Zeichen lesen. Wenn die Property CharacterStream gesetzt ist, werden ByteStream oder SystemId ignoriert.

ByteStream 
Diese Property gibt an, daß ein Bytestrom zu lesen ist. Die Property wird ignoriert, wenn CharacterStream gesetzt ist. Umgekehrt wird SystemId ignoriert, wenn ByteStream gesetzt ist. Wenn möglich, sollte man zusätzlich die Property Encoding definieren.

PublicId
Diese Property ist optional und kann ignoriert werden, aber wenn die Anwendung einen öffentlichen Bezeichner übergeben will, dann wird er hier gespeichert.

SystemId 
Dieser String enthält eine systemspezifische Bezeichnung der Datenquelle. Dabei kann es sich zum Beispiel um einen URI oder um einen Pfad im Dateisystem handeln. Falls die XML-Daten über die Properties CharacterStream oder ByteStream gelesen werden, kann das Setzen der SystemId trotzdem erforderlich sein, falls relative Entityreferenzen aufzulösen sind.

Encoding 
Falls das Codierungssystem der zu lesenden Daten bekannt ist, sollte es hier gespeichert werden.

Eine weitere mögliche Option ist Features . Damit setzt man die von SAX2 definierten Features . Zum Beispiel kann man damit dem Parser mitteilen, daß man eine spezielle Behandlung der Namensräume wünscht. Die Option erhält eine Hashreferenz als Argument mit den Feature-Namen als Schlüssel und Werten wie 0 (aus) oder 1 (an). Eine andere Möglichkeit ist die Verwendung der Methode set_feature( ). Zum Beispiel erhält man einen validierenden Parser, indem man eine der beiden folgenden Varianten anwendet:

$parser->parse( Features => { 'http://xml.org/sax/properties/validate' => 1 } );
$parser->set_feature( 'http://xml.org/sax/properties/validate', 1 );

Eine komplette Liste der von SAX2 definierten Features finden Sie in der Dokumentation von SAX. Wenn ein Parser spezielle Fähigkeiten hat, kann er diese über eigene Features an- und abschaltbar machen. Mit Hilfe der Methode get_features( ) kann man eine Liste aller Features eines Parsers abrufen, und mit get_feature( ) kann man die Unterstützung eines speziellen Features überprüfen, das man der Methode als Argument übergibt.

Beispiel: Ein Generator

Die Implementierung eines SAX-Parsers ist relativ einfach. Die meiste Arbeit wird von einer Basisklasse namens XML::SAX::Base erledigt. Man leitet eine Subklasse ab und überschreibt oder implementiert alle Methoden, deren Standardimplementierungen nicht passen oder fehlen. Das ist recht bequem, vor allem aber auch sicherer und verläßlicher als selbst alles neu zu schreiben. Zum Beispiel wird automatisch überprüft, ob der verwendete Handler eine bestimmte Methode implementiert.

Das nächste Beispiel zeigt, wie einfach man einen Parser mit Hilfe von XML::SAX schreiben kann. Dabei handelt es sich um einen Generator, ähnlich dem aus dem Abschnitt »Verarbeitung von Nicht-XML-Daten«. Allerdings sollen diesmal keine Excel-Tabellen gelesen werden, sondern Logdateien eines Webservers. Der Parser liest eine Zeile wie die folgende:

 10.16.251.137 - - [26/Mar/2000:20:30:52 -0800] "GET /index.html HTTP/1.0" 200 16171 

Daraus soll dann das folgende XML-Fragment entstehen:

<entry>
<ip>10.16.251.137</ip>
<date>26/Mar/2000:20:30:52 -0800</date>
<req>GET /index.html HTTP/1.0</req>
<stat>200</stat>
<size>16171</size>
</entry>

Das Beispiel unten implementiert den XML::SAX-Generator. Die erste Funktion in der Klasse ist parse( ). Normalerweise würden wir keine eigene parse( )-Methode schreiben, weil das die Basisklasse bereits tut. Allerdings geht diese Methode davon aus, daß es sich bei den Eingabedaten um XML handelt. Das ist in unserem Fall nicht gegeben. Also überschreiben wir diese Methode mit einer speziellen Methode für Logdateien.

Beispiel: SAX-Generator zum Lesen von Webserver-Logdateien

package LogDriver;

require 5.005_62;     
use strict;
use XML::SAX::Base;
our @ISA = ('XML::SAX::Base');
our $VERSION = '0.01';

sub parse {
     my $self = shift;
     my $file = shift;
     if( open( F, $file )) {
          $self->SUPER::start_element({ Name => 'server-log' });
          while( <F> ) {
          $self->_process_line( $_ );
          }
          close F;
          $self->SUPER::end_element({ Name => 'server-log' });
    }
}
  
sub _process_line {
     my $self = shift;
     my $line = shift;
    
     if( $line =~
          /(\S+)\s\S+\s\S+\s\[([^\]]+)\]\s\"([^\"]+)\"\s(\d+)\s(\d+)/ ) {
          my( $ip, $date, $req, $stat, $size ) = ( $1, $2, $3, $4, $5 );
    
          $self->SUPER::start_element({ Name => 'entry' });
  
          $self->SUPER::start_element({ Name => 'ip' });
          $self->SUPER::characters({ Data => $ip });
          $self->SUPER::end_element({ Name => 'ip' });
  
          $self->SUPER::start_element({ Name => 'date' });
          $self->SUPER::characters({ Data => $date });
          $self->SUPER::end_element({ Name => 'date' });
  
          $self->SUPER::start_element({ Name => 'req' });
          $self->SUPER::characters({ Data => $req });
          $self->SUPER::end_element({ Name => 'req' });
  
          $self->SUPER::start_element({ Name => 'stat' });
          $self->SUPER::characters({ Data => $stat });
          $self->SUPER::end_element({ Name => 'stat' });
  
          $self->SUPER::start_element({ Name => 'size' });
          $self->SUPER::characters({ Data => $size });
          $self->SUPER::end_element({ Name => 'size' });
  
          $self->SUPER::end_element({ Name => 'entry' });

     }
}
  
1;

Da Logdateien wie diese zeilenorientiert sind (ein Eintrag pro Zeile), macht es Sinn, je eine Zeile von einer speziellen Methode _process_line( ) verarbeiten zu lassen. Die Methode extrahiert die verschiedenen Bestandteile aus dem String und erzeugt entsprechende XML-Elemente. Die Methode parse( ) beschränkt sich darauf, die gesamte Datei in einzelne Zeilen aufzuteilen und eine nach der anderen an den Prozessor zu übergeben.

Beachten Sie, daß wir Handlermethoden nicht direkt aufrufen. Statt dessen übergeben wir die Daten an unsere Basisklasse, indem wir die entsprechenden Methoden aufrufen. Die Basisklasse ist damit eine abstrakte Schicht zwischen dem Parser und dem Handler. Das ist bequem für Sie als den Entwickler eines Parsers, weil die Basisklasse prüfen muß, ob der Handler ein Event angefordert hat. Wie schon gesagt, liegt die Aufgabe der Basisklasse darin, unsere Arbeit zu erleichtern.

Prüfen wir den Parser jetzt. Unter der Voraussetzung, daß das Modul bereits installiert ist (wie man einen XML::SAX-Parser installiert, erfahren Sie im nächsten Abschnitt), ist ein Anwendungsprogramm recht einfach. Das nächste Beispiel erzeugt eine Handlerklasse und wendet den gerade geschriebenen Handler darauf an.

Beispiel: Ein Programm zum Testen des SAX-Generators

use XML::SAX::ParserFactory;
use LogDriver;
my $handler = new MyHandler;
my $parser = XML::SAX::ParserFactory->parser( Handler => $handler );
$parser->parse( shift @ARGV );

package MyHandler;     

# Objekt mit Optionen initialisieren
#
sub new {
     my $class = shift;
     my $self = {@_};
     return bless( $self, $class );
}

sub start_element {
     my $self = shift;
     my $data = shift;
     print "<", $data->{Name}, ">";
     print "\n" if( $data->{Name} eq 'entry' );
     print "\n" if( $data->{Name} eq 'server-log' );
}

sub end_element {
     my $self = shift;
     my $data = shift;
     print "<", $data->{Name}, ">\n";
}

sub characters {
     my $self = shift;
     my $data = shift;
     print $data->{Data};
}

Wir benutzen XML::SAX::ParserFactory, um zu zeigen, wie man einen registrierten Parser auswählen kann. Wenn man möchte, kann man den Parser mit Attributen versehen, so daß er später alleine aufgrund dieser Attribute und nicht nur über seinen Namen ausgewählt werden kann.

Die Handlerklasse ist nicht besonders kompliziert; die Events werden in einen XML-Strom umgewandelt. Jeder Handler erhält eine Hashreferenz als Argument, die mit den jeweiligen Properties versehen ist. Zum Beispiel findet sich der qualifizierte Elementname unter der Property Name. Es funktioniert alles so, wie wir uns das vorstellen.

Installation eines eigenen Parsers

Unsere Behandlung von XML::SAX wäre nicht komplett, ohne zu zeigen, wie man ein Modul mit einem eigenen Installationspaket schreibt, das für einen korrekten Eintrag des Parsers in den Katalog von XML::SAX sorgt. Das ist nicht weiter schwer, die meiste Arbeit erledigt das Programm h2xs. Es wurde ursprünglich geschrieben, um die Erstellung von Perl-Modulen in C zu erleichtern, leistet inzwischen aber auch bei reinen Perl-Modulen unschätzbare Dienste.

In diesem Fall wollen wir das Modul benutzen, um eine Installationsroutine wie die der CPAN-Module zu bekommen. Wir beginnen ein neues Projekt mit der Ausführung des folgenden Kommandos:

 h2xs -AX -n LogDriver 

h2xs erzeugt automatisch ein neues Verzeichnis namens LogDriver mit den folgenden Dateien.

LogDriver.pm
Ein Template für unser neues Modul, in das im wesentlichen nur noch die gewünschten Subroutinen einzutragen sind.

Makefile.PL
Ein Perl-Programm zur Erzeugung eines Makefile. Dieses ist letzten Endes für die Installation des Moduls verantwortlich. (Für CPAN-Benutzer klingt das vertraut, oder?)

test.pl
Ein Template für ein Testprogramm, mit dem hinterher die erfolgreiche Installation überprüft werden kann.

Changes, Manifest
Andere Hilfsdateien, die bei der Installation helfen sollen und Informationen für die Anwender enthalten.

LogDriver.pm, das zu installierende Modul, benötigt nicht viel Quelltext, um h2xs glücklich zu machen. Es muß nur eine einzige Variable $VERSION gesetzt werden.

Wie Sie bereits von der Installation von CPAN-Modulen wissen, beginnt man die Installation mit der Ausführung des Kommandos perl Makefile.PL. Durch das Kommando wird eine Datei namens Makefile erzeugt, die die Installationsprozedur konfiguriert. Anschließend kann man mit den Kommandos make und make install die Installation abschließen.

Jegliche Abweichung von der Standardinstallation muß in Makefile.PL einprogrammiert werden. Bevor wir Hand daran legen, sieht dieses Programm wie folgt aus:

use ExtUtils::MakeMaker;
WriteMakefile(
     'NAME' => 'LogDriver', # Modulname
     'VERSION_FROM' => 'LogDriver.pm', # Liest Version aus $VERSION
);

Das Argument von WriteMakeFile( ) ist ein Hash mit Properties des Moduls. Diese werden bei der Erzeugung des Makefile benutzt. Wir können hier weitere Properties eintragen, um die Installationsprozedur etwas schlauer zu machen. In unserem Fall fügen wir die folgende Zeile ein:

 'PREREQ_PM' => { 'XML::SAX' => 0 } 

Damit wird das Modul XML::SAX als Voraussetzung von LogDriver deklariert. Bei der Installation wird überprüft, ob dieses Modul bereits vorhanden ist. Falls nicht, wird eine Fehlermeldung ausgegeben, und das Programm wird beendet. Der Parser soll nicht ohne das für seinen Einsatz nötige Framework installiert werden.

Die folgende Subroutine muß noch in Makefile.PL eingefügt werden:

sub MY::install {     
     package MY;
     my $script = shift->SUPER::install(@_);
     $script =~ s/install :: (.*)$/install :: $1 install_sax_driver/m;
     $script .= <<"INSTALL";
  
     install_sax_driver :
          \t\@\$(PERL) -MXML::SAX -e "XML::SAX->add_parser(q(\$(NAME)))->save_parsers( )"
  
INSTALL
  
     return $script;
}

Damit wird der Parser in den von XML::SAX betreuten Katalog eingetragen. Nun kann unser Modul installiert 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