Pythons standardbibliotek leveres med en solid XML-parser — ingen pip install nødvendig. xml.etree.ElementTree håndterer langt størstedelen af virkelig XML: RSS-feeds, SOAP-svar, konfigurationsfiler, Android-ressourcefiler, Maven POM'er. Du behøver kun at nå efter lxml, når du støder på XSD-skemavalidering, kompleks XPath eller virkelig store filer. Lad os gennemgå begge med virkelige eksempler.

ElementTree-grundlæggende — Parsing fra streng eller fil

Modulet xml.etree.ElementTree giver dig to indgangspunkter: fromstring() til parsing af XML-strenge og parse() til direkte læsning fra en fil. Her er et praktisk eksempel med en 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>"""

# Parser fra en streng
root = ET.fromstring(rss_xml)

# Parser fra en fil (alternativ)
# 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 og findtext — Søgning i træet

Disse tre metoder er dine primære værktøjer til at udtrække data. De accepterer alle et simpelt sti-udtryk (som en begrænset XPath) til at navigere i element-træet:

python
import xml.etree.ElementTree as ET

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

# find() — returnerer det første matchende element, eller None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — returnerer en liste over alle matchende elementer
items = channel.findall('item')
print(len(items))  # 2

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

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

# findtext() med en standardværdi (undgår AttributeError for manglende elementer)
author = channel.findtext('item/author', default='Unknown Author')
print(author)  # Unknown Author
Brug findtext() med en standardværdi. Hvis du bruger find().text og elementet ikke eksisterer, returnerer find() None og .text kaster en AttributeError. findtext('tag', default='') håndterer manglende elementer elegant — meget sikrere ved parsing af XML fra eksterne kilder.

Læsning af attributter

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() til attributter
    featured = product.get('featured', 'false')  # med standardværdi
    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})")

Håndtering af XML-navnerum

Navnerum er den del af XML-parsing, der får de fleste udviklere til at stønne. I ElementTree vises navnerums-URI'er i krøllede parenteser i tagnavne: {http://...}tagnavn. Sådan håndterer du dem på en ren måde:

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 udvider navnerums-præfikser til URI'er i krøllede parenteser
# Du kan definere et navnerumskort til renere XPath-lignende søgninger
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Brug navnerumskortet i 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

Bygning af XML programmatisk

ElementTree lader dig også konstruere XML fra bunden — nyttigt når du har brug for at bygge SOAP-anmodninger eller generere XML-output:

python
import xml.etree.ElementTree as ET

# Byg et ordredokument
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)

# Serialiser til streng
ET.indent(order, space='  ')  # Python 3.9+ — formatér på plads
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — Streaming af store XML-filer

For store XML-filer (titals MB eller mere) er det dyrt at indlæse hele dokumentet i hukommelsen med parse(). iterparse() streamer filen (ligesom den hændelsesdrevne SAX-tilgang) og udløser hændelser, når elementer opdages, hvilket lader dig behandle og kassere elementer løbende:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Behandl et stort RSS/Atom-feed uden at indlæse det hele i hukommelsen."""
    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', ''),
            })
            # Kritisk: ryd elementet efter behandling for at frigøre hukommelse
            elem.clear()

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

    yield from articles  # yield eventuelle resterende

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

Kaldet elem.clear() efter behandling af hvert element er nøglen til at holde hukommelsesforbruget konstant uanset filstørrelse. Uden det akkumulerer ElementTree alle parsede elementer i hukommelsen, og du mister fordelen ved streaming.

lxml — Når du har brug for mere kraft

Biblioteket lxml er et hurtigt C-baseret XML-bibliotek, der udvider ElementTree-API'et med fuld XPath 1.0-understøttelse, XSD-skemavalidering og XSLT-transformationer. Installér med pip install lxml:

python
from lxml import etree

# lxml bruger den samme API som ElementTree i de fleste tilfælde
root = etree.fromstring(rss_xml.encode())  # lxml har brug for bytes, ikke str

# Fuld XPath 1.0 — meget mere kraftfuld end ElementTree-delmængden
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath med prædikater — hent elementer fra en specifik kategori
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

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

Nyttige værktøjer til XML-arbejde

Når du bygger Python XML-integrationer, hjælper disse browserværktøjer med datasiden: XML Formatter til at gøre råe API-svar læsbare, XML Validator til at kontrollere velformethed, inden du kobler din parser op, og XML to JSON, når du hellere vil arbejde med dicts end elementer.

Opsummering

Pythons xml.etree.ElementTree dækker de fleste virkelige XML-scenarier: brug find() og findall() med et navnerumskort til SOAP og navnerums-feeds, findtext() med standardværdier for at undgå AttributeError for manglende elementer, og iterparse() til store filer, du ikke kan indlæse i hukommelsen på én gang. Nå efter lxml, når du har brug for XSD-validering eller det fulde XPath-udtryks-sprog. Standardbiblioteket klarer alt andet fint.