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/@srctext()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é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 comoproductsouold-product. A expressão longa da tabela só corresponde à classe exataproduct. - Linha 10: o XPath 1.0 não tem
ends-with(); os últimos caracteres são comparados comsubstring. O número é um a menos que o tamanho da extensão procurada (.webptem cinco caracteres,- 4). - Linha 11:
li:first-childeli:first-of-typesão diferentes. O primeiro seleciona umlique é 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 primeiropseguinte" e corresponde mesmo com outros elementos no meio. Oh2 + pdo CSS só corresponde se o elemento logo depois doh2for ump.
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:
//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:
//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:
//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:
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():
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:
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 //:
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,
querySelectorAllusa diretamente o mecanismo de seletores otimizado do navegador; o XPath comdocument.evaluatecostuma 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.parseroulxml) 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-1x9k2abousc-bdVaJapodem 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 "ospancom a classeprice" ou "a célula ao lado do título Estoque". - Prefira atributos estáveis. Atributos como
data-sku,data-testid,itempropearia-labelindependem 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 + Fno 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-testidna 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 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.




