SAX-Event-Handler

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

Um ein typisches SAX-Modul in ein Programm einzubinden, muß man ihm ein Objekt geben, dessen Methoden Handler für SAX-Events implementieren. Die folgende Tabelle enthält eine Liste der Methoden eines typischen Handlerobjekts. Der SAX-Parser übergibt dem Handler einen Hash mit den für das jeweilige Ereignis relevanten Attributen. Zum Beispiel enthält ein Handler für den Start eines Elements den Elementnamen und eine Liste von Attributen.

Tabelle: Handlermethoden der Perl-SAX-Schnittstelle

Methodenname Event Properties
start_document Der Parser hat begonnen, das Dokument zu verarbeiten. Dies ist das allererste Event. (Keine vorgegeben)
end_document Der Parser hat das gesamte Dokument gelesen und beendet seine Arbeit. Dies ist das letzte Event. (Keine vorgegeben)
start_element Das Start-Tag eines Elements wurde gefunden. Dies kann auch ein leeres Element sein. Name, Attribute
end_element Das End-Tag eines Elements wurde gefunden. Bei einem leeren Element folgt dieses Event unmittelbar auf start_element . Name
characters Ein String aus Textdaten wurde gefunden. Daten
processing_instruction Eine PI wurde gelesen. Ziel, Daten
comment Der Parser hat einen Kommentar gelesen. Daten
start_cdata Der Beginn eines CDATA-Abschnitts wurde gefunden. Im Normalfall folgt anschließend ein Event characters mit dem im CDATA-Abschnitt stehenden Text. (Keine vorgegeben)
end_cdata Ein CDATA-Abschnitt endet. Falls der Abschnitt leer war, folgt dieses Event unmittelbar auf start_cdata . (Keine vorgegeben)
entity_reference Eine interne Entityreferenz wurde gefunden. Die interne Referenz unterscheidet sich von der externen dadurch, daß bei der letzteren unter Umständen eine Datei geladen werden muß. Name, Wert

Einige Anmerkungen zu den Handlermethoden:

  • Im Falle eines leeren Elements werden unmittelbar nacheinander die Events start_element( ) und end_element( ) aufgerufen. Es gibt keine Sonderbehandlung leerer Elemente.
  • Der Handler characters( ) kann mehrere Male nacheinander aufgerufen werden, um einen zusammenhängenden String in Teilen zu übergeben. Zum Beispiel ist es denkbar, daß der Parser innerhalb eines Textabschnitts eine Entityreferenz findet. Für ihn ist es dann unter Umständen effizienter, zunächst den bereits gelesenen Text als ein Event zu signalisieren und dann den Inhalt des Entity in einem weiteren Event. Aber auch bei sehr, sehr langen Textabschnitten ist es sinnvoller, diese in kleine Teile aufzuspalten, als den ganzen Text am Stück in den Speicher zu laden, da man ja übermäßigen Speicherverbrauch vermeiden will.
  • Der Handler characters( ) wird auch für Text aufgerufen, der ausschließlich aus Leerzeichen besteht, selbst wenn der Handler diese für nicht signifikant hält. In XML werden alle Zeichen als gleichberechtigt angesehen: Es ist ganz einfach effizienter, hier keine Unterscheidung zu treffen.
  • Handler für PIs, Kommentare oder CDATA-Abschnitte sind optional. Fehlen diese Handler, werden die Daten in Kommentaren sowie PIs einfach unterdrückt. Bei CDATA-Abschnitten erfolgt natürlich ein Aufruf des Handlers characters() , Textdaten gehen also nicht verloren.
  • Die Handler start_cdata( ) und end_cdata( ) bekommen keine Daten übergeben. Sie sollen lediglich signalisieren, ob die per characters( ) übergebenen Textdaten aus einem CDATA-Abschnitt stammen oder nicht. Prinzipiell gibt es keinen Unterschied, auf welche Art und Weise Textdaten im XML-Dokument erfaßt sind.
  • Fehlt ein Handler entity_reference( ) , dann werden interne Entityreferenzen automatisch durch den Parser aufgelöst, und der resultierende Text oder Markup wird als Teil des XML-Dokuments behandelt. Wenn allerdings ein Handler entity_reference( ) vorhanden ist, werden Entityreferenzen nicht aufgelöst, und Sie können selbst entscheiden, was Sie damit tun wollen.

Schauen wir uns als nächstes ein Beispiel an. Wir schreiben einen Filter. Ein Filter ist ein spezieller Prozessor, der eine Kopie des Originaldokuments erzeugt, dabei aber einige kleinere Änderungen vornimmt. Unser Filter nimmt die folgenden Änderungen vor:

  • XML-Kommentare werden in <comment> -Elemente umgewandelt,
  • PIs werden entfernt.
  • Erscheint ein <literal> -Element innerhalb eines Elements <programlisting> , dann wird das Tag entfernt. Es ist dabei egal, wie tief die Verschachtelung ist.

Sie finden den Quelltext dieses Programms im folgenden Beispiel. Wie im letzten Programm initialisieren wir den Parser auch hier mit einer Menge von Handlern. Allerdings sind die Handler diesmal bequem in einem Objekt namens MyHandler verpackt. Beachten Sie, daß wir mehr Handler implementiert haben als in den früheren Beispielen, weil wir uns auch mit Kommentaren, PIs und dem Dokumentprolog beschäftigen wollen.

Beispiel: Ein Filterprogramm

# Initialisierung des Parsers      
#
use XML::Parser::PerlSAX;
my $parser = XML::Parser::PerlSAX->new( Handler => MyHandler->new( ) );

if( my $file = shift @ARGV ) {
      $parser->parse( Source => {SystemId => $file} );
} else {
      my $input = "";
      while( <STDIN> ) { $input .= $_; }
      $parser->parse( Source => {String => $input} );
}
exit;

#
# Globale Variablen
#
my @element_stack; # Gedächtnis für aktuell offene Elementnamen
my $in_intset; # Flag: Sind wir im internen Bereich der DTD?

###
### Package des Handlers
###
package MyHandler;

#
# Initialisierung des Objekts
#
sub new {
      my $type = shift;
      return bless {}, $type;
}

#
# Handler für Start eines Elements: Ausgabe des Start-Tag und der Attribute
#
sub start_element {
      my( $self, $properties ) = @_;
      # Falls wir noch im internen Bereich der DTD sind, müssen wir ihn jetzt abschließen
      output( "]>\n" ) if( $in_intset );
      $in_intset = 0;
      # Elementnamen im Gedächtnis behalten, indem er auf den Stack geschoben wird
      push( @element_stack, $properties->{'Name'} );
      # Ausgabe des Tags und der Attribute, AUSSER es ist ein <literal>
      # innerhalb eines <programlisting>
      unless( stack_top( 'literal' ) and
      stack_contains( 'programlisting' )) {
      output( "<" . $properties->{'Name'} );
      # Beachten Sie: Im Hash %attributes geht die Reihenfolge der Attribute verloren.
      my %attributes = %{$properties->{'Attributes'}};
      foreach( keys( %attributes )) {
      output( " $_=\"" . $attributes{$_} . "\"" );
}
output( ">" );
}
}

#
# Behandlung eines Elementendes: Ausgabe des End-Tags, AUSSER es ist von einem
# <literal> innerhalb eines <programlisting>
#
sub end_element {
      my( $self, $properties ) = @_;
      output( "</" . $properties->{'Name'} . ">" )
      unless( stack_top( 'literal' ) and
      stack_contains( 'programlisting' ));
      pop( @element_stack );
}

#
# Handler für das Event Textdaten
#
sub characters {
my( $self, $properties ) = @_;
# Der Parser hat eventuell Entityreferenzen für uns aufgelöst. Wir müssen sie also
# unsererseits wieder herstellen, um ein gültiges XML-Dokument zu erhalten.
      my $data = $properties->{'Data'};
      $data =~ s/\&/\&/;
      $data =~ s/</\&lt;/;
      $data =~ s/>/\&gt;/;
      output( $data );
}

#
# Handler eines Kommentars: Umwandlung in ein <comment>-Element
#
sub comment {
      my( $self, $properties ) = @_;
      output( "<comment>" . $properties->{'Data'} . "</comment>" );
}

#
# Handler einer PI: Lösche sie
#
sub processing_instruction {
# Tue nichts!
}

#
# Handler einer internen Entityreferenz (wir möchten sie nicht auflösen)
#
sub entity_reference {
my( $self, $properties ) = @_;
output( "&" . $properties->{'Name'} . ";" );
}

sub stack_top {
      my $guess = shift;
      return $element_stack[ $#element_stack ] eq $guess;
}

sub stack_contains {
      my $guess = shift;
      foreach( @element_stack ) {
      return 1 if( $_ eq $guess );
}
return 0;
  
}
sub output {
      my $string = shift;
      print $string;
}

Ein Blick auf die Handler zeigt uns, daß diese außer dem obligatorischen $self ein weiteres Argument übergeben bekommen. Bei diesem Argument handelt es sich um eine Hashreferenz mit Properties des Events. Diese Technik hat einen Nachteil: Im Start-Handler eines Elements werden die Attribute in einem weiteren Hash übergeben. Die ursprüngliche Reihenfolge der Attribute geht damit verloren. Semantisch gesehen, ist das kein Beinbruch, da die XML-Spezifikation festlegt, daß die Reihenfolge der Attribute keine Rolle spielt. Andererseits gibt es Fälle, wo wir diese Ordnung gerne wiederherstellen würden.

Im Falle eines Filters wie unserem würden wir zum Beispiel gerne ein Werkzeug wie das Unix-Programm diff anwenden, um die Ein- und Ausgabe zu vergleichen. Ein solcher Vergleich würde eine Reihe von Zeilen anmahnen, in denen Attribute einfach vertauscht sind, ohne daß eine tatsächliche Änderung stattgefunden hat. Eine Lösung dieses Problems wäre es, statt diff das Modul XML::SemanticDiff von Kip Hampton einzusetzen. Dieses Modul würde solche rein syntaktischen Unterschiede ignorieren.

Unser Programm ist ein Filter. Seine Ausgabe soll der Eingabe so ähnlich wie möglich sein, von den absichtlichen Änderungen natürlich abgesehen. Das Programm sollte nach Möglichkeit den Prolog erhalten, die PIs, Kommentare usw. Selbst Entityreferenzen sollen unter Umständen erhalten bleiben und nicht unbedingt durch den Parser aufgelöst werden. Aus diesem Grund hat das Programm einige Handler mehr als die früheren Beispiele, in denen wir normalerweise nur an sehr spezifischen Informationen interessiert waren.

Testen wir dieses Programm; die dazu verwendete Eingabedatei finden Sie im Beispiel unten.

Beispiel: Eingabedaten des Filters

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE book
SYSTEM "/usr/local/prod/sgml/db.dtd"
[
   <!ENTITY schwachsinn "hoo hah blah blah">
]>

<book id="mybook">
<?print newpage?>
   <title>Einführung in GRXL</title>
   <chapter id="intro">
      <title>Was ist GRXL?</title>
<!-- Der Titel sollte schmissiger werden. -->
   <para>
Einfach ein weiteres Akronym. Das war unsere ursprüngliche Absicht, aber dann stellte
sich mehr und mehr heraus, wie verblüffend nützlich diese neue Technologie namens
<literal>GRXL</literal> ist. Betrachten Sie das folgende Programm:
   </para>
<?print newpage?>
   <programlisting>AH aof -- %%%%
{{{{{{ let x = 0 }}}}}}
   print! <lineannotation><literal>wow</literal></lineannotation>
oder nicht!</programlisting>
<!-- Welchen Font sollten wir hier benutzen? -->
      <para>
Was das Programm macht? Wen kümmert’s? Es sieht einfach cool aus, oder nicht?
Wirklich, ich kann echt nur eines sagen: "&schwachsinn;".
      </para>
<?print newpage?>
   </chapter>
</book>

Das Ergebnis, das unser Filter liefert, sehen Sie im nächsten Beispiel.

Beispiel: Ausgabe des Filterprogramms

<book id="mybook">
    <title>Einführung in GRXL</title>
    <chapter id="intro">
        <title>Was ist GRXL?</title>
        <comment> Der Titel sollte schmissiger werden. </comment>
        <para>
            Einfach ein weiteres Akronym. Das war unsere ursprüngliche Absicht, aber dann stellte
            sich mehr und mehr heraus, wie verblüffend nützlich diese neue Technologie namens
            <literal>GRXL</literal> ist. Betrachten Sie das folgende Programm:
        </para>
        <programlisting>AH aof -- %%%%
{{{{{{ let x = 0 }}}}}}
print! <lineannotation>wow</lineannotation>
oder nicht!</programlisting>
        <comment> Welchen Font sollten wir hier benutzen? </comment>
        <para>
            Was das Programm macht? Wen kümmert’s? Es sieht einfach cool aus, oder nicht?
            Wirklich, ich kann echt nur eines sagen: "&schwachsinn;".
        </para>
    </chapter>
</book>

Was hat unser Filter genau gemacht? Die XML-Kommentare wurden in <comment>-Elemente umgewandelt. Die PIs wurden gelöscht. Die Tags des innerhalb eines <programlisting>-Elements stehenden <literal>-Elements sind gelöscht, der Text blieb aber erhalten. Das andere <literal>-Element ist unverändert. Entityreferenzen sind unverändert stehen geblieben. So weit, so gut. Es fehlt aber auch einiges. Die XML-Deklaration, die Dokumenttypdeklaration und der interne Bereich sind verschwunden. Ohne Deklaration des Entity schwachsinn ist dieses Dokument aber nicht länger gültig. Die von uns geschriebenen Handler reichen also nicht aus.

Das ist es in der deutschsprachigen Version noch aus einem weiteren Grund nicht: Das Dokument enthält Umlaute. In der Eingabe ist das in Ordnung, denn dort ist ISO 8859-1 als Codierungssystem angegeben. Mit der XML-Deklaration ist aber auch die Deklaration des Codierungssystems verschwunden.

  

  

<< 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