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
- Em que XML difere de HTML
- ElementTree, o módulo integrado
- lxml.etree, rápido e com XPath
- Espaços de nomes (namespaces)
- Prática: feed RSS e sitemap
- Codificações e acentos no XML
- Arquivos grandes: parsing em fluxo
- Criar e gravar XML
- Segurança ao parsear XML
- 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.
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:
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.
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 dofromstring; 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.
<feed xmlns="http://www.w3.org/2005/Atom">
<entry><title>Notícia</title></entry>
</feed>É preciso indicar o espaço de nomes explicitamente:
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:
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
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:
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 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:
# 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:
from lxml import etree
root = etree.fromstring(resp.content) # bytes, não resp.textSe a codificação do prólogo estiver errada (acontece em exportações de sistemas antigos), transcodifique na mão:
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
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.
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:
pip install defusedxmlfrom 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.