Seletor CSS ou XPath: qual usar no web scraping?

Publicado:

15 min de leitura

Acar Diveroli
Autor: Acar Diveroli
Um cubo CSS com .item à esquerda, um cartão de árvore DOM no meio e um cubo XPath com //a à direita

Depois de baixar uma página, começa o trabalho de verdade do scraping: encontrar e extrair o nome do produto, o preço e o link no meio de centenas de tags. Você descreve qual elemento quer com um seletor. Há duas linguagens comuns: os seletores CSS, que os desenvolvedores web conhecem das folhas de estilo, e o XPath, que vem do mundo XML. Muitas vezes dá para selecionar o mesmo elemento com qualquer uma das duas, mas uma é curta e legível, e a outra faz coisas que o CSS simplesmente não faz.

Neste artigo, explicamos o que são as duas linguagens, comparamos a sintaxe lado a lado, mostramos o que o XPath faz além do CSS e se a diferença de desempenho realmente importa. Depois, mostramos como escrever seletores que não quebram quando o design da página muda e como testar seletores no navegador. No meio do artigo há uma tabela de referência rápida de 20 linhas para você guardar; verificamos em uma página de teste que cada par CSS e XPath da tabela seleciona os mesmos elementos.

O que é um seletor CSS?

Um seletor CSS é a linguagem de padrões usada nas folhas de estilo CSS para indicar a quais elementos um estilo se aplica. Os navegadores também suportam essa linguagem em JavaScript por meio de document.querySelector() e querySelectorAll(). A definição atual é a Selectors Level 4 do W3C.

Um seletor CSS escolhe um elemento com base em:

  • Tag: div, a, span
  • Classe: .product
  • Id: #list
  • Atributo: a[href], img[src$=".webp"]
  • Hierarquia: ul > li (filho direto), div span (descendente em qualquer profundidade)
  • Irmãos: h2 + p (imediatamente seguinte), h2 ~ span (todos os irmãos seguintes)
  • Posição: li:first-of-type, li:nth-of-type(3)

A direção básica do CSS é para baixo e para frente: você vai de um elemento aos descendentes e aos irmãos seguintes. A pseudoclasse :has(), introduzida na Selectors Level 4, relaxa em parte essa regra: div.product:has(> span.discount) seleciona os cards de produto que contêm uma etiqueta de desconto. Mas :has() não "sobe" a partir de um elemento; ele seleciona o elemento externo olhando o que há dentro.

O que é XPath?

XPath (XML Path Language) é uma linguagem de consulta que seleciona nós em documentos XML e HTML com uma expressão de caminho. Ela enxerga o documento como uma árvore e pode se mover em qualquer direção: para baixo, para cima e para os irmãos anteriores e seguintes. A definição atual do W3C é a XPath 3.1.

Há um detalhe prático importante: os navegadores e a biblioteca lxml do Python suportam XPath 1.0. Recursos adicionados em versões posteriores, como ends-with(), funções de expressão regular e um sistema de tipos mais rico, não estão disponíveis nesses ambientes. Mantenha as expressões XPath para scraping dentro dos limites da versão 1.0.

As partes básicas do XPath:

  • // qualquer profundidade do documento, / filho direto: //ul/li
  • [...] uma condição (predicado): //a[@href], //li[3]
  • @ atributo: //img/@src
  • text() nó de texto: //h2/text()
  • Eixos: parent::, ancestor::, following-sibling::, preceding-sibling::
  • Funções: contains(), starts-with(), normalize-space(), last(), string-length()

No navegador, o XPath roda com a função JavaScript document.evaluate(); o uso dela está descrito na documentação do MDN.

Comparação de sintaxe

CritérioSeletor CSSXPath
LegibilidadeCurto, conhecido pelos desenvolvedores webLongo, curva de aprendizado mais íngreme
DireçãoPara baixo e para frente (em parte para fora com :has())Qualquer direção: pai, irmão anterior, ancestral
Seleção por textoNão está no padrãoSim: text(), contains()
Retornar valores de atributoNão (com extensões de bibliotecas)Sim: /@href
Retornar nós de textoNão (com extensões de bibliotecas)Sim: /text()
Correspondência de classe.product, exata e curtacontains(@class, ...) exige cuidado
Suporte nos navegadoresquerySelectorAlldocument.evaluate (XPath 1.0)
Suporte no PythonBeautifulSoup, lxml (cssselect), parsellxml, parsel; o BeautifulSoup não suporta
Navegadores headlessPlaywright, Puppeteer, SeleniumPlaywright, Puppeteer, Selenium
Uso típicoExtrair listas por classe e atributoTabelas rótulo-valor, seleção por texto

Tabela de referência rápida

Cada linha da tabela abaixo é a mesma seleção nas duas linguagens. Nas linhas sem equivalente em CSS, aparece o que o XPath faz a mais.

#O que é selecionado?Seletor CSSXPath
1Todos os elementos divdiv//div
2Por id#list//*[@id="list"]
3Por classe.product//*[contains(concat(" ", normalize-space(@class), " "), " product ")]
4Filho diretoul > li//ul/li
5Descendente em qualquer profundidadediv span//div//span
6Tem o atributoa[href]//a[@href]
7Valor do atributo igual ainput[name="q"]//input[@name="q"]
8Atributo começa coma[href^="https"]//a[starts-with(@href, "https")]
9Atributo contéma[href*="product"]//a[contains(@href, "product")]
10Atributo termina comimg[src$=".webp"]//img[substring(@src, string-length(@src) - 4) = ".webp"]
11Primeiro itemul > li:first-of-type//ul/li[1]
12Último itemul > li:last-of-type//ul/li[last()]
13Terceiro itemul > li:nth-of-type(3)//ul/li[3]
14Irmão imediatamente seguinteh2 + p//h2/following-sibling::*[1][self::p]
15Todos os irmãos seguintesh2 ~ span//h2/following-sibling::span
16Várias seleçõesh1, h2//h1 | //h2
17Card que contém um elemento específicodiv.product:has(> span.discount)//span[@class="discount"]/..
18Botão com texto exatoNão está no padrão//button[text()="Adicionar ao carrinho"]
19Um ancestral específico de um elementoNão está no padrão//span[@class="price"]/ancestor::div[@data-sku][1]
20Valor ao lado de um rótuloNão está no padrão//th[normalize-space()="Estoque"]/following-sibling::td[1]

Alguns detalhes da tabela causam erros frequentes:

  • Linha 3: //*[contains(@class, "product")] parece mais curto, mas também seleciona classes como products ou old-product. A expressão longa da tabela só corresponde à classe exata product.
  • Linha 10: o XPath 1.0 não tem ends-with(); os últimos caracteres são comparados com substring. O número é um a menos que o tamanho da extensão procurada (.webp tem cinco caracteres, - 4).
  • Linha 11: li:first-child e li:first-of-type são diferentes. O primeiro seleciona um li que é o primeiro filho do pai; se o primeiro filho for outra tag, não seleciona nada.
  • Linha 14: //h2/following-sibling::p[1] significa "o primeiro p seguinte" e corresponde mesmo com outros elementos no meio. O h2 + p do CSS só corresponde se o elemento logo depois do h2 for um p.

Para retornar valores de atributo e nós de texto, o XPath usa //a/@href e //h2/text(). A biblioteca parsel do Python adiciona ao CSS as extensões não padrão a::attr(href) e h2::text para esses trabalhos.

O que o XPath faz e o CSS não

Seleção por texto

Em uma página de loja online, o botão "Adicionar ao carrinho" e o botão "Esgotado" podem ter a mesma classe. A única coisa que os diferencia é o texto:

text
//button[text()="Adicionar ao carrinho"]
//span[contains(normalize-space(.), "Desconto")]

text() só olha o nó de texto direto do elemento; não corresponde se o texto estiver dividido entre tags aninhadas. normalize-space(.) junta todo o texto do elemento e remove os espaços do início e do fim, então costuma ser mais confiável.

O CSS padrão não tem seleção por texto. Algumas ferramentas oferecem extensões próprias: a opção has_text e :has-text() no Playwright, e :-soup-contains() no BeautifulSoup. Essas extensões só funcionam na respectiva ferramenta.

Subir ao pai e ao irmão anterior

Se em um card de produto só o campo de preço tem uma classe estável, você precisa partir do preço para chegar ao card inteiro:

text
//span[@class="price"]/..
//span[@class="price"]/ancestor::div[@data-sku][1]

.. seleciona o pai direto, e ancestor::, um ancestral em qualquer nível acima. [1] pega o ancestral mais próximo.

Estruturas rótulo-valor

Tabelas de especificação de produto e listas de definição estão entre as estruturas que o scraping mais encontra. Qual linha vem em qual ordem varia conforme o produto; o que fica fixo é o texto do rótulo:

text
//th[normalize-space()="Estoque"]/following-sibling::td[1]
//dt[normalize-space()="Garantia"]/following-sibling::dd[1]

Essas expressões encontram o valor ao lado de "Estoque" em qualquer linha da tabela. Para fazer o mesmo com CSS, você teria de pegar todas as linhas e percorrê-las no Python.

CSS e XPath no Python

O Python tem três bibliotecas comuns, e o suporte a seletores de cada uma é diferente.

O BeautifulSoup só suporta seletores CSS (select e select_one); não tem suporte a XPath:

python
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")
products = []
for card in soup.select("div.product"):
    products.append({
        "sku": card.get("data-sku"),
        "name": card.select_one("h2").get_text(strip=True),
        "price": card.select_one(".price").get_text(strip=True),
        "link": card.select_one("a[href]")["href"],
    })

O lxml roda XPath 1.0 diretamente e pode retornar o resultado como texto com funções como string():

python
from lxml import html as lxml_html

tree = lxml_html.fromstring(html)
stock = tree.xpath('string(//th[normalize-space()="Estoque"]/following-sibling::td[1])')
discounted_sku = tree.xpath('//span[@class="discount"]/ancestor::div[@data-sku][1]/@data-sku')

O parsel (a biblioteca de seletores do Scrapy) oferece as duas linguagens no mesmo objeto, encadeáveis entre si. Você encontra os cards com CSS e usa XPath dentro de um card:

python
from parsel import Selector

page = Selector(text=html)
for card in page.css("div.product"):
    name = card.css("h2::text").get()
    link = card.css("a::attr(href)").get()
    available = card.xpath('.//button[text()="Adicionar ao carrinho"]').get() is not None
    print(name, link, available)

No encadeamento, atenção: uma expressão XPath que roda dentro de um card começa com .//. Se você escrever //button, a busca percorre o documento inteiro em vez do card, e você encontra para cada card o primeiro botão da página.

Para decidir com qual linguagem e biblioteca trabalhar, veja Web scraping: JavaScript ou Python?.

Seletores em navegadores headless

Em páginas cujo conteúdo carrega com JavaScript, os seletores são usados dentro de uma ferramenta de automação de navegador. O Playwright suporta as duas linguagens e trata automaticamente como XPath uma expressão que começa com //:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/deals")

    prices = page.locator("div.product .price").all_inner_texts()
    stock = page.locator('xpath=//th[normalize-space()="Estoque"]/following-sibling::td[1]').inner_text()
    addable = page.locator("div.product", has_text="Adicionar ao carrinho")

    print(prices, stock, addable.count())
    browser.close()

No Selenium, os mesmos trabalhos são feitos com By.CSS_SELECTOR e By.XPATH; explicamos a instalação e a configuração de proxy em Como usar proxy com Selenium e Como usar proxy com SeleniumBase. Para saber se uma página realmente precisa de navegador, veja Páginas estáticas e dinâmicas.

A diferença de desempenho realmente importa?

A afirmação "seletores CSS são mais rápidos que XPath" aparece muito na internet. Depende do ambiente:

  • No navegador, querySelectorAll usa diretamente o mecanismo de seletores otimizado do navegador; o XPath com document.evaluate costuma ser mais lento. A diferença aparece em páginas muito grandes e em consultas repetidas milhares de vezes.
  • No Python com lxml e parsel, os seletores CSS são em sua maioria traduzidos para XPath por baixo dos panos e rodam como XPath. Então as duas linguagens usam o mesmo mecanismo e a diferença praticamente desaparece.
  • No BeautifulSoup, a velocidade de seleção depende mais do parser usado (html.parser ou lxml) do que da linguagem do seletor.

O que realmente decide é como o seletor é escrito. //*[contains(@class, "price")], que varre o documento inteiro, faz muito mais trabalho do que uma expressão que encontra primeiro o card e depois o preço dentro dele. E tudo isso costuma ser pouco perto do tempo de baixar a página pela rede ou carregá-la em um navegador headless. Tratamos do verdadeiro gargalo de um scraper em Concorrência e paralelismo.

Como evitar seletores frágeis?

O motivo mais comum de um scraper quebrar não é bloqueio, e sim mudança no design do site. Para um seletor sobreviver a pequenas mudanças na página:

  • Evite nomes de classe gerados automaticamente. Classes como css-1x9k2ab ou sc-bdVaJa podem mudar a cada versão.
  • Não escreva caminhos absolutos. Uma expressão copiada do navegador, como /html/body/div[3]/div[2]/ul/li[4]/span, quebra quando um único banner é adicionado à página.
  • Ancore no significado, não na posição. Não "o terceiro span", e sim "o span com a classe price" ou "a célula ao lado do título Estoque".
  • Prefira atributos estáveis. Atributos como data-sku, data-testid, itemprop e aria-label independem do design visual, então mudam menos.
  • Olhe primeiro os dados estruturados. Muitas páginas de produto trazem nome, preço e estoque em formato schema.org dentro de <script type="application/ld+json">. Ler esses dados é muito mais robusto do que analisar o HTML visual.
  • Selecione primeiro o contêiner, depois o campo. Encontrar um card uma vez e buscar os campos dentro dele evita misturar campos.
  • Trate resultado vazio como erro. Em vez de gravar em silêncio um valor vazio quando um seletor não encontra nada, registre e dispare um alerta; você percebe a mudança de design no primeiro dia.

Seletores voltando vazios nem sempre são mudança de design; às vezes é outra página devolvida pela proteção de bots e às vezes é conteúdo carregado depois com JavaScript. Para o diagnóstico, veja a lista em Como fazer web scraping sem ser bloqueado.

Como testar um seletor no navegador?

Testar um seletor no navegador antes de escrevê-lo no código economiza tempo. Nas ferramentas do desenvolvedor (F12) do Chrome, do Edge e do Firefox:

  • $$("div.product .price") no console retorna como array todos os elementos que correspondem ao seletor CSS.
  • $x('//th[normalize-space()="Estoque"]/following-sibling::td[1]') no console roda uma expressão XPath.
  • Ctrl + F no painel Elements abre uma caixa de busca que aceita texto simples, seletores CSS e XPath e destaca um a um os elementos correspondentes.

Dois avisos: a página que você vê no navegador é o estado depois de o JavaScript rodar. Se o seu scraper só pega o HTML que o servidor devolve por um cliente HTTP, um seletor que funciona no navegador pode voltar vazio no código; confira vendo o código-fonte (Ctrl + U). Segundo, o recurso "Copy XPath" do navegador geralmente gera caminhos absolutos e frágeis; use-os como ponto de partida e simplifique.

Casos de uso

  • Monitoramento de preços: cards de produto com CSS e campos rótulo-valor como "Estoque" com XPath. A configuração está na nossa página de solução de monitoramento de preços.
  • Coleta de catálogos: primeiro JSON-LD; se não houver, seletores CSS com atributos data-* estáveis. A configuração geral de coleta de dados está na nossa página de solução de extração de dados.
  • Sites comparadores: um arquivo de definição de seletores por site para estruturas diferentes; XPath para campos baseados em texto.
  • Automação de testes: seletores CSS com atributos data-testid na sua própria aplicação; testes que não são afetados por mudanças de design.
  • Coleta regular de muitas páginas: enquanto os seletores se mantêm estáveis, o trabalho vira questão de distribuir requisições; dá para usar um Proxies rotativos para IPs de saída diferentes por um único endereço.

Guia de decisão

Sua necessidadeRecomendação
Selecionar por classe, id ou atributoSeletor CSS
Você usa BeautifulSoupSeletor CSS (sem suporte a XPath)
Encontrar um elemento pelo textoXPath
Ir de um elemento ao pai ou ao irmão anteriorXPath
Tabelas rótulo-valor e listas de definiçãoXPath
Retornar diretamente um valor de atributoXPath /@attr ou parsel ::attr()
Os dois no mesmo projetoparsel
Navegador headlessLocators do Playwright, CSS primeiro
Robustez contra mudanças de designAtributos data-* ou JSON-LD

Perguntas frequentes

Aprendo seletores CSS ou XPath?

Aprenda primeiro os seletores CSS; são mais curtos, mais comuns e úteis também no desenvolvimento web. Quando no scraping você precisar selecionar por texto e subir aos pais, basta aprender os eixos e as funções do XPath. Com bibliotecas como o parsel, que usam os dois, a troca é fácil.

O BeautifulSoup suporta XPath?

Não. O BeautifulSoup só suporta seletores CSS. Se você precisar de XPath, pode analisar o mesmo HTML com lxml ou parsel.

O CSS consegue selecionar por texto?

No CSS padrão, não. Algumas ferramentas oferecem extensões próprias: has_text e :has-text() no Playwright, e :-soup-contains() no BeautifulSoup. Essas extensões só funcionam na respectiva ferramenta e não são portáveis.

Por que o XPath copiado do navegador não funciona?

O caminho gerado pelo navegador geralmente é absoluto e baseado na página depois de o JavaScript rodar. Essa estrutura pode não existir no HTML que o servidor devolve inicialmente, ou uma pequena mudança de design pode quebrar o caminho. Simplifique o caminho para que comece em uma classe, um id ou um atributo estáveis.

Qual é a diferença entre text() e normalize-space()?

text() retorna os nós de texto diretos do elemento e deixa os espaços em branco como estão. normalize-space(.) junta todo o texto do elemento, incluindo tags aninhadas, remove os espaços do início e do fim e reduz os espaços internos a um. Para comparar textos, normalize-space() costuma ser mais confiável.

Por que os meus seletores às vezes voltam vazios?

Há três motivos comuns: o design da página mudou, o conteúdo é carregado depois com JavaScript ou o site está devolvendo uma página de verificação ou de erro em vez da esperada. Salvar o HTML da resposta em um arquivo e testar o seletor nesse arquivo é o jeito mais rápido de diferenciar os três casos.

Em resumo

Seletores CSS são curtos, legíveis e suportados por todas as ferramentas; devem ser a escolha padrão para selecionar por tag, classe, id e atributo. O XPath é necessário para o que o CSS não faz, como selecionar por texto, subir a pais e irmãos anteriores e lidar com estruturas rótulo-valor; nos navegadores e no lxml, fica limitado à versão 1.0. Na maioria dos trabalhos de scraping, a diferença de desempenho é desprezível perto do tempo de rede; o que realmente importa é escrever seletores que sobrevivam a mudanças de design. Tipos de proxy adequados ao seu trabalho de coleta de dados estão nos nossos serviços de proxy.