XML

Die Extensible Markup Language ist ein Format und ein Metasprache für (primär) hierarchische Sprachen. Da XML in einigen anderen Lehrveranstaltungen verwendet wird, gehe ich hier nicht näher darauf ein, sondern möchte nur kurz zeigen, wie man XML mit Python verarbeiten und erzeugen kann.

XML Bibliotheken in Python

Die in der Standardbibliothek vorhandenen XML-Bibliotheken finden sich im Package xml. Diese sind:

  • dom.sax - liest XML als Datenstrom ein und generiert Events um z.B. auf bestimmte Tags zu reagieren. Dieses Modul wird vor allem verwendet, um riesige XML-Dokumente zu verarbeiten, ohne dabei viel RAM zu verbrauchen.
  • xml.dom.minidom - Macht ein XML-Dokument als Document-Objekt-Model verfügbar. Das DOM ist eine abstrakte Sichtweise und API auf ein XML-Dokument, das von vielen Programmiersprachen unterstützt wird. DOM spielt z.B. beim Zugriff von JavaScript auf HTML eine große Rolle.
  • xml.dom.pulldom ist eine etwas exotische Zwischenlösung zwischen SAX und DOM, mit einem sehr überschaubaren Einsatzbereich.
  • xml.etree.ElementTree ist eine sehr "pythonische" Art, XML zu verarbeiten. Es ist von der Idee her mit DOM vergleichbar, bietet aber ein einfacheres Interface.

Zusatzbibliotheken

Hier ist vor allem lxml (http://lxml.de/) zu erwähnen. lxml ist so etwas wie der größere, klügere und stärkere Bruder von xml.etree.ElementTree . Beide sind jedoch so ähnlich, dass man, wenn man xml.etree.ElementTree verwendet hat, mit minimalen Codeänderungen auf lxml umsteigen kann. Der Hauptunterschied ist einerseits die Verarbeitungsgeschwindigkeit und die Unterstützung von XPath, die in lxml im Unterschied zu etree vollständig implementiert ist. lxml bietet darüber hinaus noch ein Reihe weiterer Möglichkeiten, etwas zum Parsen von HTML.

In der Folge werden wir auf ElementTree fokusieren, weil dies die gebräuchlichste Art ist, XML-Daten mit Python zu verarbeiten und weitgehend gleich verwendet wird, wie das mächtigere lxml.

Im Anhang werde ich noch kurze Beispiele für die anderen Möglichkeiten geben.

Die Beispieldaten

Wir verwenden hier sehr einfache Daten, die hoffentlich einfach zu verstehen sind. Dabei handelt es sich um eine kurze Rezepthandlung.

In XML sind Daten baumförmig organisiert. Ein Wurzelelement (hier: <recipes>) enthält ein oder mehrere Kindelement, die wiederum Kindelemente enthalten können. Im Beispiel kann das Wurzelement <recipes> beliebig viele <recipe>-Elemente enthalten. Jedes <recipe>-Element enthält wiederum diese Elemente: <title>, <coocingTime>, <ingredients> und <instructions> usw.

Man kann sich diese Struktur wie eine Menge ineinander steckender Behälter vorstellen:

oder auch als Baum, ähnlich etwa einer Verzeichnisstruktur:

Wichtig ist auch noch, dass Elemente Attribute haben können. Beispielsweise wird das verwendet, um jedem Rezept einen type und eine Sprache (xml:lang) zuzuweisen:

<recipe type="soup" xml:lang="de">

Die gesamte XML-Datei finden Sie hier: data/recipes.xml

Die XML-Datei einlesen

Wir werden ins bei den folgenden Beispiel mit etree begnügen. Die Beispiele sollten aber grundsätzlich auch mit lxml funktionieren.

Zunächst müssen wir das Modul importieren. Um nicht immer den langen Modulnamen eintippen zu müssen, importieren wir das Modul unter dem Namen ET:


In [ ]:
import xml.etree.ElementTree as ET

Dann lesen wir die Datei ein und weisen den so entstandenen ElementTree der Variable tree zu:


In [ ]:
tree = ET.parse('data/recipes.xml')

Um im Tree navigieren zu können, brauchen wir seine Wurzel:


In [ ]:
root = tree.getroot()

Dann können wir uns root genauer ansehen:


In [ ]:
root

Das Element-Objekt

root ist also ein Element-Objekt. Dieses hat drei wichtige Eigenschaften:

  • tag repräsentiert den Tag-Namen des Elements
  • text repräsentiert einen eventuell dem Element untergeordneten Text-Knoten
  • attrib ist ein Dictionary mit allen Attributen des XML-Elements.

In [ ]:
root.tag

In [ ]:
root.text

Unser Wurzelelement enthält also keinen richtigen Text; wir werden uns diese Eigenschaft später an einem anderen Element anschauen.


In [ ]:
root.attrib

Wie wir sehen, ist für das Element recipes kein Attribut definiert, daher ist die Eigenschaft attrib ein leeres Dicitonary.

Kindelemente

Von jedem beliebigen Element aus können wir auf seine Kindelemente zugreifen. Die getchildren()-Methode liefert eine Liste von Element-Objekten. Wenn das Element keine Kindelemente hat, liefert die Methode eine leere Liste.


In [ ]:
root.getchildren()

Wir sehen, dass das recipes-Element 6 recipe-Element enthält.

Da Element-Objekte iterable sind, können wir statt getchildren() auch einfach den in-Operator verwenden, um auf Kindelement nach Kindelement zuzugreifen:


In [ ]:
for child in root:
    print(child)

Betrachten wir nun einmal das erste Rezept genauer, indem wir ausgehend vom Root-Element eine Referenz auf das erste Kindelement setzen. Dazu können wir bequemerweise, wie bei einer Liste eine Indexzahl verwenden:


In [ ]:
recipe1 = root[0]
recipe1

recipe1 hat auch Attribute, auf die wir über die Eigenschaft attrib zugreifen können:


In [ ]:
recipe1.attrib

Um auf den Wert eines bestimmten Attributs zuzugreifen bietet Element die get-Methode:


In [ ]:
recipe1.get('type')

Greift man auf ein nicht existierendes Attribut zu, liefert get() None:


In [ ]:
print(recipe1.get('hudriwurdri'))

Auf bestimmte Elemente zugreifen

find()

Sehen wir uns einmal die Kindelemente von recipe1 an:


In [ ]:
for child in recipe1:
    print(child.tag)

Das Element hat also vier Kindelemente. Wollen wir auf ein bestimmtes Kindelement zugreifen, können wir die find(<gesuchter_tag>) Methode nutzen. Sie liefert das erste unmittelbare Kindelement mit dem angegebenen Tag-Namen:


In [ ]:
recipe1.find('title')

Wir können auch direkt auf die Text-Eigenschaft des gefundenen Elements zugreifen:


In [ ]:
recipe1.find('title').text

Damit könnten wir uns schon ein Übersicht über die vorhanden Rezepte verschaffen:


In [ ]:
for recipe in root:
    print(recipe.find('title').text)

findall()

Während find() nur das erste gesuchte Kindelement liefert, findet findall() alle direkten Kindelemente mit einem bestimmten Tag.


In [ ]:
recipe1.findall('title')

Da es pro Rezept nur ein title-Element gibt, bekommen wir eine Liste mit nur einem Eintrag. Interessanter wird es, wenn wir findall() auf das Element ingredients anwenden:


In [ ]:
recipe1.find('ingredients').findall('ingredient')

ingredients hat also 4 Kindelemente vom Typ ingredient.

Anstatt nur die Elemente auszugeben, können wir das nutzen um eine Liste aller Zutaten zu erzeugen:


In [ ]:
for ingredient in recipe1.find('ingredients').findall('ingredient'):
    print('{} {} {}'.format(
        ingredient.get('quantity', ''), 
        ingredient.get('unit', ''), 
        ingredient.text))

iter()

Die Methoden find() und findall() durchsuchen nur die direkten Kindelemente. Wenn wir sie vom Element recipe1 aus anwenden, um nach ingredient zu suchen, wird nichts gefunden, weil das Element ingredients dazwischen liegt:


In [ ]:
recipe1.findall('ingredient')

Wollen wir tiefer als eine Ebene suchen, brauchen wir die Methode iter(). Diese liefert einen Iterator, der ein Element nach dem anderen liefert:


In [ ]:
for ingredient in recipe1.iter('ingredient'):
    print(ingredient.text)

Damit können wir auch alle Zutaten für alle Rezepte sehr einfach ausgeben, weil iter() alle Elemente beliebig tief in der Struktur findet:


In [ ]:
for ingredient in root.iter('ingredient'):
    print(ingredient.text)

XPath

XPath ist eine Sprache, die es erlaubt, Zugriffspfade auf ein oder mehrere XML-Elemente zu definieren. ElementTree unterstützt XPath, allerdings unvollständig (mehr dazu hier: https://docs.python.org/3/library/xml.etree.elementtree.html#elementtree-xpath). Daher noch einmal der Verweis auf lxml für komplexere XML-Projekte.

Das letzte Beispiel ließe sich unter Verwendung von XPath auch so schreiben:


In [ ]:
for ingredient in root.findall('./recipe/ingredients/ingredient'):
    print(ingredient.text)

oder kürzer (aber langsamer) so:


In [ ]:
for ingredient in root.findall('.//ingredient'):
    print(ingredient.text)

Sind wird nur an den Zudaten für Desserts interessiert, können wir nach dem Wert des Attributs type im Element recipe filtern:


In [ ]:
for ingredient in root.findall('./recipe[@type="dessert"]/ingredients/ingredient'):
    print(ingredient.text)

Selbstverständlich können wir von jedem gefundenen Element aus weiter durch den Pfad navigieren. Hier suchen wir zunächst via XPath alle Rezepte des Typs dessert. Von jedem gefundenen Rezept-Element aus verwenden wir einene weiteren XPath-Ausdruck, um den Title des Rezepts auszugeben und einen zweiten Xpath, um zu den Zudaten des Rezepts zu navigieren.


In [ ]:
for recipe in root.findall('./recipe[@type="dessert"]'):
    print(recipe.find('./title').text)
    for ingredient in recipe.findall('./ingredients/ingredient'):
        print('\t{}'.format(ingredient.text))

lxml unterstützt neben XPath noch weitere Methoden wie getprevious() getnext() oder getparent() um durch den Baum zu navigieren.

Daten verändern

Attribute und Text verändern

ElementTree unterstützt natürlich auch die Veränderung von Daten. Wir können beispielsweise die Bezeichnung 'grams' auf 'Gramm' ändern. Suchen wir zunächst via XPath nach allen ingredient-Elementen, in denen dieser Wert vorkommen:


In [ ]:
for ingredient in root.findall('./recipe/ingredients/ingredient[@unit="grams"]'):
    print(ingredient.attrib)

In [ ]:
for ingredient in root.findall('./recipe/ingredients/ingredient[@unit="grams"]'):
    ingredient.set('unit', 'Gramm')

Zur Kontrolle können wir uns das ganze XML-Dokument ausgeben lassen:


In [ ]:
ET.tostring(root, encoding='utf-8')

oder in eine Datei schreiben lassen:


In [ ]:
tree.write('rezepte_de.xml', encoding="utf-8")

Genauso wie die Attribute eines Elements, können wir die Text-Eigenschaft verändern:


In [ ]:
for ingredient in root.findall('./recipe/ingredients/ingredient'):
    ingredient.text = ingredient.text.replace('Erdäpfel', 'Kartoffel')
tree.write('rezepte_de.xml', encoding="utf-8")

Löschen und Hinzufügen neuer Element


In [ ]:
new_recipe = ET.SubElement(root, 'recipe')
new_recipe.set('type', 'mainDish')
new_recipe.set('xml:lang', 'de')
title = ET.SubElement(new_recipe, 'title')
title.text = 'Ravioli aus der Dose'
ingredients = ET.SubElement(new_recipe, 'ingredients')
ingredient = ET.SubElement(ingredients, 'ingredient')
ingredient.set('quantity', '1')
ingredient.set('unit', 'pieces')
ingredient.text = 'Dose Ravioli'

In [211]:
ET.dump(new_recipe)


<recipe type="mainDish" xml:lang="de"><title>Ravioli aus der Dose</title><ingredients><ingredient quantity="1" unit="pieces">Dose Ravioli</ingredient></ingredients></recipe>

Sax und Minidom

Wie bereits beschrieben, gibt es weitere Möglichkeiten, mit Python XML zu verarbeiten. Wir sehen uns hier in aller Kürze zwei davon an.

Sax

Sax ist eventbasiert und, weil nie das gesamte Dokument im Speicher gehalten werden muss, speicherschonend. Man erzeugt einen Parser und weist diesem einen selbstgeschriebenen ContentHandler zu. Dann übergibt man dem Parser das zu parsende Dokument:


In [216]:
import xml.sax as sax
parser = sax.make_parser()
parser.setContentHandler(RecipeHandler())
parser.parse('data/recipes.xml')


Miso-Suppe
Jogurtsuppe
Brathuhn mit Zitrone
Erdäpfel mit Tomaten
Feigen mit Mandeln
Mittelalterlicher Birnenpudding (Birnenmus)

Dieser Code funktioniert noch nicht, weil wir zuerst den ContentHandler schreiben müssen:


In [218]:
class RecipeHandler(sax.handler.ContentHandler):
    
    def __init__(self):
        self.in_title = False # set to True if we are inside a <title> Tag
        self.in_ingredient = False
        self.content = ''
        
    def startElement(self, name, attrs):
        "This method is called for each opening tag."
        if name == 'title':
            self.in_title = True
        if name == 'ingredient':
            self.in_ingredient = True

    def characters(self, content):
        "Content within tag markers"
        if self.in_title or self.in_ingredient:
            self.content = content
            
    def endElement(self, name):
        if name == 'title':
            self.in_title = False
            print(self.content)
        elif name == 'ingredient':
            self.in_ingredient = False
            print("\t{}".format(self.content))

Nun funktioniert auch der bereits geschriebene Code:


In [219]:
parser = sax.make_parser()
parser.setContentHandler(RecipeHandler())
parser.parse('data/recipes.xml')


Miso-Suppe
	Tofu
	Frühlingszwiebel
	Dashi
	Miso-Paste
Jogurtsuppe
	Jogurt
	heißes Wasser
	Zwiebel, fein gehackt
	Mehl
	gehackte Walnüsse
	Öl oder Butter
	Salz
	Pfeffer
Brathuhn mit Zitrone
	Huhn
	Unbehandelte Zitronen
	Salz
	Pfeffer
Erdäpfel mit Tomaten
	Kleine Erdäpfel
	Cocktailtomaten
	Salz
	Sumach (gemahlen)
	Honig
	Rosmarin (fein gehackt)
	Petersilie (gehackt)
	Olivenöl
Feigen mit Mandeln
	Feigen
	Granatapfelsaft
	Rohrzucker
	Schale einer unbehandelten Orange
	Mandeln (blanchiert)
	Jogurt
	Honig
Mittelalterlicher Birnenpudding (Birnenmus)
	Birnen
	trockener Weißwein
	Butter
	Zucker
	Obers
	Eigelb
	Zimt

MiniDOM

Mit MiniDOM bietet Python eine Sichtweise auf XML-Daten die weitgehend konform mit dem vom World Wide Web Consortium definierten DOM (Document Object Model) ist, das weite Verbreitung hat und von vielen Programmiersprachen unterstützt wird. DOM ist im Vergleich zu ElementTree relativ umständlich und vor allem im Vergleich zu lxml (bei großen Dokumenten) sehr langsam. Sehen wir uns ein kleines Beispiel an:


In [242]:
import xml.dom.minidom as minidom

tree = minidom.parse('data/recipes.xml')
for recipe in tree.getElementsByTagName('recipe'):
    title = recipe.getElementsByTagName('title')[0]
    print(title.firstChild.data)
    for ingredient in recipe.getElementsByTagName('ingredient'):
        print("\t{}".format(ingredient.firstChild.data))


Miso-Suppe
	Tofu
	Frühlingszwiebel
	Dashi
	Miso-Paste
Jogurtsuppe
	Jogurt
	heißes Wasser
	Zwiebel, fein gehackt
	Mehl
	gehackte Walnüsse
	Öl oder Butter
	Salz
	Pfeffer
Brathuhn mit Zitrone
	Huhn
	Unbehandelte Zitronen
	Salz
	Pfeffer
Erdäpfel mit Tomaten
	Kleine Erdäpfel
	Cocktailtomaten
	Salz
	Sumach (gemahlen)
	Honig
	Rosmarin (fein gehackt)
	Petersilie (gehackt)
	Olivenöl
Feigen mit Mandeln
	Feigen
	Granatapfelsaft
	Rohrzucker
	Schale einer unbehandelten Orange
	Mandeln (blanchiert)
	Jogurt
	Honig
Mittelalterlicher Birnenpudding (Birnenmus)
	Birnen
	trockener Weißwein
	Butter
	Zucker
	Obers
	Eigelb
	Zimt

In [ ]: