Scraping por linguagem 6 min de leitura

Web scraping em Python com lxml: velocidade e XPath

Por que lxml é um dos parsers de HTML e XML mais rápidos do Python: consultas XPath, comparação com BeautifulSoup e técnicas para documentos muito grandes.

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

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

  1. Instalação e os dois módulos do lxml
  2. Parsing de HTML
  3. XPath: a base da potência do lxml
  4. Seletores CSS com cssselect
  5. Extração de dados: texto, atributos e links
  6. Acentos e codificações no lxml
  7. Documentos grandes: iterparse
  8. lxml vs BeautifulSoup
  9. Prós e contras

1. Instalação e os dois módulos do lxml

bash
pip install lxml

O 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

python
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 sozinho

fromstring 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:

python
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.

python
# 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

python
# 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:

python
# 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):

bash
pip install cssselect
python
# 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

python
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:

python
# <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:

python
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href')   # agora todos os links são absolutos

make_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:

python
# 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):

python
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:

python
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á processados

Essa 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.
  • iterparse para 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.