Informationen gewinnen

(Auszug aus "Python & XML" von Christopher A. Jones & Fred L. Drake, Jr.)

Mit DOM ist es einfach, aus einem Dokument Informationen zu gewinnen. Der wesentliche Teil der Arbeit besteht darin, den Dokumentbaum zu traversieren und die Knoten zu wählen, die für die Anwendung von Interesse sind. Wenn das einmal erledigt ist, ist es normalerweise einfach, eine Methode eines Knotens (oder mehrerer Knoten) aufzurufen oder den Wert eines Knotenattributs zu erhalten. Um jedoch mit DOM Informationen zu extrahieren, müssen wir erst ein DOM-Dokumentobjekt erstellen.

Erstellen eines Dokumentobjekts

Der vielleicht größte Mangel in den DOM-Spezifikationen ist, daß die API keine Möglichkeit bietet, ein Dokumentobjekt aus einem existierenden XML-Dokument zu erhalten. In einem Browser wird das Dokument komplett geladen, bevor der DOM-Clientcode in eingebetteten oder verknüpften Skripten auf das Dokument zugreifen kann, also wird das Dokumentobjekt an einer wohlbekannten Stelle in der Ausführungsumgebung des Skriptes plaziert. Da dieser Ansatz nicht bei Anwendungen funktioniert, die ohne Browser auskommen müssen, brauchen wir also eine andere Lösung.

Unsere Lösung hängt von der jeweiligen DOM-Implementierung ab, die wir benutzen. Aus einer Datei, einem String oder einer URL können wir jederzeit ein Dokumentobjekt erzeugen.

Laden eines Dokuments mit 4DOM

In Python eine DOM-Instanz zu erzeugen, mit der man arbeiten kann, ist einfach. In 4DOM müssen wir nur eine Funktion aufrufen, um ein Dokument aus einer geöffneten Datei zu laden:

from xml.dom.ext.reader.Sax2 import FromXmlStream
doc = FromXmlStream(sys.stdin)

Laden eines Dokuments mit minidom

Im Modul xml.dom.minidom gibt es zwei bequeme Methoden, um ein Dokument zu laden. Die parse-Funktion hat einen Parameter, der ein String mit einem Dateinamen oder einer URL sein kann, aber auch ein zum Lesen geöffnetes Dateiobjekt:

import xml.dom.minidom
doc = xml.dom.minidom.parse(sys.stdin)

Mit einer anderen Funktion, parseString, kann ein Dokument aus einem Puffer geladen werden, der XML-Text enthält, welcher bereits in den Speicher geladen wurde:

 doc = xml.dom.minidom.parseString("<doc>Mein kleines Dokument.</doc>") 

Bestimmen eines Knotentyps

Sie können die in DOM eingebauten Konstanten benutzen, um zu sehen, mit welcher Knotenart Sie es zu tun haben. Es könnte ein Element, ein Attribut, ein CDATA-Abschnitt oder eine Menge anderer Dinge sein. (In Python-DOM-API werden alle Knotentyp-Konstanten aufgelistet.)

Um den Typ eines Knotens zu prüfen, vergleichen Sie sein nodeType-Attribut mit der Konstante, die Sie brauchen. Eine CDATASection-Instanz hat z. B. einen Wert von CDATA_SECTION_NODE für nodeType. Ein Element (eventuell mit Kindern) hat dafür den Wert ELEMENT_NODE. Beim Traversieren eines DOM-Baums können Sie jederzeit einen Knoten testen, um zu bestimmen, ob er das ist, was Sie suchen:

for node in nodes.childNodes:
   if node.nodeType == node.ELEMENT_NODE:
      print "Gefunden!"

Die Node-Schnittstelle hat andere identifizierende Merkmale, z. B. einen Wert und einen Namen. Der Wert von nodeName stellt den Tag-Namen von Elementen dar, während in einem Textknoten nodeName einfach nur #text ist. Das nodeValue-Attribut kann bei Elementen auch leer sein und sollte eigentlich aus den Zeichendaten eines Textelements oder aus anderen Blattelementen bestehen.

Zugreifen auf Kindknoten

Beim Arbeiten mit einem DOM-Baum benutzen Sie vorwiegend Knoten und Knotenlisten. Eine Knotenliste ist eine Ansammlung von Knoten. Jede Ebene eines XML-Dokuments kann als Knotenliste dargestellt werden. Jeder Knoten der Liste kann seinerseits andere Knotenlisten enthalten, was ein Potential für unendliche Komplexität von XML-Dokumenten darstellt.

Die Node-Schnittstelle bietet zwei Methoden, um schnell zu einem bestimmten Kindknoten zu gelangen, und eine Methode, um eine Knotenliste mit den Kindknoten eines Knotens zu erhalten. firstChild bezieht sich auf den ersten Kindknoten irgendeines gegebenen Knotens. Das Ergebnis ist None, wenn der Knoten keine Kinder hat. Das ist praktisch, wenn Sie die Struktur des Dokuments genau kennen, das Sie bearbeiten. Wenn Sie mit einem strengen Inhaltsmodell arbeiten, das von einem Schema oder einer DTD erzwungen wird, können Sie sich darauf verlassen, daß das Dokument in einer bestimmten Weise organisiert ist (vorausgesetzt, Sie verwenden einen Validierungsschritt). Aber meistens ist es das Beste, der Idee von XML zu folgen und tatsächlich das Dokument nach den Daten zu traversieren, die Sie suchen, statt anzunehmen, daß irgendeine Logik dahintersteckt, wo sich diese Daten aufhalten. Egal wie, firstChild kann sehr mächtig sein und wird oft benutzt, um das erste Element unterhalb des document-Elements zu bekommen.

Das lastChild-Attribut ist ähnlich zu firstChild, gibt jedoch den letzten Kindknoten eines beliebigen Knotens zurück. Das kann wiederum sehr praktisch sein, wenn Sie die genaue Dokumentstruktur kennen oder wenn Sie einfach nur versuchen, das letzte Kind zu bekommen, unabhängig von seiner Bedeutung.

Das childNodes-Attribut enthält eine Knotenliste mit allen Kindern eines bestimmten Knotens. Dieses Attribut wird beim Arbeiten mit DOM oft benutzt. Beim einfachen Iterieren über die Kinder eines Elements kann das childNodes-Attribut genauso benutzt werden wie bei der Iteration über eine Liste:

for child in node.childNodes:
   print "Kind:", child.nodeName

Der Wert des childNodes-Attributs ist ein NodeList-Objekt. Um Informationen aus dem DOM zu gewinnen, verhält es sich wie eine Python-Liste, unterstützt jedoch keine Teilbereichsbildung (Slicing). NodeList-Objekte sollten nicht zur Modifikation von DOM-Inhalten verwendet werden, da das jeweilige Verhalten dabei abhängig von der DOM-Implementierung sein kann.

Die NodeList-Schnittstelle bietet einige weitere Methoden/Attribute, die über die von Listen hinausgehen. Diese werden in Python kaum benutzt, sind jedoch verfügbar, weil DOM ihr Vorhandensein und Verhalten spezifiziert. Das length-Attribut gibt die Anzahl der Knoten in der Liste an. Man beachte, daß dies die Gesamtlänge zurückgibt, aber die Indexierung bei null beginnt. Zum Beispiel hat eine NodeList mit einer Länge von 3 Knoten mit den Indizes 0, 1 und 2 (was widerspiegelt, wie Arrays in Python normalerweise indexiert werden). Die meisten Python-Programmierer ziehen es vor, die eingebaute Funktion len zu benutzen, die bei NodeList -Objekten korrekt funktioniert.

Die Methode item gibt den Eintrag an dem Index zurück, der ihr als Parameter übergeben wurde. So liefert z. B. item(1) den zweiten Knoten der NodeList zurück oder None, wenn es weniger als zwei Knoten gibt. Das ist anders als bei Pythons Index-Operation, bei der NodeList einen IndexError auslöst, wenn der Index außerhalb der gültigen Grenzen liegt.

Zugreifen auf Geschwisterknoten

Da XML-Dokumente hierarchisch sind und DOM sie als Baum darstellt, ist es vernünftig, nicht nur auf die Kinder eines Knotens, sondern auch auf seine Geschwisterknoten zuzugreifen. Dies wird mit den Attributen previousSibling und nextSibling bewerkstelligt. Wenn ein Knoten das erste Kind seines Elternknotens ist, ist sein previousSibling gleich None; wenn es umgekehrt das letzte Kind ist, so ist sein nextSibling gleich None. Wenn ein Knoten das einzige Kind seines Elternknotens ist, sind, wie erwartet, beide Attribute dafür gleich None.

In Kombination mit den Attributen firstChild oder lastChild kann man mit den Geschwisterattributen über die Kinder eines Elements iterieren. Der benötigte Code ist ein wenig umfangreicher, dafür aber auch besser geeignet, um den Dokument-Baum in gewisser Weise zu bearbeiten, insbesondere wenn Knoten von dem Element, über das iteriert wird, entfernt oder dort hinzugefügt werden.

Betrachten Sie z. B., wie Directory-Elemente von einem anderen Directory-Element entfernt werden könnten, um ein Verzeichnis zu erstellen, das nur Dateien enthält. Wenn wir mit seinem childNodes-Attribut über das oberste Element iterieren und alle Directory-Kindelemente entfernen, die wir sehen können, werden einige Knoten nicht korrekt untersucht. (Das passiert deswegen, weil in Python for-Schleifen den Index in der Liste benutzen, wir aber gleichzeitig verbleibende Kinder nach links schieben, wenn wir eines entfernen, so daß dann eines übersprungen wird.) Es gibt viele Möglichkeiten, um zu verhindern, daß Elemente übersprungen werden, aber die vielleicht einfachste ist es, nextSibling zum Iterieren zu benutzen:

child = node.firstChild
while child is not None:
  next = child.nextSibling
  if (child.nodeType == node.ELEMENT_NODE
    and child.tagName == "Directory"):
   node.removeChild(child)
child = next

Extrahieren von Elementen anhand ihrer Namen

Abhängig davon, was Sie tun möchten, kann DOM Ihnen einige Vorteile gegenüber SAX bieten. Bei DOM müssen Sie keinen separaten Handler für jede Art von Ereignis schreiben oder Flags setzen, um Ereignisse zu gruppieren, wie es für SAX im Beispiel Verbesserter ArticleHandler vorgemacht wurde. Stellen Sie sich vor, Sie hätten eine lange Liste von Bestellungen in XML. Jemand hat Sie gefragt, wie man Artikelnummern – und nur diese – für einen Bericht aus dem Dokument herausziehen kann. In SAX können Sie einen Handler schreiben, der nach Elementen mit dem Namen sucht, mit dem Artikelnummern identifiziert werden (in diesem Beispiel sku), und dann ein Flag setzen, um Zeichenereignisse abzufangen, bis der Parser das Artikelnummern-Element verläßt. Bei DOM verfügen Sie mit der Methode getElementsByTagName der Dokument-Schnittstelle über einen anderen Ansatz.

Um zu zeigen, wie einfach dadurch einige Operationen werden können, betrachten wir ein simples Beispiel. Erzeugen Sie eine neue XML-Datei, wie im folgenden Beispiel gezeigt. Dieses Dokument stellt die Beispielbestellung für das nächste Skript dar:

<?xml version="1.0"?>
<purchaseOrder>
  <item>
    <name>Mushroom Lamp</name>
    <sku>229-987488</sku>
    <price>$34.99</price>
    <qty>1</qty>
  </item>
  <item>
    <name>Bass Drum</name>
    <sku>228-988347</sku>
    <price>$199.99</price>
    <qty>1</qty>
  </item>
  <item>
    <name>Toy Steam Engine</name>
    <sku>221-388833</sku>
    <price>$19.99</price>
    <qty>1</qty>
  </item>
</purchaseOrder>

Mit DOM können Sie ganz einfach eine Liste von Knoten erzeugen, die alle Knoten eines einzelnen Elementtyps im Dokument referenziert. Sie könnten z. B. alle sku-Elemente des Dokuments in eine neue Knotenliste packen. Diese Liste kann wie jedes andere NodeList-Objekt benutzt werden, mit dem Unterschied, daß die Knoten darin nicht das gleiche Elternelement haben müssen wie in dem Beispiel mit dem childNodes-Wert. Da DOM auf dem Strukturbaum des XML-Dokuments arbeitet, ist es in der Lage, mit einem einzigen Methodenaufruf eine Untermenge des Dokuments in eine separate Knotenliste zu ziehen. Im folgenden Beispiel wird die Methode getElementsByTagName dazu benutzt, eine einzige NodeList aller sku-Elemente im Dokument zu erzeugen. Unser Beispiel zeigt, daß sku-Elemente Textknoten als Kinder haben, aber wir wissen, daß ein Text-String im Dokument in DOM durch mehrere Textknoten dargestellt werden kann. Um es einfacher zu machen, mit dem Baum zu arbeiten, können Sie die Methode normalize der Node-Schnittstelle dazu benutzen, um alle benachbarten Textknoten in einen einzigen Textknoten zu konvertieren, was es leichter macht, das firstChild-Attribut der Element-Klasse zu verwenden, um verläßlich den kompletten Text der sku-Elemente zu erhalten.

Beispiel: po.py

#!/usr/bin/env python

from xml.dom.ext.reader.Sax2 import FromXmlStream
import sys

doc = FromXmlStream(sys.stdin)

for sku in doc.getElementsByTagName("sku"):
  sku.normalize( )
  print "Sku: " + sku.firstChild.data

Dieses Beispiel benötigt beträchtlich weniger Code, verglichen mit dem, was Sie brauchen, um einen SAX-Handler zu implementieren, der den gleichen Zweck hat. Die Extraktion kann unabhängig von anderen Aufgaben ablaufen, die auf dem Dokument ausgeführt werden. Wenn Sie das Programm starten, wiederum mit po.xml, erhalten Sie auf der Standardausgabe etwas Ähnliches hierzu:

Sku: 229-987488
Sku: 228-988347
Sku: 221-388833

Sie können im Beispiel Verbesserter ArticleHandler sehen, wie etwas Ähnliches mit SAX gemacht wird.

Untersuchen von NodeList-Elementen

Betrachten wir ein Programm, das viele dieser Konzepte kombiniert und die Datei article.xml aus dem vorherigen Abschnitt benutzt (Beispiel article.xml). Das folgende Beispiel zeigt eine rekursive Funktion, mit der Text aus den Elementen eines Dokuments extrahiert wird.

Beispiel: textme.py

#!/usr/bin/env python

from xml.dom.ext.reader.Sax2 import FromXmlStream
import sys       

def findTextNodes(nodeList):
   for subnode in nodeList:
     if subnode.nodeType == subnode.ELEMENT_NODE:
       print "Element-Knoten: " + subnode.tagName

       # Rufe Funktion erneut auf, um Kinder zu erhalten
       findTextNodes(subnode.childNodes)

    elif subnode.nodeType == subnode.TEXT_NODE:
       print "Text-Knoten: ",
       print subnode.data

doc = FromXmlStream(sys.stdin)
findTextNodes(doc.childNodes)

Sie können dieses Skript mit article.xml als Standardeingabe starten:

 $> python textme.py < article.xml 

Es sollte eine Ausgabe ähnlich zu dieser produzieren:

Element-Knoten: webArticle                   
Text-Knoten:

Element-Knoten: header
Text-Knoten:

Element-Knoten: body
Text-Knoten: Seattle, WA - Today an anonymous individual
                   announced that NASA has completed building a
                   Warp Drive and has parked a ship that uses
                   the drive in his back yard. This individual
                   claims that although he hasn't been contacted by
                   NASA concerning the parked space vessel, he assumes
                   that he will be launching it later this week to
                   mount an exhibition to the Andromeda Galaxy.

Text-Knoten:

In der Ausgabe können Sie sehen, wie Leerraum als eigener Textknoten behandelt wird und wie benachbarte Zeichendaten-Strings ebenfalls als Textknoten zusammengehalten werden. Die Ausgabe, die Sie sehen, mag sich von der hier dargestellten unterscheiden. Abhängig vom jeweils benutzten Parser (betrachten Sie verschiedene Versionen oder verschiedene Plattformen als verschiedene Parser, da die Interaktion zwischen Puffer und Betriebssystem von Bedeutung sein kann), können sich die genauen Grenzen der Textknoten unterscheiden, und Sie können eventuell sehen, wie benachbarte Blöcke von Zeichendaten mit mehr als nur einem Textknoten dargestellt werden.

Betrachten von Attributen

Nun, da wir gesehen haben, wie man den hierarchischen Inhalt eines XML-Dokuments mit DOM untersucht, wollen wir uns anschauen, wie man mit DOM auf die einzigen nicht-hierarchischen XML-Komponenten zugreifen kann, nämlich Attribute. Wie alle anderen Informationen in DOM werden auch Attribute als Knoten beschrieben. Attributknoten haben eine sehr besondere Beziehung zu der Baumstruktur eines XML-Dokuments. Wir werden sehen, daß die Schnittstellen, die es uns ermöglichen, damit zu arbeiten, ebenfalls verschieden sind.

Als wir vorher die Kinder von Elementen betrachteten (wie im obigen Beispiel textme.py), haben wir nur Knoten für Kindelemente und Textdaten gesehen. Deswegen können wir annehmen, daß Attribute keine Kinder des Elements sind, zu dem sie gehören. Dennoch sind sie verfügbar, wenn man einige spezielle Methoden von Element-Knoten benutzt. Es gibt ein Attribut in der Node-Schnittstelle, das nur für Elementattribute benutzt wird.

Der einfachste Weg, an den Wert eines Attributs zu kommen, ist die Verwendung der Methode getAttribute des Elementknotens. Diese Methode erwartet den Attributnamen als String und gibt einen String mit dem Wert des Attributs oder einen leeren String zurück, falls das Attribut nicht existiert. Um das Knotenobjekt zum Attribut zu erhalten, verwenden Sie statt dessen die Methode getAttributeNode; falls das Attribut nicht existiert, gibt sie None zurück. Wenn Sie die Existenz eines Attributs prüfen müssen, ohne den Knoten oder Attributwert zu benutzen, sollte die Methode hasAttribute hilfreich sein.

Eine andere Art, Attribute zu inspizieren, ist die Verwendung einer Struktur namens NamedNodeMap. Dieses Objekt funktioniert ähnlich wie ein Dictionary, und die Python-Version dieser Struktur hat vieles mit der Schnittstelle eines Dictionary gemeinsam. Zur Node-Schnittstelle gehört ein Attribut namens attributes, das nur für Elementknoten benutzt wird; bei anderen Knotentypen ist es immer auf None gesetzt. Obwohl NamedNodeMap genauso wie NodeList über die item-Methode und das length-Attribut verfügt, wird es in Python normalerweise als Mapping-Objekt benutzt, das den größten Teil der von Dictionary-Objekten zur Verfügung gestellten Schnittstellen unterstützt. Die Schlüssel sind die Attributnamen, und die Werte sind die Attributknoten.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Python & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 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 "Python & 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