Scraping por linguagem 8 min de leitura

Parsing de XML em Python: ElementTree, lxml e processamento em fluxo

Trabalhar com XML em Python: ElementTree para tarefas simples, lxml para XPath e parsing em fluxo de arquivos de vários gigabytes com iterparse.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 27 janeiro 2025

XML continua em toda parte: feeds RSS e Atom, sitemaps, respostas de SOAP e de muitas APIs REST, exportações de sistemas ERP, formatos bancários e governamentais. Python oferece várias formas de parsear XML: do ElementTree integrado ao veloz lxml, passando pelo SAX em fluxo para arquivos gigantescos. Vejamos quando cada uma convém.

Este é um artigo especializado que dá sequência ao guia geral «Web scraping com Python». Se o XML chega até você misturado com páginas HTML, comece por ele; aqui o foco é o XML.

Índice

  1. Em que XML difere de HTML
  2. ElementTree, o módulo integrado
  3. lxml.etree, rápido e com XPath
  4. Espaços de nomes (namespaces)
  5. Prática: feed RSS e sitemap
  6. Codificações e acentos no XML
  7. Arquivos grandes: parsing em fluxo
  8. Criar e gravar XML
  9. Segurança ao parsear XML
  10. Vantagens e desvantagens de cada abordagem

1. Em que XML difere de HTML

Embora ambos sejam linguagens de marcação, a diferença é fundamental:

  • XML é estrito: toda tag precisa ser fechada, maiúsculas e minúsculas importam e a estrutura tem de ser válida. O HTML perdoa os erros.
  • No XML não há tags predefinidas: quem as define é você (ou o formato): <book>, <price>, <author>.
  • O XML usa com frequência espaços de nomes para separar vocabulários.

Pela sua rigidez, o XML é parseado de forma mais previsível que o HTML, mas qualquer erro na marcação derruba a análise. Para XML inválido, ajuda o modo «tolerante» do lxml.

Vale ter em mente uma tendência geral: nas APIs modernas, o JSON, mais leve, deslocou o XML em grande medida; se a fonte oferece um endpoint JSON, ele costuma ser mais fácil de parsear (as técnicas estão em «Parsing de JSON em Python»). Mas onde o XML segue sendo o padrão (RSS, sitemap, SOAP, formatos governamentais, exportações de ERP), não há outro caminho: é preciso parseá-lo.


2. ElementTree, o módulo integrado

xml.etree.ElementTree vem na biblioteca padrão, sem instalar dependências. É suficiente para a maioria das tarefas.

python
import xml.etree.ElementTree as ET

xml_data = """
<catalog>
    <book id="1">
        <title>Python para iniciantes</title>
        <price>590</price>
    </book>
    <book id="2">
        <title>Extração de dados</title>
        <price>720</price>
    </book>
</catalog>
"""

root = ET.fromstring(xml_data)          # a partir de uma string
# root = ET.parse("catalog.xml").getroot()   # a partir de um arquivo

for book in root.findall("book"):
    title = book.find("title").text
    price = book.find("price").text
    book_id = book.get("id")            # atributo
    print(book_id, title, price)

Métodos-chave:

  • find("tag") — o primeiro elemento filho com essa tag;
  • findall("tag") — todos esses elementos;
  • .text — o texto dentro do elemento;
  • .get("attr") — o valor de um atributo;
  • .attrib — o dicionário com todos os atributos.

O ElementTree aceita um XPath limitado:

python
root.findall(".//book")                 # todos os book em qualquer profundidade
root.findall("book[@id='1']")           # por atributo
root.findall(".//price")

3. lxml.etree, rápido e com XPath completo

Quando se precisa de velocidade ou de um XPath completo, recorre-se ao lxml.etree. A API dele quase coincide com a do ElementTree, então a transição é suave.

python
from lxml import etree

root = etree.fromstring(xml_data.encode("utf-8"))

# XPath completo
titles = root.xpath("//book/title/text()")
expensive = root.xpath("//book[price > 600]/title/text()")
first_id = root.xpath("//book[1]/@id")

O lxml oferece o que o ElementTree não tem: XPath 1.0 completo com funções (contains, starts-with, comparações), eixos (parent, sibling) e transformações XSLT. Sobre as capacidades dele com HTML, veja o artigo sobre lxml.

Dica: no lxml, codifique a string para bytes (.encode("utf-8")) antes do fromstring; caso contrário, se o XML incluir uma declaração <?xml encoding=...?>, pode ocorrer um erro.


4. Espaços de nomes (namespaces)

É a pedra no sapato dos iniciantes. Se o XML tem xmlns, um simples find("title") não encontra nada: as tags estão «escondidas» no espaço de nomes.

xml
<feed xmlns="http://www.w3.org/2005/Atom">
    <entry><title>Notícia</title></entry>
</feed>

É preciso indicar o espaço de nomes explicitamente:

python
import xml.etree.ElementTree as ET

root = ET.fromstring(xml_data)
ns = {"atom": "http://www.w3.org/2005/Atom"}

# por meio de um dicionário de prefixos
titles = root.findall(".//atom:title", ns)
for t in titles:
    print(t.text)

No lxml é mais cômodo: dá para usar local-name() e ignorar o espaço de nomes:

python
from lxml import etree
root = etree.fromstring(xml_bytes)
# pegamos title sem nos importar com o namespace
titles = root.xpath("//*[local-name()='title']/text()")

local-name() é o salva-vidas quando há muitos espaços de nomes ou quando eles não são conhecidos de antemão.


5. Prática: feed RSS e sitemap

Feed RSS

python
import requests
import xml.etree.ElementTree as ET

resp = requests.get("https://example.com/rss", timeout=10)
root = ET.fromstring(resp.content)       # .content são bytes

for item in root.findall(".//item"):
    title = item.findtext("title")
    link = item.findtext("link")
    date = item.findtext("pubDate")
    print(title, link, date)

findtext é mais prático que find().text: não quebra com AttributeError se o elemento não existe — retorna None ou o valor padrão indicado.

Sitemap (mapa do site)

O sitemap quase sempre carrega espaço de nomes: uma armadilha habitual na hora de coletar todas as URLs de um site:

python
import requests
from lxml import etree

resp = requests.get("https://example.com/sitemap.xml", timeout=10)
root = etree.fromstring(resp.content)

# contornamos o namespace com local-name()
urls = root.xpath("//*[local-name()='loc']/text()")
print(f"Encontradas {len(urls)} URLs")

O sitemap é um excelente ponto de partida para um crawler: a lista de todas as páginas do site sem seguir links. Sobre como armazenar essas URLs em uma fila, veja o guia geral, seção «Filas». Quando há muitos desses feeds ou arquivos de sitemap (em sites grandes o sitemap se divide em dezenas de partes), convém baixá-los em paralelo: veja «Scraping assíncrono em Python».


6. Codificações e acentos no XML

No XML, a codificação é declarada no prólogo:

xml
<?xml version="1.0" encoding="ISO-8859-1"?>

A regra de ouro: passe ao parser bytes, não uma string, para que ele mesmo leia essa declaração:

python
# CORRETO: bytes
root = ET.fromstring(resp.content)

# ERRO: se a string já foi decodificada errado, aparecem caracteres corrompidos
root = ET.fromstring(resp.text)

Um erro típico do lxml é ValueError: Unicode strings with encoding declaration are not supported. Ele ocorre quando se passa uma string já decodificada a um documento com declaração de codificação. A solução é passar bytes:

python
from lxml import etree
root = etree.fromstring(resp.content)        # bytes, não resp.text

Se a codificação do prólogo estiver errada (acontece em exportações de sistemas antigos), transcodifique na mão:

python
text = resp.content.decode("iso-8859-1", errors="replace")
# removemos o prólogo problemático e transcodificamos
text = text.replace('encoding="iso-8859-1"', 'encoding="utf-8"')
root = etree.fromstring(text.encode("utf-8"))

A teoria geral das codificações está no guia geral, seção «Codificações».


7. Arquivos grandes: parsing em fluxo

Exportações de centenas de megabytes (feeds de produtos, dumps) não podem ser carregadas inteiras na memória. Duas abordagens em fluxo.

iterparse no lxml, o recomendado

python
from lxml import etree

for event, elem in etree.iterparse("huge_feed.xml", tag="offer"):
    title = elem.findtext("name")
    price = elem.findtext("price")
    process(title, price)

    elem.clear()                          # liberamos a memória do que já foi processado
    while elem.getprevious() is not None:
        del elem.getparent()[0]

A combinação de clear() com a remoção dos nós anteriores mantém o uso de memória quase constante, seja qual for o tamanho do arquivo. Isso permite parsear arquivos de dezenas de gigabytes.

SAX, análise por eventos

O xml.sax integrado invoca seus callbacks a cada tag de abertura/fechamento. Consome pouquíssima memória, mas o código é mais verboso e menos cômodo que o iterparse. Na prática, o iterparse do lxml cobre quase todas as necessidades de processamento em fluxo.


8. Criar e gravar XML

O parsing costuma andar de mãos dadas com a geração: por exemplo, para salvar o resultado em XML.

python
from lxml import etree

root = etree.Element("catalog")
book = etree.SubElement(root, "book", id="1")
etree.SubElement(book, "title").text = "Web scraping com Python"
etree.SubElement(book, "price").text = "590"

xml_bytes = etree.tostring(
    root,
    pretty_print=True,
    xml_declaration=True,
    encoding="utf-8",
)
with open("output.xml", "wb") as f:       # 'wb': bytes!
    f.write(xml_bytes)

O encoding="utf-8" no tostring garante que os acentos e a cedilha sejam gravados corretamente, e a escrita no modo "wb" evita que os bytes sejam recodificados de novo.


9. Segurança ao parsear XML

O XML de fontes não confiáveis pode conter ataques: por exemplo, o «billion laughs» (expansão exponencial de entidades que esgota a memória) ou entidades externas (XXE) que leem arquivos do seu servidor.

A defesa é o pacote defusedxml:

bash
pip install defusedxml
python
from defusedxml.ElementTree import fromstring   # substituto seguro
root = fromstring(untrusted_xml)

Se você parseia XML de fontes externas fora do seu controle, use defusedxml no lugar dos parsers padrão. Ele elimina as principais classes de ataques via XML.


10. Vantagens e desvantagens de cada abordagem

Ferramenta Vantagens Desvantagens
ElementTree integrado, sem dependências, API simples XPath limitado, mais lento que o lxml
lxml.etree rápido, XPath completo, iterparse, XSLT dependência externa, mais rígido com os erros
xml.sax memória mínima em arquivos enormes verboso, incômodo de desenvolver
defusedxml proteção contra ataques via XML só para parsear, não para gravar

Como escolher:

  • XML pequeno, sem dependências extras → ElementTree.
  • Precisa de velocidade, XPath complexo ou namespaces → lxml.etree.
  • O arquivo não cabe na memória → iterparse (lxml).
  • XML de fonte não confiável → defusedxml.