A biblioteca padrão do Python inclui um sólido parser XML — sem necessidade de pip install. xml.etree.ElementTree lida com a grande maioria do XML do mundo real: feeds RSS, respostas SOAP, arquivos de configuração, arquivos de recursos Android, Maven POMs. Você só precisa recorrer ao lxml quando atingir validação de esquema XSD, XPath complexo, ou arquivos realmente massivos. Vamos passar pelos dois, com exemplos reais.

Fundamentos do ElementTree — Analisando de uma String ou Arquivo

O módulo xml.etree.ElementTree oferece dois pontos de entrada: fromstring() para analisar strings XML, e parse() para ler diretamente de um arquivo. Aqui está um exemplo prático usando uma estrutura 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>"""

# Analisa de uma string
root = ET.fromstring(rss_xml)

# Analisa de um arquivo (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 e findtext — Pesquisando na Árvore

Esses três métodos são suas ferramentas principais para extrair dados. Todos aceitam uma expressão de caminho simples (como um XPath limitado) para navegar pela árvore de elementos:

python
import xml.etree.ElementTree as ET

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

# find() — retorna o primeiro elemento correspondente, ou None
first_item = channel.find('item')
print(first_item.find('title').text)  # Understanding Database Indexes

# findall() — retorna uma lista de todos os elementos correspondentes
items = channel.findall('item')
print(len(items))  # 2

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

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

# findtext() com um padrão (evita AttributeError em elementos ausentes)
author = channel.findtext('item/author', default='Unknown Author')
print(author)  # Unknown Author
Use findtext() com um padrão. Se você usar find().text e o elemento não existir, find() retorna None e .text lança um AttributeError. findtext('tag', default='') lida com elementos ausentes graciosamente — muito mais seguro ao analisar XML de fontes externas.

Lendo 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')  # com padrão
    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})")

Lidando com Namespaces XML

Namespaces são a parte da análise XML que faz a maioria dos desenvolvedores reclamar. No ElementTree, URIs de namespace aparecem entre chaves nos nomes de tag: {http://...}tagname. Veja como lidar com isso de forma limpa:

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 prefixos de namespace para URIs entre chaves
# Você pode definir um mapa de namespace para buscas no estilo XPath mais limpas
ns = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'inv': 'http://www.example.com/invoice'
}

# Usa o mapa de namespace em 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

Construindo XML Programaticamente

ElementTree também permite construir XML do zero — útil quando você precisa criar requisições SOAP ou gerar saída XML:

python
import xml.etree.ElementTree as ET

# Constrói um 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)

# Serializa para string
ET.indent(order, space='  ')  # Python 3.9+ — formata no lugar
xml_output = ET.tostring(order, encoding='unicode', xml_declaration=True)
print(xml_output)

iterparse — Transmitindo Arquivos XML Grandes

Para arquivos XML grandes (dezenas de MB ou mais), carregar o documento inteiro na memória com parse() é custoso. iterparse() transmite o arquivo (semelhante à abordagem SAX baseada em eventos) e dispara eventos à medida que os elementos são encontrados, permitindo que você processe e descarte elementos conforme avança:

python
import xml.etree.ElementTree as ET

def process_large_feed(filepath):
    """Processa um feed RSS/Atom grande sem carregá-lo todo na memória."""
    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', ''),
            })
            # Essencial: limpa o elemento após processar para liberar memória
            elem.clear()

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

    yield from articles  # retorna quaisquer itens restantes

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

A chamada elem.clear() após processar cada elemento é a chave para manter o uso de memória constante independentemente do tamanho do arquivo. Sem isso, ElementTree acumula todos os elementos analisados na memória e você perde o benefício do streaming.

lxml — Quando Você Precisa de Mais Poder

A biblioteca lxml é uma biblioteca XML rápida baseada em C que estende a API do ElementTree com suporte completo a XPath 1.0, validação de esquema XSD e transformações XSLT. Instale com pip install lxml:

python
from lxml import etree

# lxml usa a mesma API que ElementTree na maioria dos casos
root = etree.fromstring(rss_xml.encode())  # lxml precisa de bytes, não str

# XPath 1.0 completo — muito mais poderoso que o subconjunto do ElementTree
items = root.xpath('//item[position() <= 2]/title/text()')
print(items)  # ['Understanding Database Indexes', 'REST API Design Patterns']

# XPath com predicados — obtém itens de uma categoria específica
db_items = root.xpath('//item[category="Database"]/title/text()')
print(db_items)  # ['Understanding Database Indexes']

# Validação 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("Valid!")
else:
    for error in schema.error_log:
        print(f"Line {error.line}: {error.message}")

Ferramentas Úteis para Trabalho com XML

Ao construir integrações XML em Python, essas ferramentas do navegador ajudam no lado dos dados: Formatador XML para tornar legíveis as respostas brutas de API, Validador XML para verificar o formato antes de conectar seu parser, e XML para JSON quando você preferir trabalhar com dicts em vez de elementos.

Conclusão

O xml.etree.ElementTree do Python cobre a maioria dos cenários XML do mundo real: use find() e findall() com um mapa de namespace para SOAP e feeds com namespace, findtext() com padrões para evitar AttributeError em elementos ausentes, e iterparse() para arquivos grandes que você não pode carregar na memória de uma vez. Recorra ao lxml quando precisar de validação XSD ou da linguagem de expressão XPath completa. A biblioteca padrão lida bem com todo o resto.