De standaardbibliotheek van Python bevat een solide XML-parser — geen pip install nodig. xml.etree.ElementTree verwerkt de grote meerderheid van reëel XML: RSS-feeds, SOAP-responses, configuratiebestanden, Android-resourcebestanden, Maven POM's. Je hoeft pas te grijpen naar lxml wanneer je XSD-schemavalidatie, complexe XPath of echt enorme bestanden nodig hebt. Laten we beide behandelen, met echte voorbeelden.

ElementTree Basis — Parsen vanuit een String of Bestand

De module xml.etree.ElementTree geeft je twee toegangspunten: fromstring() voor het parsen van XML-strings, en parse() voor het direct lezen vanuit een bestand. Hier is een praktijkvoorbeeld met een RSS-feedstructuur:

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

# Parseer vanuit een string
root = ET.fromstring(rss_xml)

# Parseer vanuit een bestand (alternatief)
# 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 en findtext — Zoeken in de Boom

Deze drie methoden zijn je primaire gereedschappen voor het extraheren van data. Ze accepteren allemaal een eenvoudige padexpressie (zoals een beperkte XPath) om door de elementenboom te navigeren:

python
import xml.etree.ElementTree as ET

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

# find() — retourneert het eerste overeenkomende element, of None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — retourneert een lijst van alle overeenkomende elementen
items = channel.findall('item')
print(len(items))  # 2

for item in items:
    title = item.findtext('title')      # findtext() retourneert .text direct
    link = item.findtext('link')
    pub_date = item.findtext('pubDate')
    print(f"{title} — {pub_date}")

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

# findtext() met een standaardwaarde (vermijdt AttributeError bij ontbrekende elementen)
author = channel.findtext('item/author', default='Unknown Author')
print(author)  # Unknown Author
Gebruik findtext() met een standaardwaarde. Als je find().text gebruikt en het element niet bestaat, retourneert find() None en gooit .text een AttributeError. findtext('tag', default='') verwerkt ontbrekende elementen soepel — veel veiliger bij het parsen van XML van externe bronnen.

Attributen Lezen

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() voor attributen
    featured = product.get('featured', 'false')  # met standaardwaarde
    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 Verwerken

Namespaces zijn het deel van XML-parsen waar de meeste ontwikkelaars over klagen. In ElementTree verschijnen namespace-URI's tussen accolades in tagnamen: {http://...}tagname. Zo ga je er netjes mee om:

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 breidt namespace-prefixen uit naar URI's tussen accolades
# Je kunt een namespace-map definiëren voor overzichtelijkere XPath-zoekopdrachten
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Gebruik de namespace-map in find/findall
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 Opbouwen

ElementTree stelt je ook in staat XML van scratch op te bouwen — handig wanneer je SOAP-verzoeken moet opbouwen of XML-uitvoer moet genereren:

python
import xml.etree.ElementTree as ET

# Bouw een besteldocument
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)

# Serialiseer naar string
ET.indent(order, space='  ')  # Python 3.9+ — mooi opmaken in-place
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — Grote XML-bestanden Streamen

Voor grote XML-bestanden (tientallen MB of meer) is het laden van het hele document in geheugen met parse() duur. iterparse() streamt het bestand (vergelijkbaar in geest met de event-gestuurde SAX-aanpak) en vuurt events af wanneer elementen worden gevonden, zodat je elementen kunt verwerken en weggooien terwijl je verder gaat:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Verwerk een grote RSS/Atom-feed zonder alles in geheugen te 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', ''),
            })
            # Cruciaal: wis het element na verwerking om geheugen vrij te maken
            elem.clear()

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

    yield from articles  # geef eventuele resterende items terug

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

De aanroep elem.clear() na het verwerken van elk element is de sleutel tot het vlak houden van geheugengebruik ongeacht de bestandsgrootte. Zonder dit accumuleert ElementTree alle geparseerde elementen in geheugen en verlies je het voordeel van streamen.

lxml — Wanneer Je Meer Kracht Nodig Hebt

De lxml-bibliotheek is een snelle C-gebaseerde XML-bibliotheek die de API van ElementTree uitbreidt met volledige XPath 1.0-ondersteuning, XSD-schemavalidatie en XSLT-transformaties. Installeer het met pip install lxml:

python
from lxml import etree

# lxml gebruikt in de meeste gevallen dezelfde API als ElementTree
root = etree.fromstring(rss_xml.encode())  # lxml heeft bytes nodig, geen str

# Volledige XPath 1.0 — veel krachtiger dan de subset van ElementTree
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath met predicaten — haal items op uit een specifieke categorie
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

# XSD-schemavalidatie
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}")

Handige Tools voor XML-werk

Bij het bouwen van Python XML-integraties helpen deze browsertools met de datakant: XML Formatter om ruwe API-responses leesbaar te maken, XML Validator om goed gevormd zijn te controleren vóór het instellen van je parser, en XML naar JSON wanneer je liever met dicts werkt dan met elementen.

Samenvatting

Python's xml.etree.ElementTree dekt de meeste reële XML-scenario's: gebruik find() en findall() met een namespace-map voor SOAP en naamruimte-feeds, findtext() met standaardwaarden om AttributeError bij ontbrekende elementen te vermijden, en iterparse() voor grote bestanden die je niet in één keer in geheugen kunt laden. Grijp naar lxml wanneer je XSD-validatie of de volledige XPath-expressietaal nodig hebt. De standaardbibliotheek regelt de rest prima.