lxml é a biblioteca de parsing de HTML e XML mais rápida do ecossistema Python. É construída sobre as bibliotecas C libxml2 e libxslt, por isso supera com folga os parsers escritos em Python puro e, além disso, oferece XPath completo, mais potente que os seletores CSS. Se você bateu no limite de desempenho do BeautifulSoup em volumes grandes ou precisa de seleções complexas, o lxml é a escolha certa.
Este artigo aprofunda um tema do guia geral «Web scraping com Python», onde o lxml aparece mencionado ao lado de outros parsers. Aqui o analisamos em detalhe.
Sumário
- Instalação e os dois módulos do lxml
- Parsing de HTML
- XPath: a base da potência do lxml
- Seletores CSS com cssselect
- Extração de dados: texto, atributos e links
- Acentos e codificações no lxml
- Documentos grandes: iterparse
- lxml vs BeautifulSoup
- Prós e contras
1. Instalação e os dois módulos do lxml
pip install lxmlO lxml tem dois módulos-chave:
lxml.html: para parsear HTML (perdoa a marcação «suja», igual a um navegador).lxml.etree: para XML estrito (veja o artigo dedicado «Parsing de XML em Python»).
Para o scraping web, em 95% dos casos você vai precisar do lxml.html.
2. Parsing de HTML
import requests
from lxml import html
resp = requests.get("https://example.com", timeout=10)
tree = html.fromstring(resp.content) # passamos bytes: o lxml detecta a codificação sozinhofromstring retorna o elemento raiz da árvore. A partir daí, aplicam-se a ele XPath ou seletores CSS. Também dá para parsear a partir de um arquivo:
tree = html.parse("page.html").getroot()Importante: passe resp.content (bytes), e não resp.text (string); assim o lxml lerá corretamente a declaração de codificação do próprio HTML. Isso elimina a maioria dos problemas com acentos e caracteres especiais.
3. XPath: a base da potência do lxml
XPath é uma linguagem para endereçar nós dentro da árvore do documento, e é aqui que o lxml mostra todo o seu potencial.
# texto de todos os cabeçalhos h2 com a classe title
titles = tree.xpath('//h2[@class="title"]/text()')
# atributo href de todos os links
links = tree.xpath('//a/@href')
# texto dentro de um bloco específico
price = tree.xpath('//div[@class="price"]/text()')[0]Construções úteis de XPath
# por correspondência parcial da classe (quando class="title big featured")
tree.xpath('//h2[contains(@class, "title")]/text()')
# pelo texto do elemento
tree.xpath('//a[text()="Ver mais"]/@href')
# enésimo elemento (a numeração começa em 1!)
tree.xpath('(//div[@class="item"])[3]')
# caminho relativo a partir de um nó encontrado
for card in tree.xpath('//div[@class="card"]'):
name = card.xpath('.//h3/text()') # o ponto significa «a partir do nó atual»
link = card.xpath('.//a/@href')O ponto inicial (.//) em um XPath relativo é crítico: sem ele, a busca partirá da raiz do documento, e não do card atual. É o erro mais frequente entre quem está começando.
Eixos do XPath
O XPath sabe se mover pela árvore em qualquer direção, algo que o CSS não permite:
# o pai do elemento
tree.xpath('//span[@class="price"]/parent::div')
# o elemento seguinte do mesmo nível
tree.xpath('//h2/following-sibling::p[1]/text()')
# o ancestral com uma classe determinada
tree.xpath('//a[@id="buy"]/ancestor::div[@class="product"]')
4. Seletores CSS com cssselect
Se o XPath lhe parecer enrolado, o lxml aceita seletores CSS (é necessário o pacote cssselect):
pip install cssselect# o método .cssselect() retorna uma lista de elementos
cards = tree.cssselect("div.product-card")
title = tree.cssselect("h1.title")[0].text_content()
links = [a.get("href") for a in tree.cssselect("a.product-link")]O CSS é mais familiar para quem vem do front-end. Por baixo, o cssselect traduz o CSS para XPath, então a velocidade se mantém. Para as seleções complexas (por texto, por ancestrais) o XPath continuará sendo necessário.
5. Extração de dados: texto, atributos e links
el = tree.cssselect(".product")[0]
el.text_content() # todo o texto interno, incluindo tags aninhadas
el.get("href") # valor de um atributo
el.attrib # dicionário com todos os atributos
el.tag # nome da tag
el.text # apenas o texto direto do nó (sem os aninhados)A diferença entre .text e .text_content() importa:
# <p>Olá, <b>mundo</b>!</p>
p.text # "Olá, " (apenas até a tag aninhada)
p.text_content() # "Olá, mundo!" (todo o texto, recursivamente)Para converter os links relativos em absolutos:
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href') # agora todos os links são absolutosmake_links_absolute poupa a colagem manual de URLs, algo muito prático ao percorrer um site.
6. Acentos e codificações no lxml
O lxml lida sem problemas com os acentos, o ç e qualquer caractere não ASCII, desde que receba bytes, e não uma string já decodificada:
# CORRETO: bytes; o lxml lerá a codificação do <meta charset>
tree = html.fromstring(resp.content)
# ARRISCADO: string já decodificada pelo requests (talvez de forma errada)
tree = html.fromstring(resp.text)Se precisar fixar a codificação manualmente (por exemplo, quando o servidor mente nos cabeçalhos):
from lxml import html
parser = html.HTMLParser(encoding="windows-1252")
tree = html.fromstring(resp.content, parser=parser)A teoria completa do problema das codificações está no guia geral, seção «Codificações».
7. Documentos grandes: iterparse
Quando o documento é enorme (um feed XML de centenas de megabytes), carregá-lo inteiro na memória é um desperdício. O iterparse o lê em fluxo, processando os elementos à medida que aparecem e liberando memória:
from lxml import etree
for event, element in etree.iterparse("huge.xml", tag="item"):
title = element.findtext("title")
process(title)
element.clear() # liberamos memória
while element.getprevious() is not None:
del element.getparent()[0] # removemos os nós já processadosEssa técnica (leitura + clear()) permite parsear arquivos que não cabem inteiros na RAM. Há mais detalhes sobre o parsing de XML em fluxo em «Parsing de XML em Python».
8. lxml vs BeautifulSoup
| Critério | lxml | BeautifulSoup |
|---|---|---|
| Velocidade | muito alta (C) | menor (sem o motor lxml) |
| API | mais estrita, exige saber XPath | amigável, lê-se como texto |
| XPath | completo | não (só busca CSS) |
| HTML «sujo» | bem | muito bem, perdoa quase tudo |
| Memória | mais eficiente, com iterparse | consumo maior |
| Barreira de entrada | mais alta | mais baixa |
Na prática, os dois são combinados com frequência: o BeautifulSoup pode usar o lxml como motor (BeautifulSoup(html, "lxml")), e assim você obtém a comodidade da API do bs4 e a velocidade do lxml. Se precisar de XPath ou do parsing em fluxo de arquivos de gigabytes, use o lxml diretamente. A comparação dos diferentes parsers está no guia geral, e a extração de tabelas, em um artigo à parte.
9. Prós e contras do lxml
Prós:
- Um dos parsers mais rápidos do ecossistema Python (núcleo em C).
- XPath completo com eixos (parent, sibling, ancestor), algo inalcançável para o CSS.
- Suporte a CSS via cssselect: o melhor dos dois mundos.
iterparsepara o processamento em fluxo de documentos gigantescos.- Consumo de memória contido.
Contras:
- Curva de aprendizado mais íngreme, sobretudo por causa do XPath.
- API mais estrita e menos «indulgente» que a do BeautifulSoup.
- Mensagens de erro às vezes crípticas.
- A instalação às vezes exige bibliotecas do sistema (em alguns sistemas operacionais).
Conclusão: o lxml é a opção quando desempenho e seleções complexas vêm em primeiro lugar. Para tarefas pontuais e máxima legibilidade, o BeautifulSoup é mais simples; para volumes gigantescos com seleções CSS também vale uma olhada no selectolax. Justamente pela velocidade, o lxml costuma formar dupla com o scraping assíncrono: quando se baixam centenas de páginas ao mesmo tempo, um parser lento vira o gargalo, e o motor em C do lxml o elimina.