Pythons Standardbibliothek enthält einen soliden XML-Parser — kein pip install erforderlich. xml.etree.ElementTree bewältigt die große Mehrheit des realen XML: RSS-Feeds, SOAP-Antworten, Konfigurationsdateien, Android-Ressourcendateien, Maven-POMs. Auf lxml müssen Sie nur zurückgreifen, wenn Sie XSD-Schema-Validierung, komplexes XPath oder wirklich riesige Dateien benötigen. Schauen wir uns beide mit echten Beispielen an.

ElementTree-Grundlagen — Parsen aus einem String oder einer Datei

Das xml.etree.ElementTree-Modul bietet zwei Einstiegspunkte: fromstring() zum Parsen von XML-Strings und parse() zum direkten Lesen aus einer Datei. Hier ist ein praktisches Beispiel mit einer RSS-Feed-Struktur:

python
import xml.etree.ElementTree as ET

rss_xml = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Engineering Blog</title>
    <link>https://blog.example.com</link>
    <description>Articles for developers</description>
    <item>
      <title>Understanding Database Indexes</title>
      <link>https://blog.example.com/db-indexes</link>
      <pubDate>Mon, 15 Jan 2024 09:00:00 GMT</pubDate>
      <category>Database</category>
    </item>
    <item>
      <title>REST API Design Patterns</title>
      <link>https://blog.example.com/rest-patterns</link>
      <pubDate>Wed, 17 Jan 2024 09:00:00 GMT</pubDate>
      <category>API</category>
    </item>
  </channel>
</rss>"""

# Aus einem String parsen
root = ET.fromstring(rss_xml)

# Aus einer Datei parsen (Alternative)
# tree = ET.parse('feed.xml')
# root = tree.getroot()

print(root.tag)           # rss
print(root.attrib)        # {'version': '2.0'}

channel = root.find('channel')
print(channel.find('title').text)  # Engineering Blog

find, findall und findtext — Den Baum durchsuchen

Diese drei Methoden sind Ihre wichtigsten Werkzeuge zur Datenextraktion. Alle akzeptieren einen einfachen Pfadausdruck (ähnlich einem eingeschränkten XPath) zur Navigation im Elementbaum:

python
import xml.etree.ElementTree as ET

root = ET.fromstring(rss_xml)
channel = root.find('channel')

# find() — gibt das erste passende Element zurück, oder None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — gibt eine Liste aller passenden Elemente zurück
items = channel.findall('item')
print(len(items))  # 2

for item in items:
    title = item.findtext('title')      # findtext() gibt .text direkt zurück
    link = item.findtext('link')
    pub_date = item.findtext('pubDate')
    print(f"{title} — {pub_date}")

# Verschachtelter Pfad mit '/'
all_titles = channel.findall('item/title')
print([el.text for el in all_titles])
# ['Understanding Database Indexes', 'REST API Design Patterns']

# findtext() mit Standardwert (vermeidet AttributeError bei fehlenden Elementen)
author = channel.findtext('item/author', default='Unknown Author')
print(author)  # Unknown Author
Verwenden Sie findtext() mit einem Standardwert. Wenn Sie find().text verwenden und das Element nicht existiert, gibt find() None zurück und .text wirft einen AttributeError. findtext('tag', default='') behandelt fehlende Elemente problemlos — viel sicherer beim Parsen von XML aus externen Quellen.

Attribute lesen

python
import xml.etree.ElementTree as ET

xml_str = """<catalog>
  <product id="P001" featured="true">
    <name>Mechanical Keyboard</name>
    <price currency="USD">189.00</price>
  </product>
  <product id="P002" featured="false">
    <name>USB-C Hub</name>
    <price currency="USD">49.99</price>
  </product>
</catalog>"""

root = ET.fromstring(xml_str)

for product in root.findall('product'):
    product_id = product.get('id')           # get() für Attribute
    featured = product.get('featured', 'false')  # mit Standardwert
    name = product.findtext('name')
    price_el = product.find('price')
    price = float(price_el.text)
    currency = price_el.get('currency')

    print(f"{product_id}: {name} — {currency} {price} (featured: {featured})")

XML-Namespaces verarbeiten

Namespaces sind der Teil des XML-Parsens, der die meisten Entwickler stöhnen lässt. In ElementTree erscheinen Namespace-URIs in geschweiften Klammern in Tag-Namen: {http://...}tagname. So gehen Sie damit sauber um:

python
import xml.etree.ElementTree as ET

soap_xml = """<?xml version="1.0"?>
<soap:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:inv="http://www.example.com/invoice">
  <soap:Body>
    <inv:GetInvoiceResponse>
      <inv:InvoiceId>INV-2024-0042</inv:InvoiceId>
      <inv:Amount currency="EUR">1250.00</inv:Amount>
      <inv:Status>Paid</inv:Status>
    </inv:GetInvoiceResponse>
  </soap:Body>
</soap:Envelope>"""

root = ET.fromstring(soap_xml)

# ElementTree erweitert Namespace-Präfixe auf URIs in geschweiften Klammern
# Sie können eine Namespace-Map für sauberere XPath-ähnliche Suchen definieren
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Die Namespace-Map in find/findall verwenden
invoice_id = root.find('.//inv:InvoiceId', ns).text
amount_el = root.find('.//inv:Amount', ns)
status = root.findtext('.//inv:Status', namespaces=ns)

print(invoice_id)                    # INV-2024-0042
print(amount_el.text)                # 1250.00
print(amount_el.get('currency'))     # EUR
print(status)                        # Paid

XML programmatisch erstellen

ElementTree ermöglicht auch das Erstellen von XML von Grund auf — nützlich, wenn Sie SOAP-Anfragen erstellen oder XML-Ausgaben generieren müssen:

python
import xml.etree.ElementTree as ET

# Ein Bestelldokument erstellen
order = ET.Element('order', id='ORD-9981', status='pending')

customer = ET.SubElement(order, 'customer')
ET.SubElement(customer, 'name').text = 'Jane Smith'
ET.SubElement(customer, 'email').text = '[email protected]'

items = ET.SubElement(order, 'items')
for product_id, name, qty, price in [
    ('P001', 'Mechanical Keyboard', 1, 189.00),
    ('P002', 'USB-C Hub', 2, 49.99),
]:
    item = ET.SubElement(items, 'item', productId=product_id, qty=str(qty))
    ET.SubElement(item, 'name').text = name
    ET.SubElement(item, 'price', currency='USD').text = str(price)

# Als String serialisieren
ET.indent(order, space='  ')  # Python 3.9+ — formatiert in-place
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — Große XML-Dateien streamen

Bei großen XML-Dateien (mehrere dutzend MB oder mehr) ist das Laden des gesamten Dokuments in den Speicher mit parse() aufwendig. iterparse() streamt die Datei (ähnlich dem ereignisgesteuerten SAX-Ansatz) und feuert Events, wenn Elemente gefunden werden, sodass Sie Elemente während der Verarbeitung verwerfen können:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Verarbeitet einen großen RSS/Atom-Feed ohne alles in den Speicher zu laden."""
    articles = []

    for event, elem in ET.iterparse(filepath, events=('end',)):
        if elem.tag == 'item':
            articles.append({
                'title': elem.findtext('title', ''),
                'link': elem.findtext('link', ''),
                'pub_date': elem.findtext('pubDate', ''),
            })
            # Wichtig: Element nach der Verarbeitung löschen, um Speicher freizugeben
            elem.clear()

        if len(articles) >= 1000:
            yield from articles
            articles.clear()

    yield from articles  # verbleibende Einträge zurückgeben

for article in process_large_feed('large_feed.xml'):
    print(article['title'])

Der elem.clear()-Aufruf nach der Verarbeitung jedes Elements ist der Schlüssel, um die Speichernutzung unabhängig von der Dateigröße konstant zu halten. Ohne ihn sammelt ElementTree alle geparsten Elemente im Speicher an, und Sie verlieren den Vorteil des Streamings.

lxml — Wenn Sie mehr Leistung brauchen

Die lxml-Bibliothek ist eine schnelle C-basierte XML-Bibliothek, die die ElementTree-API mit vollständiger XPath-1.0-Unterstützung, XSD-Schema-Validierung und XSLT-Transformationen erweitert. Installieren Sie sie mit pip install lxml:

python
from lxml import etree

# lxml verwendet in den meisten Fällen die gleiche API wie ElementTree
root = etree.fromstring(rss_xml.encode())  # lxml benötigt bytes, nicht str

# Vollständiges XPath 1.0 — viel mächtiger als ElementTrees Teilmenge
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath mit Prädikaten — Elemente einer bestimmten Kategorie abrufen
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

# XSD-Schema-Validierung
xsd_doc = etree.parse('schema.xsd')
schema = etree.XMLSchema(xsd_doc)
xml_doc = etree.parse('data.xml')

if schema.validate(xml_doc):
    print("Valid!")
else:
    for error in schema.error_log:
        print(f"Line {error.line}: {error.message}")

Nützliche Tools für die XML-Arbeit

Beim Aufbau von Python-XML-Integrationen helfen diese Browser-Tools auf der Datenseite: XML-Formatierer zum Lesbar-Machen roher API-Antworten, XML-Validator zur Wohlgeformtheitsprüfung vor der Parser-Anbindung, und XML zu JSON, wenn Sie lieber mit Dicts als mit Elementen arbeiten.

Fazit

Pythons xml.etree.ElementTree deckt die meisten realen XML-Szenarien ab: Verwenden Sie find() und findall() mit einer Namespace-Map für SOAP und namespace-behaftete Feeds, findtext() mit Standardwerten, um AttributeError bei fehlenden Elementen zu vermeiden, und iterparse() für große Dateien, die Sie nicht auf einmal in den Speicher laden können. Greifen Sie auf lxml zurück, wenn Sie XSD-Validierung oder die vollständige XPath-Ausdruckssprache benötigen. Die Standardbibliothek reicht für alles andere vollkommen aus.