Erzeugung einer Cross-Referenz zu einem XML Dokument mit Python und XSLT
Immer wieder gibt es Situationen, in denen es hilfreich wäre, schnell mal zu sehen, welche Attribute in einem bestimmten Tag innerhalb eines XML-Dokuments vorkommen können. Sind keine DTD oder Schema zur Hand oder das Lesen einer komplexen DTD zu zeitraubend, hilft eine Cross-Referenz, die zeigt, welche Attribute ein Element haben kann, wie oft es im Dokument vorkommt, welche Kindelemente auftreten und welches die Elternelemente sind. Mit Hilfe eines kleinen Python-Skripts und eines XSLT-Stylesheets lässt sich das im Handumdrehen generieren.
Der Versuch, die Aufgabe allein mit XSLT-Mitteln zu lösen, ist sehr aufwändig. Denn zumindest XSLT 1.0 unterstützt Gruppieren nicht ohne Tricks, die auch geübten Stylesheetautoren Kopfschmerzen bereiten. Und Gruppieren von Elementen nach Namen ist eine zentrale Anforderung an ein Dokumentationswerkzeug wie das gerade beschriebene. Denn potenziell kann ein Element auf jeder Hierarchiestufe eines XML-Dokuments vorkommen und es kann jeweils verschiedene Mengen von optionalen Attributen enthalten. Es genügt also nicht, das erstbeste Auftreten eines Elements als Grundlage für eine Dokumentation zu verwenden. Es müssen alle Vorkommen im Dokument überprüft werden. Weiterhin ist es möglich, dass ein Element ein gleichartiges Element als Kindelement enthält. Ein -Element kann rekursiv weitere -Elemente enthalten. Auch das lässt sich mit XSLT-Mitteln allein schlecht analysieren und dokumentieren.
Mit Hilfe einiger Hash-Variablen (dictionary im Python-Jargon) und einem SAX-Parser ist genau dies jedoch relativ einfach zu bewerkstelligen. Als Datentypen finden sich Hash-Variablen in jeder modernen Programmiersprache, ebenso sind SAX-Parser verfügbar. Das Analyseprogramm sammelt beim Ablauf alle Elemente und deren Aufbau in solchen Variablen und gibt sie am Ende in einen XML-Datenstrom aus. Dieser kann dann durch ein XSLT-Stylesheet leicht in eine Crossreferenz umgewandelt werden. Werfen wir zunächst einen kurzen Blick auf das verwendete Python-Skript.
Was man braucht und wie es läuft
Zum Betrieb des Skripts braucht man eine Java-Laufzeitumgebung, eine Python-Laufzeitumgebung (2.1 oder höher), einen Apache Xalan XSLT-Prozessor und eine dazu passende CLASSPATH-Umgebungsvariable und die pyana-Schnittstelle zu Xalan.
Aufrufen kann man das Skript von der Kommandozeile mit:
python xmlDocumenter.py <xmldatei> [xsl-stylesheet] [Ausgabedatei]
Ereignisreich durch das Dokument
Beim Durchlesen des Dokuments generiert ein SAX-Parser bei jedem Start- und Endtag ein Ereignis, das mit einer eigenen Methode behandelt werden kann. Existiert beim
startElement-Ereignis noch kein Eintrag für das Element, werden für Attribute, Kind- und Elternelemente entsprechende Hash-Variablen angelegt. Als Key wird immer der Elementname eingesetzt, als Wert jeweils eine Liste der gefundenen Eltern-, Kind- bzw. Attributnamen. Für den Zähler genügt natürlich ein skalarer Wert. Da wir es mit einem flüchtigen SAX-Datenstrom zu tun haben, brauchen wir noch einen Elementstack. Beim nächsten SAX-Event dient er als Gedächtnis, das weiß, wie das vorige Element hieß und die Abbildung von Elementhierarchien ermöglicht.
Im vorgestellten Skript wird eine dafür angepasste Klasse als Content Handler (Callback-Methode) beim Parser registriert [1]. Das folgende Codefragment zeigt die Initialisierung der Hilfsvariablen in der Content Handler-Klasse:
def __init__(self):
self.estack = ['document root']
self.tags = {} #count occurance of tagname here
self.attributes = {}
self.parents = {}
self.children = {}
Die Hauptarbeit liegt natürlich bei der
startElement-Methode: Sie muss jeweils nachsehen, ob für den Elementnamen ein Eintrag existiert, dito für die im SAX-Datenstrom vorbeirauschenden Attribute. Außerdem muss Sie das aktuelle Elternelement in die Liste der Elternelemente aufnehmen und schließlich den aktuellen Elementnamen auf den Stack legen, denn im SAX-Datenstrom weiß bereits das nächste Element nichts mehr vom Aktuellen (siehe Listing 1).
Listing 1 def startElement(self, name, attr):
if not self.tags.has_key(name):
# new tag occurs, create attribute- and parents list for it
self.tags[name] = 0
self.attributes[name] = []
self.parents[name] = []
self.children[name] = []
if not self.estack[-1] in self.parents[name]:
# find parent on stack top
self.parents[name].append(self.estack[-1])
for att in attr.keys():
# check wheather att is new and keep it
if not att in self.attributes[name]:
self.attributes[name].append(att)
self.tags[name] += 1
self.estack.append(name)
Die
endElement-Methode muss den Stack abräumen und darauf achten, ob der aktuell vom Stack entfernte Name als Kind im Elternelement gelistet ist.
Die
main-Routine des Skripts muss lediglich einen SAX-Parser initialisieren, die angepasste
ContentHandler-Klasse registrieren und einen Dateinamen übergeben. Wenn der Parsing-Vorgang korrekt abgelaufen ist, müssen die Ergebnisse noch in XML-Form gebracht werden. Das geschieht mit einigen geschachtelten Schleifen, die einen XML-String erzeugen:
xmlstring += "<?xml version='1.0' encoding='UTF-8'?>"
xmlstring += "<document name='" + infile +"'>"
for tag in tags:
xmlstring += "<" + tag + " occurs='"+ str(handler.tags[tag]) + "' >"
for att in handler.attributes[tag]:
xmlstring += "<attribute name='"+ att + "'/>"
for child in handler.children[tag]:
xmlstring += "<child name='"+ child + "'/>"
for parent in handler.parents[tag]:
xmlstring += "<parent name='"+ parent + "'/>"
xmlstring += "</" + tag + ">"
xmlstring += "</document>"
Jeder Elementeintrag wird als gleichnamiger Tag ausgegeben und dabei die Häufigkeit seines Auftretens als Attribut mit angegeben. Attribute des Quelldokuments werden, wie Kind- oder Elternelemente auch, innerhalb von entsprechend benannten Elementen dargestellt. Das Ergebnis dieser Ausgabe kann beispielsweise so aussehen:
<DocumentFolder occurs="156">
<attribute name="owner" />
<attribute name="created" />
<attribute name="id" />
<attribute name="title" />
<attribute name="comment" />
<child name="Document" />
<child name="DocumentFolder" />
<child name="ArticleFolder" />
<child name="NewsFolder" />
<parent name="MemberFolder" />
<parent name="DocumentFolder" />
<parent name="ProjectFolder" />
</DocumentFolder>
Sicher ist eine kompaktere Darstellung der Attribute möglich. Im Stylesheet lässt sich diese geschwätzige Form jedoch einfacher verarbeiten.
Die XSLT-Transformation
Anschaulich wäre es, wenn die XML-Zeichenkette in eine Datei geschrieben und von dort aus mit den gewohnten Transformationswerkzeugen in die gewünschte Dokumentation umgewandelt werden könnte. Im vollständigen Python-Code ist diese Möglichkeit auskommentiert. Schneller geht die Verarbeitung allerdings, wenn das Zwischenformat nicht in eine Datei geschrieben, sondern direkt dem Transformationsprozess übergeben wird. Mit Hilfe der Pyana-Bibliothek [2] ist dies sehr bequem möglich. Diese Bibliothek bietet eine Schnittstelle zu Apache Xalan an. Eine funktionierende Java-Laufzeitumgebung und ein installierter Xalan machen die Transformation aus dem Skript heraus zu einem Zweizeiler:
output = Pyana.transform2String(source=str(xmlstring), style=str(xsl))
open(outfile, 'wb').write(output)
Hier werden der
Pyana.transform2String-Methode lediglich der XML-String und der Inhalt des Stylesheets übergeben. Das Transformationsergebnis ist seinerseits eine Zeichenkette, die lediglich noch mit einem Dateinamen versehen und auf die Platte geschrieben werden muss.
Dass hier überhaupt noch eine XSLT-Transformation verwendet wurde und nicht direkt aus dem Python-Skript die HTML-Dokumentation erzeugt wird, hat seinen Sinn darin, dass es mit diesem Aufbau möglich ist, ein weiteres Stylesheet aufzurufen, das etwas anderes als HTML erzeugt. So kann leicht das Gerüst eines XSLT-Stylesheets für das Quelldokument erzeugt werden, das für jedes Element einen
-Eintrag enthält und in einem Kommentar auf Attribute und Kindelemente hinweist. Auf diese Weise können den Stylesheetautoren Schreibarbeit abgenommen und Fehlerquellen vermindert werden.
Resümee
Obwohl XSLT als das Mittel der Wahl bei der Transformation und Analyse von XML-Daten positioniert ist, gibt es Fälle, in denen auf traditionelle Programmierung zurückgegriffen werden sollte, um Geschwindigkeit und Übersichtlichkeit zu erreichen. SAX-Parser eignen sich aufgrund ihrer hohen Ausführungsgeschwindigkeit gut für eine Vorab-Aufbereitung von komplexen XML-Quelldokumenten. Und ist ein XML-Dokument auf kleinere Einheiten eingedampft, arbeitet auch XSLT wieder flott.
Links und Literatur
- [1] Das Beispielskript greift zwei Abschnitte aus dem Python Cookbook von Martelli & Ascher auf (O'Reilly, 2002, S. 285 ff.)
- [2] Pyana: pyana.sourceforge.net/