La biblioteca estándar de Python viene con un sólido parser XML — sin pip install requerido. xml.etree.ElementTree maneja la gran mayoría de XML del mundo real: feeds RSS, respuestas SOAP, archivos de configuración, archivos de recursos Android, POMs de Maven. Solo necesitas recurrir a lxml cuando llegas a la validación de esquemas XSD, XPath complejo, o archivos verdaderamente masivos. Repasemos ambos, con ejemplos reales.

Fundamentos de ElementTree — Analizar desde una cadena o archivo

El módulo xml.etree.ElementTree te da dos puntos de entrada: fromstring() para analizar cadenas XML, y parse() para leer directamente desde un archivo. Aquí hay un ejemplo práctico usando una estructura de feed 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>"""

# Analizar desde una cadena
root = ET.fromstring(rss_xml)

# Analizar desde un archivo (alternativa)
# 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 y findtext — Buscar en el árbol

Estos tres métodos son tus herramientas principales para extraer datos. Todos aceptan una expresión de ruta simple (como un XPath limitado) para navegar por el árbol de elementos:

python
import xml.etree.ElementTree as ET

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

# find() — devuelve el primer elemento coincidente, o None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — devuelve una lista de todos los elementos coincidentes
items = channel.findall('item')
print(len(items))  # 2

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

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

# findtext() con un valor por defecto (evita AttributeError en elementos faltantes)
author = channel.findtext('item/author', default='Autor desconocido')
print(author)  # Autor desconocido
Usa findtext() con un valor por defecto. Si usas find().text y el elemento no existe, find() devuelve None y .text lanza un AttributeError. findtext('tag', default='') maneja los elementos faltantes con elegancia — mucho más seguro al analizar XML de fuentes externas.

Leer atributos

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() para atributos
    featured = product.get('featured', 'false')  # con valor por defecto
    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} (destacado: {featured})")

Manejar espacios de nombres XML

Los espacios de nombres son la parte del análisis XML que hace gruñir a la mayoría de los desarrolladores. En ElementTree, los URIs de espacios de nombres aparecen entre llaves en los nombres de etiquetas: {http://...}tagname. Así es cómo manejarlos de forma limpia:

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 expande los prefijos de espacio de nombres a URIs entre llaves
# Puedes definir un mapa de espacios de nombres para búsquedas XPath más limpias
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Usar el mapa de espacios de nombres en 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

Construir XML mediante programación

ElementTree también te permite construir XML desde cero — útil cuando necesitas crear peticiones SOAP o generar salida XML:

python
import xml.etree.ElementTree as ET

# Construir un documento de pedido
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)

# Serializar a cadena
ET.indent(order, space='  ')  # Python 3.9+ — sangría en su lugar
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — Transmitir archivos XML grandes

Para archivos XML grandes (decenas de MB o más), cargar todo el documento en memoria con parse() es costoso. iterparse() transmite el archivo (similar en espíritu al enfoque SAX orientado a eventos) y dispara eventos a medida que se encuentran los elementos, permitiéndote procesar y descartar elementos sobre la marcha:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Procesar un feed RSS/Atom grande sin cargarlo todo en memoria."""
    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', ''),
            })
            # Crítico: limpiar el elemento después de procesarlo para liberar memoria
            elem.clear()

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

    yield from articles  # yield cualquier restante

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

La llamada elem.clear() después de procesar cada elemento es la clave para mantener el uso de memoria estable independientemente del tamaño del archivo. Sin ella, ElementTree acumula todos los elementos analizados en memoria y pierdes el beneficio del streaming.

lxml — Cuando necesitas más potencia

La biblioteca lxml es una biblioteca XML rápida basada en C que extiende la API de ElementTree con soporte completo de XPath 1.0, validación de esquemas XSD y transformaciones XSLT. Instálala con pip install lxml:

python
from lxml import etree

# lxml usa la misma API que ElementTree en la mayoría de los casos
root = etree.fromstring(rss_xml.encode())  # lxml necesita bytes, no str

# XPath 1.0 completo — mucho más potente que el subconjunto de ElementTree
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath con predicados — obtener elementos de una categoría específica
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

# Validación de esquema XSD
xsd_doc = etree.parse('schema.xsd')
schema = etree.XMLSchema(xsd_doc)
xml_doc = etree.parse('data.xml')

if schema.validate(xml_doc):
    print("¡Válido!")
else:
    for error in schema.error_log:
        print(f"Línea {error.line}: {error.message}")

Herramientas útiles para trabajar con XML

Al construir integraciones XML en Python, estas herramientas del navegador ayudan con el lado de los datos: Formateador XML para hacer legibles las respuestas API sin procesar, Validador XML para verificar la buena formación antes de conectar tu parser, y XML a JSON cuando prefieres trabajar con dicts en lugar de elementos.

Resumen

El módulo xml.etree.ElementTree de Python cubre la mayoría de los escenarios XML del mundo real: usa find() y findall() con un mapa de espacios de nombres para SOAP y feeds con espacios de nombres, findtext() con valores por defecto para evitar AttributeError en elementos faltantes, e iterparse() para archivos grandes que no puedes cargar en memoria de una vez. Recurre a lxml cuando necesites validación XSD o el lenguaje de expresión XPath completo. La biblioteca estándar maneja todo lo demás perfectamente bien.