Python unter Linux: XML

WARNUNG: Dieses Kapitel ist vollständig veraltet und sollte ersetzt oder entfernt werden. Es beschreibt nicht die aktuell zumeist verwendeten XML-Bibliotheken ElementTree und lxml, sondern das ältere, langsame und sehr speicherintensive MiniDOM-Paket und die kompliziert zu verwendende SAX-Bibliothek. Diesen gegenüber bieten ElementTree und lxml sehr hohe Geschwindigkeit und Speichereffizienz, leichtere Bedienbarkeit und eine wesentlich höhere Abdeckung von XML-Standards und -Features (z.B. Validierung, XPath oder XSLT). Während es für SAX zumindest noch einige wenige vertretbare Anwendungsfälle gibt, sollte neu geschriebener Code insbesondere nicht mehr MiniDOM verwenden.

Einleitung Bearbeiten

XML ist eine Auszeichnungssprache, mit der sich andere Sprachen definieren lassen. Ebenfalls nutzt man XML, um hierarchisch gegliederte Dokumente zu erzeugen. Mehr über XML findet sich in den Wikibooks XML und XSLT. Eine der Sprachen, die mit Hilfe von XML definiert wurden ist XHTML (HTML). Dieses Kapitel deckt Teile der XML-Module ab, die mit Python mitgeliefert werden. Wenn Sie höherwertige Anwendungen mit Schemata und Namespaces benötigen, sollten Sie sich das Modul python-libxml2 anschauen. Es lässt sich mit den Paketverwaltungstools ihrer Linux-Distribution leicht installieren.

XML-Dokument erzeugen Bearbeiten

Das folgende Beispiel erzeugt ein XML-Dokument und gibt es formatiert aus. Wir benutzen hierzu das Modul xml.dom. Das Dokument enthält drei Knoten (Node oder Element genannt), das eigentliche Dokument, Test1-Knoten, einen Knoten namens Hello, der ein Attribut trägt und einen Textknoten:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.dom

# Dokument erzeugen
implement = xml.dom.getDOMImplementation()
doc = implement.createDocument(None, "Test1", None)

# Child Element "Hello"
elem = doc.createElement("Hello")
elem.setAttribute("output", "yes")
text = doc.createTextNode("Hello, World!")
elem.appendChild(text)

# Child an Dokument anhängen
doc.documentElement.appendChild(elem)

# Ausgeben
print doc.toprettyxml()
  Ausgabe

user@localhost:~$ ./xml1.py

<?xml version="1.0" ?>
<Test1>
        <Hello output="yes">
                Hello, World!
        </Hello>
</Test1>

Mit xml.dom.getDOMImplementation() beschaffen wir uns Informationen über die aktuelle XML-Implementation. Optional können wir mit der Funktion Eigenschaften anfordern, es würde dann eine passende Implementation herausgesucht. Diese Implementation benutzen wir, um ein eigenes Dokument mit dem so genannten Root-Element zu erzeugen: createDocument(). Dieser Funktion könnten wir noch den Namespace und den Dokumententyp mitteilen.

Haben wir ein Dokument erzeugt, können wir mit createElement() nach Belieben neue Elemente erstellen. Knoten können Attribute haben, die man mit setAttribute() in das Element einfügt. Wird ein Element erstellt, so wird es nicht sofort in das Dokument eingefügt. appendChild() erledigt dies.

createTextNode() erzeugt einen speziellen Knoten, der nur aus Text besteht. Dieser kann keine weiteren Attribute haben. Das Root-Element holt man sich mit doc.documentElement, an dieses kann man zum Schluss den Elemente-Baum anhängen.

XML-Dokumente möchte man auch auf die Festplatte schreiben. Das folgende Beispiel implementiert eine Adresseneingabe. Lässt man den Namen bei der Eingabe leer, so wird die Eingabe abgebrochen, das XML-Dokument ausgegeben. Bitte beachten Sie, dass die Datei bei jedem Aufruf überschrieben wird. Es sollte sich also nach Möglichkeit keine andere wichtige Datei gleichen Namens im Verzeichnis befinden. Das Beispielprogramm erzeugt einen Baum, der <Adressen> beinhaltet. Jede dieser Adressen wird durch einen <Adresse>-Knoten repräsentiert, in dem sich <Name>- und <Anschrift>-Knoten befinden. Als Besonderheit wird noch ein Kommentar in jedes Adressenfeld geschrieben:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.dom

# Dokument erzeugen
implement = xml.dom.getDOMImplementation()
doc = implement.createDocument(None, "Adressen", None)

while True:
    # Namen holen, ist er leer dann Abbruch
    name_text = raw_input("Name der Person: ")
    if name_text == "": break

    # Kommentar
    kommentar_text = "Adresse von %s" % (name_text)
    commentNode = doc.createComment(kommentar_text)

    # neues Adresse-Elemen
    adresseElem = doc.createElement("Adresse")
    adresseElem.appendChild(commentNode)
    # Name anfügen
    nameElem = doc.createElement("Name")
    adresseElem.appendChild(nameElem)
    nameTextElem = doc.createTextNode(name_text)
    nameElem.appendChild(nameTextElem)
    # Anschrift
    anschrift_text = raw_input("Anschrift: ")
    anschriftElem = doc.createElement("Anschrift")
    adresseElem.appendChild(anschriftElem)
    anschriftTextElem = doc.createTextNode(anschrift_text)
    anschriftElem.appendChild(anschriftTextElem)
    # Anhängen an Dokument
    doc.documentElement.appendChild(adresseElem)

# Ausgeben
datei = open("testxml2.xml", "w")
doc.writexml(datei, "\n", "  ")
datei.close()

So könnte bei geeigneter Eingabe die Datei testxml2.xml aussehen:

  Ausgabe

user@localhost:~$ cat testxml2.xml

<?xml version="1.0" ?>
<Adressen>
  <Adresse>
    <!--Adresse von Sir Spamalot-->
    <Name>
      Sir Spamalot
    </Name>
    <Anschrift>
      Spamhouse 123
    </Anschrift>
  </Adresse>
  <Adresse>
    <!--Adresse von Lady of the Lake-->
    <Name>
      Lady of the Lake
    </Name>
    <Anschrift>
      West End 23
    </Anschrift>
  </Adresse>
  <Adresse>
    <!--Adresse von Brian-->
    <Name>
      Brian
    </Name>
    <Anschrift>
      Im Flying Circus
    </Anschrift>
  </Adresse>
</Adressen>

Dieses Beispiel enthält verschachtelte Elemente. Es wird nach Namen und Anschrift gefragt, dazu werden passende Knoten erzeugt und aneinander gehängt. Nach der Eingabe des Namens erzeugt createComment() einen Kommentar, der ebenfalls mit appendChild() angehängt werden kann. Neu ist, dass wir das erzeugte Dokument schreiben. Dazu nutzen wir writexml(), welche eine offene Datei erwartet. Außerdem geben wir noch die Art der Einrückung mit: Zwischen zwei Elementen soll eine neue Zeile eingefügt werden, je zwei Element-Ebenen sollen durch Leerzeichen getrennt werden.

XML lesen Bearbeiten

Eine XML-Datei kann man bequem mit den Funktionen von xml.dom.minidom, einer "leichtgewichtigen" Implementation von XML lesen. Hierzu nutzen wir eine rekursive Funktion, um uns alle Knoten auszugeben.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.dom.minidom

datei = open("testxml2.xml", "r")
dom = xml.dom.minidom.parse(datei)
datei.close()

def dokument(domina):
    for node in domina.childNodes:
        print "NodeName:", node.nodeName,
        if node.nodeType == node.ELEMENT_NODE:
            print "Typ ELEMENT_NODE"
        elif node.nodeType == node.TEXT_NODE:
            print "Typ TEXT_NODE, Content: ", node.nodeValue.strip()
        elif node.nodeType == node.COMMENT_NODE:
            print "Typ COMMENT_NODE, "
        dokument(node)

dokument(dom)
  Ausgabe

user@localhost:~$ ./xml3.py
NodeName: Adressen Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Adresse Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #comment Typ COMMENT_NODE,
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Name Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: Sir Spamalot
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Anschrift Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: Spamhouse 123
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Adresse Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #comment Typ COMMENT_NODE,
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Name Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: Lady of the Lake
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Anschrift Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: West End 23
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Adresse Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #comment Typ COMMENT_NODE,
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Name Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: Brian
NodeName: #text Typ TEXT_NODE, Content:
NodeName: Anschrift Typ ELEMENT_NODE
NodeName: #text Typ TEXT_NODE, Content: Im Flying Circus
NodeName: #text Typ TEXT_NODE, Content:
NodeName: #text Typ TEXT_NODE, Content:

Die Überraschung dürfte groß sein. In einer so kleinen Textdatei werden so viele Knoten gefunden. Der Ausspruch "Man sieht den Wald vor lauter Bäumen nicht mehr" dürfte hier passen. Im Programm wird mit xml.dom.minidom.parse() eine offene Datei gelesen. Die Funktion dokument() wird mit jedem Kindelement, das mit childNodes ermittelt wird, neu aufgerufen. Kinder können Elemente, wie <Adresse> oder Kommentare oder beliebiger Text sein. Beliebigen Text, in der Ausgabe an den Stellen mit #text zu erkennen, haben wir alleine dadurch recht oft, da wir den Text mit Einrückungszeichen (Indent) beim Speichern gefüllt haben. Nur sehr wenige TEXT_NODE-Zeilen stehen hierbei für sinnvollen Text, alle anderen sind Knoten, die nur wegen der Leerzeichen und Newline-Zeichen (\n) hineingeschrieben wurden. Da wir strip() bei jeder Ausgabe benutzen, sehen wir von diesen Zeichen nichts.

Um nur die interessanten Elemente auszugeben, müssen wir die Struktur berücksichtigen. Wir lassen alles außer Acht, was nicht zum gewünschten Elementinhalt passt:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.dom.minidom

datei = open("testxml2.xml", "r")
dom = xml.dom.minidom.parse(datei)
datei.close()

def liesText(pretext, knoten):
    for k in knoten.childNodes:
        if k.nodeType == k.TEXT_NODE:
            print pretext, k.nodeValue.strip()

def liesAdressen(knoten):
    for num, elem in enumerate(knoten.getElementsByTagName("Adresse")):
        print "%d. Adresse:" % (num + 1)
        for knotenNamen in elem.getElementsByTagName("Name"):
            liesText("Name:", knotenNamen)
        for knotenAnschrift in elem.getElementsByTagName("Anschrift"):
            liesText("Anschrift:", knotenAnschrift)
        print

def dokument(start):
    for elem in start.getElementsByTagName("Adressen"):
        liesAdressen(elem)

dokument(dom)
  Ausgabe

user@localhost:~$ ./xml4.py
1. Adresse:
Name: Sir Spamalot
Anschrift: Spamhouse 123

2. Adresse:
Name: Lady of the Lake
Anschrift: West End 23

3. Adresse:
Name: Brian
Anschrift: Im Flying Circus

Diese Ausgabe ist wesentlich nützlicher. Mit getElementsByTagName() können wir für die interessanten Tags alle Kindelemente holen. In der Funktion dokument() wird so für den Adressen-Tag, das Root-Element, genau einmal die Funktion liesAdressen() aufgerufen. Diese Funktion erzeugt die eigentliche Adressenausgabe. Es wird dabei über alle Adresse-Elemente iteriert, da Kindelemente von Adresse nur Name und Anschrift sein können, werden diese in der Funktion ebenfalls verarbeitet. Alle Textelemente werden von liesText() bearbeitet.

SAX Bearbeiten

SAX ist ein Ereignis-basierendes Protokoll zum Lesen von XML-Dokumenten. Jedes Mal, wenn ein Ereignis auftritt, zum Beispiel ein Element gelesen wird, wird eine Methode aufgerufen, die dieses Ereignis behandelt. Anders als bei DOM können so sehr lange XML-Dokumente gelesen werden, die wesentlich größer sind als der zur Verfügung stehende Speicher. Das folgende Beispiel zeigt, wie man einen Handler erstellt, der auf einige Ereignisse reagieren kann:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.sax

class MiniHandler(xml.sax.handler.ContentHandler):
    def startDocument(self):
        print "ANFANG"

    def endDocument(self):
        print "ENDE"

    def startElement(self, name, attrs):
        print "Element", name

    def characters(self, content):
        s = content.strip()
        if s != "":
            print "Textinhalt:", s

handler = MiniHandler()
datei = open("testxml2.xml", "r")
xml.sax.parse(datei, handler)
datei.close()
  Ausgabe

user@localhost:~$ ./xml5.py
ANFANG
Element Adressen
Element Adresse
Element Name
Textinhalt: Sir Spamalot
Element Anschrift
Textinhalt: Spamhouse 123
Element Adresse
Element Name
Textinhalt: Lady of the Lake
Element Anschrift
Textinhalt: West End 23
Element Adresse
Element Name
Textinhalt: Brian
Element Anschrift
Textinhalt: Im Flying Circus
ENDE

xml.sax.handler.ContentHandler ist die Basisklasse für eigene Handler. In davon abgeleiteten Klassen werden einige Ereignisse wie startDocument() neu definiert, um so passend zur Anwendung darauf reagieren zu können. In unserem Beispiel geben wir die Meldungen aus. Eine Instanz des MiniHandlers wird, neben der offenen Datei, xml.sax.parse() mitgegeben. Das Dokument wird sukzessive verarbeitet, die im MiniHandler anlaufenden Ereignisse dort bearbeitet. Diese sind selbstverständlich nur ein Auszug aller möglichen Ereignisse.

Nützliche Helfer Bearbeiten

Für diese nützlichen Helfer bindet man das Modul xml.sax.saxutils ein:

Funktion Bedeutung
escape(Daten[, Entitäten]) Ersetzt in einem String "Daten" alle Vorkommen von Sonderzeichen durch im Dictionary angegebene Entitäten. Zum Beispiel < durch &lt;. Einige Entitäten werden ohne ein erweitertes Entitäten-Verzeichnis ersetzt.
unescape(Daten[, Entitäten]) Wie escape(), nur umgekehrt. Für ein Beispiel siehe Kapitel Netzwerk. Sie können ein für escape() vorbereitetes Entitäten-Verzeichnis hier nicht wiederverwenden, sondern müssen den Inhalt umkehren.
#!/usr/bin/python
# -*- coding: utf-8 -*-

import xml.sax.saxutils as util

buch = {'ä' : '&auml;'}

print util.escape("Anzahl der Männer < Anzahl aller Menschen!", buch)
  Ausgabe

user@localhost:~$ ./xml6.py
Anzahl der M&auml;nner &lt; Anzahl aller Menschen!

Hier brauchten wir das Entitätenverzeichnis nur für die Umlaute, das Kleiner-Zeichen wurde per Vorgabe ersetzt.

Zusammenfassung Bearbeiten

In diesem Kapitel haben Sie einen Überblick über die Möglichkeiten der Verarbeitung von XML-Dokumenten mit den mitgelieferten Modulen bekommen. Es wurden Techniken wie DOM und SAX vorgestellt. Tiefgreifendere Techniken werden von anderen extern erhältlichen Modulen abgedeckt. Diese liegen außerhalb der Zielsetzung dieses Buches.