58. XML II: SAX

SAX (Simple API for XML) ist eine Schnittstelle zu XML-Parsern, die ähnlich in anderen Sprachen auch vorhanden ist. In ihr schickt der Parser “Ereignisse” (Öffnender Tag, Text, Schließender Tag) an die Anwendung. Sie bietet sich an, wenn man nur Teile einer XML-Datei verabeiten möchte oder das, was man liest, unmittelbar verarbeiten kann.

Zentrale Funktion ist xml.sax.parse, die ein Datei und einen “Handler” übergeben bekommt. Der Handler ist ein Objekt, das die Ereignisse in Form von Methodenaufrufen geschickt bekommt. Ggf. kann die Funktion auch noch ein Objekt zur Fehlerbehandlung übergeben bekommen. Details findet ihr in der Doku zu xml.sax.

Der Handler ist in der Regel eine Unterklasse von xml.sax.handler.ContentHandler. Schlichte ContentHandler ignorieren einfach alle Ereignisse. Um das zu ändern, überschreibt man Methoden, am häufigsten

  • startElement(name, attrs) wird bei einem öffnenden Tag aufgerufen.
  • endElement(name) wird bei einem schließenden Tag aufgerufen.
  • characters(content) wird für Text aufgerufen

Als Beispiel für den Umgang mit XML-Daten mit SAX wollen wir XML “prettyprinten”, also ein XML-Dokument so umformatieren, dass es etwas übersichtlicher wird, Einrückung und alles. Damit die Dinge einfach bleiben, kümmern wir uns erstmal nicht um Attribute und schon gar nicht um die Frage des Whitespace-Handlings in XML.

Wir wollen einfach den Inhalt eines Elements gegenüber den umschließenden Tags einrücken.

Was braucht es dazu? Nun, zunächst müssen wir offenbar in jedem startElement das Startelement soweit eingerückt ausgeben, wie es unserer augenblicklichen Schachtelungstiefe entspricht (diese werden wir uns also merken müssen, womit auch schon der erste Zustand der Klasse klar ist).

Bei jedem endElement müssen wir dann entsprechend “ausrücken” und den schließenden Tag mit der Einrückung des entsprechenden öffenenden Tags ausgeben.

Tags können wir damit, nicht aber eventuelle Daten. Hier wollen wir einerseits wordwrap machen (damit nicht die Zeilenenden “hinten rausgucken”), andererseits muss gewährleistet sein, dass die Daten nicht an Tags “vorbeitrutschen”. Ersteres impliziert, dass wir nicht einfach jedes Mal, wenn wir Zeichen bekommen, diese blind auf den Schirm ausgeben wollen, zweiteres, dass wir nicht einfach alle Daten zu einem Element sammeln und geschlossen ausgeben können, wenn der schließende Tag kommt des Elements kommt – wir müssen bei jedem Tag sehen, ob Daten zur Ausgabe anstehen.

Die Lösung ist, einfach die über characters einkommenden Daten zu sammeln und bei jedem start- oder endElement-Event eine Funktion aufzurufen, die eventuell angefallene Daten ausgibt.

Abweichend vom Plan oben merken wir uns die Schachtelungstiefe nicht direkt, sondern bauen lieber einen Stack (hier als Liste modelliert) von Tags, so dass wir jederzeit sämtliche einschließenden Elemente im Zugriff haben könnten, wenn wir das wollten. Die Länge dieser Liste gibt die Schachtelungstiefe.

Hier brauchen wir das eigentlich nicht, aber bei der Verarbeitung kontextfreier Sprachen (in denen Verschachtelung vorkommen kann) ist der Stack eine ganz typische Datenstruktur, sie wird in SAX-Handlern also häufig vorkommen. Die Idee eines Stacks ist, dass man an einem Ende Daten anhängen kann (“Daten auf den Stack legen”, push) und sie von dort auch wieder wegnehmen kann (“vom Stack runternehmen”, pop), sonst aber nicht viel damit tun kann. Diese Beschränkung mag zunächst willkürlich erscheinen, sie hilft aber sehr, Dinge zu strukturieren.

Python-Listen haben die beiden Methoden schon; pop heißt pop (tatsächlich ist die Python-Version etwas allgemeiner und erlaubt durch Angabe eines optionalen Elements ein anderes als das letzte Element zu holen und gleichzeitig zu entfernen, aber genau das wollen wir hier nicht), währen push einfach unser gutes altes append ist. Wir modellieren daher den Stack einfach durch eine Liste, bei der wir vereinbaren, dass wir nur pop und append verwenden. Durch Verkapseln in einer Klasse könnte man diese Vereinbarung auch, nun, betonen, aber das ist zunächst nicht nötig.

Hier der entscheidende Code (der volle Code ist im Anhang dieser Seite):

class XmlPrettyPrinter(ContentHandler):
  def __init__(self, scrwid=79, *args):
    ContentHandler.__init__(self, *args)
    self.scrwid, self.indentSize = scrwid, 2
    self.elementStack, self.charCache = [], []

  def startElement(self, name, attrs):
    self._emitChars()
    self._printIndented("<%s>"%name)
    self.elementStack.append(name)

  def endElement(self, name):
    self._emitChars()
    self.elementStack.pop()
    self._printIndented("</%s>"%name)

  def characters(self, chars):
    self.charCache.append(chars)

  def _emitChars(self):
    contents = "".join(self.charCache).strip()
    if contents:
      self._printIndented(contents)
      self.charCache = []

parse(sys.argv[1], XmlPrettyPrinter())

Das komische *args klären wir auf der nächsten Folie.

Vielleicht ahnt man an dieser Stelle schon den Pferdefuß von SAX: Man muss beim Parsen unter Umständen Unmengen von State, also Informationen zum Kontext eines Elements, mitschleppen. Das wird besonders schlimm, wenn man z.B. Referenzen innerhalb eines Dokuments auflösen muss, das Dokument in nichttrivialer Weise manipulieren will oder auch nur etwas komplexere Datenstrukturen aus XML-Dateien aufbauen will. In diesen Fällen bietetsich eher DOM an.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Wir haben hier den Stack einfach durch eine Liste simuliert (die pop-Methode der Listen hat uns etwas geholfen). Schöner ist es natürlich, wenn man eine richtige Stack-Klasse hat, die die Methoden push(el) -> None (“auf den Stack legen”), pop() -> element (“vom Stack nehmen”) und peek() -> element (das oberste (letzte) Element des Stacks “ansehen”, ohne den Stack zu verändern) unterstützen soll. Außerdem soll len(aStack) die augenblickliche Zahl der Elemente im Stack zurückgeben. Schreibt eine Klasse, die das tut.

(2)

Schreibt einen FilteringXmlPrettyPrinter, der alle Elemente außer denen in einer Menge (bzw. den Schlüsseln eines Dictionaries) ignoriert. Man soll die Klasse also mit einem Dictionary instanzieren, und alle Elemente, deren Schlüssel nicht in dem Dictionary vorkommen, sollen (samt Inhalt!) ignoriert werden.

(3)

Sorgt dafür, dass auch eventuelle Attribute der Elemente ausgegeben werden (d.h. scheibt in startElement etwas rein, das sinnvoll mit dem atts-Argument umgeht). Ihr wollt dafür wahrscheinlich die Dokumentation zu ContentHandlern lesen – andererseits unterstützt das Objekt, das wir über attr bekommen, im Groben alles, was Dictionaries auch können.

Dateien zu diesem Abschnitt


Markus Demleitner

Copyright Notice