---
title: "Seletor CSS ou XPath: qual usar no web scraping?"
description: "Seletores CSS são curtos e legíveis; o XPath também seleciona por texto e por elemento pai. Comparamos sintaxe, velocidade e exemplos em Python."
url: https://proxynet.io/pt-br/blog/css-selector-vs-xpath
date: 2026-09-13
author: "Acar Diveroli"
category: "Comparativos, Web scraping"
lang: pt-BR
---

# Seletor CSS ou XPath: qual usar no web scraping?

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.

> **Nota: Resposta rápida**
>
> Se você seleciona elementos por tag, classe, id e atributo, os seletores CSS são mais curtos, mais legíveis e suportados por todas as ferramentas. Se precisa encontrar um elemento pelo texto, ou ir de um elemento ao pai ou ao irmão anterior, você precisa de XPath. A diferença de velocidade entre as duas linguagens costuma ser desprezível perto do tempo de rede e de carregamento da página. A abordagem prática: CSS por padrão, XPath onde o CSS não dá conta.

## 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](https://www.w3.org/TR/selectors-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](https://www.w3.org/TR/xpath-31/).

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](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate).

## Comparação de sintaxe

| Critério | Seletor CSS | XPath |
|---|---|---|
| Legibilidade | Curto, conhecido pelos desenvolvedores web | Longo, curva de aprendizado mais íngreme |
| Direção | Para baixo e para frente (em parte para fora com `:has()`) | Qualquer direção: pai, irmão anterior, ancestral |
| Seleção por texto | Não está no padrão | Sim: `text()`, `contains()` |
| Retornar valores de atributo | Não (com extensões de bibliotecas) | Sim: `/@href` |
| Retornar nós de texto | Não (com extensões de bibliotecas) | Sim: `/text()` |
| Correspondência de classe | `.product`, exata e curta | `contains(@class, ...)` exige cuidado |
| Suporte nos navegadores | `querySelectorAll` | `document.evaluate` (XPath 1.0) |
| Suporte no Python | BeautifulSoup, lxml (cssselect), parsel | lxml, parsel; o BeautifulSoup não suporta |
| Navegadores headless | Playwright, Puppeteer, Selenium | Playwright, Puppeteer, Selenium |
| Uso típico | Extrair listas por classe e atributo | Tabelas 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 CSS | XPath |
|---|---|---|---|
| 1 | Todos os elementos `div` | `div` | `//div` |
| 2 | Por id | `#list` | `//*[@id="list"]` |
| 3 | Por classe | `.product` | `//*[contains(concat(" ", normalize-space(@class), " "), " product ")]` |
| 4 | Filho direto | `ul > li` | `//ul/li` |
| 5 | Descendente em qualquer profundidade | `div span` | `//div//span` |
| 6 | Tem o atributo | `a[href]` | `//a[@href]` |
| 7 | Valor do atributo igual a | `input[name="q"]` | `//input[@name="q"]` |
| 8 | Atributo começa com | `a[href^="https"]` | `//a[starts-with(@href, "https")]` |
| 9 | Atributo contém | `a[href*="product"]` | `//a[contains(@href, "product")]` |
| 10 | Atributo termina com | `img[src$=".webp"]` | `//img[substring(@src, string-length(@src) - 4) = ".webp"]` |
| 11 | Primeiro item | `ul > li:first-of-type` | `//ul/li[1]` |
| 12 | Último item | `ul > li:last-of-type` | `//ul/li[last()]` |
| 13 | Terceiro item | `ul > li:nth-of-type(3)` | `//ul/li[3]` |
| 14 | Irmão imediatamente seguinte | `h2 + p` | `//h2/following-sibling::*[1][self::p]` |
| 15 | Todos os irmãos seguintes | `h2 ~ span` | `//h2/following-sibling::span` |
| 16 | Várias seleções | `h1, h2` | `//h1 \| //h2` |
| 17 | Card que contém um elemento específico | `div.product:has(> span.discount)` | `//span[@class="discount"]/..` |
| 18 | Botão com texto exato | Não está no padrão | `//button[text()="Adicionar ao carrinho"]` |
| 19 | Um ancestral específico de um elemento | Não está no padrão | `//span[@class="price"]/ancestor::div[@data-sku][1]` |
| 20 | Valor ao lado de um rótulo | Nã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?](/pt-br/blog/web-scraping-javascript-vs-python). Os detalhes de `find_all`, da seleção por classe e de `get_text` no BeautifulSoup estão no nosso [guia de BeautifulSoup](/pt-br/blog/beautifulsoup-tutorial).

## 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](/pt-br/blog/selenium) e [Como usar proxy com SeleniumBase](/pt-br/blog/how-to-use-proxy-with-seleniumbase). Para saber se uma página realmente precisa de navegador, veja [Páginas estáticas e dinâmicas](/pt-br/blog/static-vs-dynamic-pages).

## 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](/pt-br/blog/concurrency-vs-parallelism).

## 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. Converter esses campos em valores do tipo certo é uma etapa à parte, explicada em [O que é parsing de dados? Tipos, erros e exemplos](/pt-br/blog/what-is-data-parsing).
- **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](/pt-br/blog/web-scraping-without-getting-blocked).

## 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](/pt-br/price-monitoring).
- **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](/pt-br/data-scraping).
- **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](https://proxynet.io/pt-br/rotating-proxy) para IPs de saída diferentes por um único endereço.

## Guia de decisão

| Sua necessidade | Recomendação |
|---|---|
| Selecionar por classe, id ou atributo | Seletor CSS |
| Você usa BeautifulSoup | Seletor CSS (sem suporte a XPath) |
| Encontrar um elemento pelo texto | XPath |
| Ir de um elemento ao pai ou ao irmão anterior | XPath |
| Tabelas rótulo-valor e listas de definição | XPath |
| Retornar diretamente um valor de atributo | XPath `/@attr` ou parsel `::attr()` |
| Os dois no mesmo projeto | parsel |
| Navegador headless | Locators do Playwright, CSS primeiro |
| Robustez contra mudanças de design | Atributos `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](/pt-br/proxy).
