Biblioteka standardowa Pythona zawiera solidny parser XML — bez pip install. xml.etree.ElementTree obsługuje zdecydowaną większość XML-a spotykanegow praktyce: kanały RSS, odpowiedzi SOAP, pliki konfiguracyjne, pliki zasobów Android, pliki Maven POM. Po lxml sięgasz dopiero gdy potrzebujesz walidacji schematu XSD, złożonego XPath lub naprawdę dużych plików. Przejdźmy przez oba, na realnych przykładach.

Podstawy ElementTree — parsowanie z łańcucha lub pliku

Moduł xml.etree.ElementTree daje Ci dwa punkty wejścia: fromstring() do parsowania łańcuchów XML oraz parse() do odczytu bezpośrednio z pliku. Oto praktyczny przykład z użyciem struktury kanału RSS:

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

# Parsowanie z łańcucha
root = ET.fromstring(rss_xml)

# Parsowanie z pliku (alternatywa)
# 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 i findtext — przeszukiwanie drzewa

Te trzy metody to Twoje główne narzędzia do wyodrębniania danych. Wszystkie przyjmują proste wyrażenie ścieżki (podobne do ograniczonego XPath) do nawigacji po drzewie elementów:

python
import xml.etree.ElementTree as ET

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

# find() — zwraca pierwszy pasujący element lub None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — zwraca listę wszystkich pasujących elementów
items = channel.findall('item')
print(len(items))  # 2

for item in items:
    title = item.findtext('title')      # findtext() zwraca bezpośrednio .text
    link = item.findtext('link')
    pub_date = item.findtext('pubDate')
    print(f"{title} — {pub_date}")

# Zagnieżdżona ścieżka z '/'
all_titles = channel.findall('item/title')
print([el.text for el in all_titles])
# ['Understanding Database Indexes', 'REST API Design Patterns']

# findtext() z domyślną wartością (unika AttributeError przy brakujących elementach)
author = channel.findtext('item/author', default='Unknown Author')
print(author)  # Unknown Author
Używaj findtext() z wartością domyślną. Jeśli używasz find().text i element nie istnieje, find() zwraca None, a .text zgłasza AttributeError. findtext('tag', default='') obsługuje brakujące elementy w sposób bezpieczny — znacznie lepsze rozwiązanie przy parsowaniu XML z zewnętrznych źródeł.

Odczyt atrybutów

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() do atrybutów
    featured = product.get('featured', 'false')  # z wartością domyślną
    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})")

Obsługa przestrzeni nazw XML

Przestrzenie nazw to ta część parsowania XML, która sprawia, że większość programistów wzdycha. W ElementTree URI przestrzeni nazw pojawiają się w nawiasach klamrowych w nazwach tagów: {http://...}nazwaElementu. Oto jak sobie z tym poradzić w czysty sposób:

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 rozszerza prefiksy przestrzeni nazw do URI w nawiasach klamrowych
# Możesz zdefiniować mapę przestrzeni nazw dla czystszych wyszukiwań w stylu XPath
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Użyj mapy przestrzeni nazw w 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

Programowe budowanie XML

ElementTree pozwala też konstruować XML od zera — przydatne przy budowaniu żądań SOAP lub generowaniu wyjścia XML:

python
import xml.etree.ElementTree as ET

# Zbuduj dokument zamówienia
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)

# Serializacja do łańcucha
ET.indent(order, space='  ')  # Python 3.9+ — formatowanie w miejscu
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — strumieniowe przetwarzanie dużych plików XML

Dla dużych plików XML (dziesiątki MB lub więcej) wczytywanie całego dokumentu do pamięci za pomocą parse() jest kosztowne. iterparse() strumieniuje plik (podobnie do podejścia zdarzeniowego SAX) i uruchamia zdarzenia podczas napotkania elementów, pozwalając przetwarzać i odrzucać elementy na bieżąco:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Przetwarzaj duży kanał RSS/Atom bez wczytywania całości do pamięci."""
    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', ''),
            })
            # Kluczowe: wyczyść element po przetworzeniu, aby zwolnić pamięć
            elem.clear()

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

    yield from articles  # zwróć pozostałe

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

Wywołanie elem.clear() po przetworzeniu każdego elementu to klucz do utrzymania stałego zużycia pamięci niezależnie od rozmiaru pliku. Bez tego ElementTree gromadzi wszystkie przetworzone elementy w pamięci i tracisz korzyści ze strumieniowania.

lxml — gdy potrzebujesz więcej możliwości

Biblioteka lxml to szybka, oparta na C biblioteka XML, która rozszerza API ElementTree o pełną obsługę XPath 1.0, walidację schematu XSD i transformacje XSLT. Zainstaluj za pomocą pip install lxml:

python
from lxml import etree

# lxml używa tego samego API co ElementTree w większości przypadków
root = etree.fromstring(rss_xml.encode())  # lxml potrzebuje bajtów, nie str

# Pełny XPath 1.0 — znacznie potężniejszy niż podzbiór ElementTree
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath z predykatami — pobierz elementy z określonej kategorii
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

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

Przydatne narzędzia do pracy z XML

Przy budowaniu integracji XML w Pythonie te narzędzia pomagają po stronie danych: XML Formatter do czytelnego formatowania surowych odpowiedzi API, XML Validator do sprawdzania poprawności składniowej przed konfigurowaniem parsera, oraz XML to JSON, gdy wolisz pracować ze słownikami zamiast z elementami.

Podsumowanie

xml.etree.ElementTree Pythona obsługuje większość rzeczywistych scenariuszy XML: używaj find() i findall() z mapą przestrzeni nazw dla SOAP i namespaced kanałów, findtext() z wartościami domyślnymi, by uniknąć AttributeError przy brakujących elementach, oraz iterparse() dla dużych plików, których nie możesz wczytać naraz do pamięci. Po lxml sięgaj, gdy potrzebujesz walidacji XSD lub pełnego języka wyrażeń XPath. Biblioteka standardowa z resztą radzi sobie doskonale.