import xml.etree.ElementTree as ET

"""
Explanation: 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: &lt;recipes&gt;) enthält ein oder mehrere Kindelement, die wiederum Kindelemente enthalten können. Im Beispiel kann das Wurzelement &lt;recipes&gt; beliebig viele &lt;recipe&gt;-Elemente enthalten. Jedes &lt;recipe&gt;-Element enthält wiederum diese Elemente: &lt;title&gt;, &lt;coocingTime&gt;, &lt;ingredients&gt; und &lt;instructions&gt; usw. 
Man kann sich diese Struktur wie eine Menge ineinander steckender Behälter vorstellen:
<img src="img/recipes_box.png" style="height: 200px;"/>
oder auch als Baum, ähnlich etwa einer Verzeichnisstruktur:
<img src="img/recipes_tree.png" style="width: 500px;"/>
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:
End of explanation
"""


tree = ET.parse('data/recipes.xml')

"""
Explanation: Dann lesen wir die Datei ein und weisen den so entstandenen ElementTree der Variable tree zu:
End of explanation
"""


root = tree.getroot()

"""
Explanation: Um im Tree navigieren zu können, brauchen wir seine Wurzel:
End of explanation
"""


root

"""
Explanation: Dann können wir uns root genauer ansehen:
End of explanation
"""


root.tag

root.text

"""
Explanation: 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.
End of explanation
"""


root.attrib

"""
Explanation: Unser Wurzelelement enthält also keinen richtigen Text; wir werden uns diese Eigenschaft später an einem anderen Element anschauen.
End of explanation
"""


root.getchildren()

"""
Explanation: 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.
End of explanation
"""


for child in root:
    print(child)

"""
Explanation: 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:
End of explanation
"""


recipe1 = root[0]
recipe1

"""
Explanation: 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:
End of explanation
"""


recipe1.attrib

"""
Explanation: recipe1 hat auch Attribute, auf die wir über die Eigenschaft attrib zugreifen können:
End of explanation
"""


recipe1.get('type')

"""
Explanation: Um auf den Wert eines bestimmten Attributs zuzugreifen bietet Element die get-Methode:
End of explanation
"""


print(recipe1.get('hudriwurdri'))

"""
Explanation: Greift man auf ein nicht existierendes Attribut zu, liefert get() None:
End of explanation
"""


for child in recipe1:
    print(child.tag)

"""
Explanation: Auf bestimmte Elemente zugreifen
find()
Sehen wir uns einmal die Kindelemente von recipe1 an:
End of explanation
"""


recipe1.find('title')

"""
Explanation: Das Element hat also vier Kindelemente. Wollen wir auf ein bestimmtes Kindelement zugreifen, können wir die find(&lt;gesuchter_tag&gt;) Methode nutzen. Sie liefert das erste unmittelbare Kindelement mit dem angegebenen Tag-Namen:
End of explanation
"""


recipe1.find('title').text

"""
Explanation: Wir können auch direkt auf die Text-Eigenschaft des gefundenen Elements zugreifen:
End of explanation
"""


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

"""
Explanation: Damit könnten wir uns schon ein Übersicht über die vorhanden Rezepte verschaffen:
End of explanation
"""


recipe1.findall('title')

"""
Explanation: findall()
Während find() nur das erste gesuchte Kindelement liefert, findet findall() alle direkten Kindelemente mit einem bestimmten Tag.
End of explanation
"""


recipe1.find('ingredients').findall('ingredient')

"""
Explanation: 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:
End of explanation
"""


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

"""
Explanation: 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:
End of explanation
"""


recipe1.findall('ingredient')

"""
Explanation: 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:
End of explanation
"""


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

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


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

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


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

"""
Explanation: 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:
End of explanation
"""


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

"""
Explanation: oder kürzer (aber langsamer) so:
End of explanation
"""


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

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


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))

"""
Explanation: 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.
End of explanation
"""


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

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

"""
Explanation: 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:
End of explanation
"""


ET.tostring(root, encoding='utf-8')

"""
Explanation: Zur Kontrolle können wir uns das ganze XML-Dokument ausgeben lassen:
End of explanation
"""


tree.write('rezepte_de.xml', encoding="utf-8")

"""
Explanation: oder in eine Datei schreiben lassen:
End of explanation
"""


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

"""
Explanation: Genauso wie die Attribute eines Elements, können wir die Text-Eigenschaft verändern:
End of explanation
"""


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'

ET.dump(new_recipe)

"""
Explanation: Löschen und Hinzufügen neuer Element
End of explanation
"""


import xml.sax as sax
parser = sax.make_parser()
parser.setContentHandler(RecipeHandler())
parser.parse('data/recipes.xml')

"""
Explanation: 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:
End of explanation
"""


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))
    

"""
Explanation: Dieser Code funktioniert noch nicht, weil wir zuerst den ContentHandler schreiben müssen:
End of explanation
"""


parser = sax.make_parser()
parser.setContentHandler(RecipeHandler())
parser.parse('data/recipes.xml')

"""
Explanation: Nun funktioniert auch der bereits geschriebene Code:
End of explanation
"""


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))

"""
Explanation: 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:
End of explanation
"""


