Die in der Standardbibliothek vorhandenen XML-Bibliotheken finden sich im Package xml. Diese sind:
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.
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
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
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.
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'))
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)
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))
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 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.
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")
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)
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 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')
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')
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))
In [ ]: