XPath vs Seletores CSS: Qual Usar para Extração de Dados na Web
Advanced Data Extraction Specialist
TL;DR:
- CSS e XPath retornam resultados idênticos para extração comum: os mesmos vinte títulos de livros foram retornados de
article.product_pod h3 a::attr(title)e//article[@class='product_pod']/h3/a/@titlena mesma página. - XPath é o único dos dois que sobe a árvore.
parent::,ancestor::,preceding-sibling::efollowing-sibling::cada um correspondeu a 20 nós naquela página; CSS não tem equivalente para nenhum deles. - "CSS é mais rápido" é um fato do navegador, não um fato do Python. No parsel, a mesma extração custou 487,1 ms por CSS contra 309,1 ms por XPath em 2.000 iterações, porque o cssselect compila cada consulta CSS em XPath antes de executá-la.
a:contains('Sharp')retorna uma correspondência no parsel e jogaSyntaxErrordentro de uma página real do Chrome, razão pela qual um seletor pode funcionar em um scraper e falhar no DevTools.- Nenhuma das linguagens importa quando a marcação nunca chega; um seletor só pode consultar um DOM que foi realmente construído.
- Ambas as linguagens leem os bytes que sua camada de busca decodificou, então uma página servida sem um charset pode retornar
£47.82onde o navegador mostra£47.82. - Execute qualquer sintaxe contra páginas totalmente renderizadas no Scrapeless free plan.
Abra o inspetor em qualquer listagem de produto, copie o seletor que o Chrome oferece, cole-o em um scraper Python, e geralmente funciona. Os casos interessantes são aqueles onde não funciona — onde o seletor é válido em um mecanismo e um erro de sintaxe em outro, ou onde o CSS arrumado que você escreveu acaba sendo mais lento do que o XPath que você evitou.
As duas linguagens se sobrepõem amplamente, então a comparação útil está nas bordas: o que cada uma pode expressar, o que cada uma custa e qual motor está realmente executando a consulta.
Cada medição abaixo vem de uma página pública construída para prática de scraping — uma listagem de categoria de vinte livros em books.toscrape.com — analisada com lxml 6.0.2, cssselect 1.3.0 e parsel 1.10.0.
A Mesma Consulta em Ambas as Linguagens
Para os alvos comuns, as duas linguagens são uma tradução direta uma da outra. A tabela abaixo emparelha as consultas que selecionam conjuntos de nós idênticos na página de teste.
| Alvo | CSS | XPath |
|---|---|---|
| Qualquer tag | article |
//article |
| Classe | .product_pod |
//*[contains(concat(' ',normalize-space(@class),' '),' product_pod ')] |
| ID | #messages |
//*[@id='messages'] |
| Descendente | article h3 a |
//article//h3//a |
| Filho direto | div > span |
//div/span |
| Valor do atributo | a[title] |
//a[@title] |
| N-ésimo filho | li:nth-child(2) |
//li[2] |
| Texto do atributo | a::attr(href) |
//a/@href |
Três pares daquela tabela, executados na página ao vivo, retornaram conjuntos de nós correspondentes:
python
from parsel import Selector
import requests
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
sel = Selector(text=response.content.decode("utf-8"))
pairs = [
("article.product_pod h3 a::attr(href)", "//article[@class='product_pod']/h3/a/@href"),
("p.star-rating::attr(class)", "//p[contains(@class,'star-rating')]/@class"),
("div.image_container img::attr(alt)", "//div[@class='image_container']//img/@alt"),
]
for css_query, xpath_query in pairs:
css_hits = sel.css(css_query).getall()
xpath_hits = sel.xpath(xpath_query).getall()
print(len(css_hits), len(xpath_hits), css_hits == xpath_hits)
Cada par imprimiu 20 20 True. Onde ambas as linguagens podem expressar o alvo, a escolha é legibilidade, não capacidade.
O Que Apenas XPath Pode Expressar
CSS seleciona para baixo. Ele se move de um ancestral para um descendente e nunca volta, então uma regra pode dizer "o preço dentro deste cartão", mas não "o cartão contendo este preço".
XPath possui eixos, e quatro deles não têm equivalente CSS. Cada um correspondeu a 20 nós na página de teste:
python
axes = [
("parent::", "//p[@class='price_color']/parent::div/@class"),
("ancestor::", "//h3/ancestor::article/@class"),
("preceding-sibling::", "//div[@class='product_price']/preceding-sibling::h3/a/@title"),
("following-sibling::", "//h3/following-sibling::div[@class='product_price']//p[@class='price_color']/text()"),
]
for label, query in axes:
hits = sel.xpath(query).getall()
print(f"{label:20s} {len(hits):2d} hits {hits[0]!r}")
text
parent:: 20 hits 'product_price'
ancestor:: 20 hits 'product_pod'
preceding-sibling:: 20 hits 'Sharp Objects'
following-sibling:: 20 hits '£47.82'
Aquele último é o padrão que vale a pena manter. "Encontre o cabeçalho, então pegue o preço que o segue" é uma relação de irmãos, e expressá-la em CSS significa selecionar o preço separadamente e re- juntar as duas listas por índice — o que produz silenciosamente pares errados no momento em que um cartão está faltando um preço.
A especificação do W3C Selectors Level 4 define a gramática CSS, e a travessia para cima está ausente por design: a linguagem foi construída para estilização, onde um renderizador resolve regras de cima para baixo. A recomendação XPath 1.0 define treze eixos porque foi construída para endereçar nós arbitrários em uma árvore de documentos.
A Divisão :contains
O emparelhamento de texto é onde as duas linguagens divergem de uma maneira que produz relatórios de bug confusos.
XPath sempre teve contains(). CSS teve uma :contains() pseudo-classe em um rascunho inicial e ela foi descartada antes da estabilização da especificação. A questão é que o cssselect do Python ainda a implementa.
python
print(len(sel.css("a:contains('Sharp')").getall())) # 1
print(len(sel.xpath("//a[contains(., 'Sharp')]").getall())) # 1
Ambos imprimem 1. Agora as mesmas duas consultas dentro de uma página real do navegador, avaliadas através dos próprios mecanismos do DOM:
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
JS = """() => {
const out = {};
try { out.css_class = document.querySelectorAll('article.product_pod h3 a').length; }
catch (e) { out.css_class = 'ERR ' + e.name; }
try { out.css_contains = document.querySelectorAll("a:contains('Sharp')").length; }
catch (e) { out.css_contains = 'ERR ' + e.name + ': ' + e.message.slice(0, 60); }
const xp = (q) => document.evaluate(q, document, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
out.xpath_class = xp("//article[@class='product_pod']/h3/a");
out.xpath_contains = xp("//a[contains(., 'Sharp')]");
out.xpath_parent = xp("//p[@class='price_color']/parent::div");
out.xpath_ancestor = xp("//h3/ancestor::article");
return out;
}"""
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
for key, value in page.evaluate(JS).items():
print(f"{key:16s} {value}")
browser.close()
text
css_class 20
css_contains ERR SyntaxError: Failed to execute 'querySelectorAll' on 'Document': 'a:conta
xpath_class 20
xpath_contains 1
xpath_parent 20
xpath_ancestor 20
A correspondência de texto CSS lança um erro. A correspondência de texto XPath retorna seu um nó. Assim, um seletor que passa em testes em um scraper Python levanta um erro de sintaxe no momento em que alguém o cola no DevTools para checá-lo — e o inverso é verdade para quem valida seletores no console antes de enviá-los.
Duas coisas se destacam dessa execução que merecem ser notadas. O XPath do navegador não foi degradado: parent:: e ancestor:: resolveram cada um 20 nós através de a interface document.evaluate do DOM. E :contains é uma extensão de biblioteca em vez de um recurso de linguagem, então ele viaja apenas até onde a biblioteca vai.
Se um seletor precisar funcionar em ambos os lugares, //a[contains(., 'Sharp')] é a forma portátil.
Qual deles é realmente mais rápido
A resposta comum é que o CSS é mais rápido. Isso é verdade em um navegador, onde querySelectorAll é um caminho nativo rápido, e é reverso em Python.
No lxml, não há motor CSS. Cada consulta CSS é traduzida em XPath pelo cssselect e o motor XPath a executa, o que a documentação de seletores do Scrapy afirma diretamente. Escrever CSS compra o passo de tradução.
python
import time
N = 2000
start = time.perf_counter()
for _ in range(N):
sel.css("article.product_pod h3 a::attr(title)").getall()
css_ms = (time.perf_counter() - start) * 1000
start = time.perf_counter()
for _ in range(N):
sel.xpath("//article[@class='product_pod']/h3/a/@title").getall()
xpath_ms = (time.perf_counter() - start) * 1000
print(f"CSS {css_ms:8.1f} ms ({css_ms / N * 1000:6.1f} us/call)")
print(f"XPath {xpath_ms:8.1f} ms ({xpath_ms / N * 1000:6.1f} us/call)")
text
CSS 487.1 ms ( 243.5 us/call)
XPath 309.1 ms ( 154.5 us/call)
O custo do CSS é 1,58 vezes o tempo do XPath para uma saída idêntica. Imprimir a tradução mostra onde ela vai:
python
from cssselect import GenericTranslator
print(GenericTranslator().css_to_xpath("article.product_pod h3 a"))
text
descendant-or-self::article[@class and contains(concat(' ', normalize-space(@class), ' '), ' product_pod ')]/descendant-or-self::*/h3/descendant-or-self::*/a
O //article[@class='product_pod']/h3/a escrito à mão compara um atributo. A forma gerada normaliza espaços em branco e preenche a lista de classes em cada nó candidato, porque tem que estar correta para elementos que carregam várias classes. Esse predicado defensivo é o 1,58x.
Coloque isso em proporção antes de otimizar para isso: a diferença é de aproximadamente 89 microssegundos por chamada em uma página de 50 KB. Uma única requisição HTTP custa milhares de vezes mais. A linguagem do seletor não é onde o tempo de um scraper vai, e a legibilidade geralmente é o melhor comércio — o número importa apenas em um loop de parsing quente sobre HTML em cache.
A armadilha que ambas as linguagens compartilham
Um seletor retorna o que sua camada de obtenção decodificou. Quando um servidor envia text/html sem charset, as requisições retornam para ISO-8859-1 sob as regras de tipo de mídia da especificação de semântica HTTP, enquanto os bytes na rede são UTF-8:
python
import requests
from parsel import Selector
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
print(response.headers.get("content-type"))
print(response.encoding)
print(response.apparent_encoding)
print(Selector(text=response.text).css("p.price_color::text").get())
print(Selector(text=response.content.decode("utf-8")).css("p.price_color::text").get())
text
text/html
ISO-8859-1
utf-8
'£47.82'
'£47.82'
O seletor estava certo ambas as vezes. Decodificar response.content explicitamente, em vez de confiar em response.text, é o que torna o preço extraído utilizável — e nenhuma mudança na linguagem do seletor corrige isso.
Testar um seletor contra uma página que renderiza do lado do cliente precisa de um navegador real. O plano gratuito do Scrapeless cobre sessões suficientes para tentar ambas as sintaxes contra um DOM ao vivo.
Onde nenhuma das linguagens ajuda
Ambas as linguagens consultam um DOM. Nenhuma constrói um.
Quando um listagem renderiza do lado do cliente, o HTML que um cliente HTTP simples recebe contém a estrutura e não os registros, então um seletor correto retorna zero nós e parece um bug no seletor. A correção está antes do seletor: renderize a página primeiro e depois consulte-a. O Navegador de Scraping Scrapeless expõe um navegador em nuvem sobre CDP, então a mesma página que o navegador montou é a que seu seletor roda — que é exatamente como os números dentro do navegador acima foram capturados.
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
cdp_endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": 300,
"proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
titles = page.eval_on_selector_all(
"article.product_pod h3 a", "els => els.map(e => e.title)"
)
print(len(titles), titles[0])
browser.close()
text
20 Sharp Objects
Uma vez que o DOM existe, a questão do seletor volta a ser uma questão de estilo. Para um passeio pela camada de seleção em si, nosso guia de parsel cobre ambos os dialetos sobre uma árvore respaldada pelo lxml, e preços lista o que custa uma sessão de renderização.
Escolha CSS Quando, Escolha XPath Quando
| Situação | Busque por |
|---|---|
| Classe, id, atributo ou correspondência de descendente | CSS |
| O seletor é compartilhado com colegas de front-end | CSS |
A regra também deve funcionar em DevTools ou querySelectorAll |
CSS ou XPath portátil |
| Selecionando um recipiente pelo que ele contém | XPath |
| Emparelhando um rótulo com o valor que o segue | XPath |
| Correspondência no texto visível | XPath |
| Subindo até um ancestral | XPath |
| Analisando XML, RSS ou um sitemap | XPath |
| Um loop quente sobre HTML em cache no lxml | XPath |
Escolha CSS quando o alvo é acessível para baixo e a consulta será lida por pessoas que escrevem folhas de estilo. É mais curto e, na página de teste, selecionou todos os campos ordinários sem perda.
Escolha XPath quando a relação que você precisa é estrutural, em vez de hierárquica — subindo a árvore, atravessando irmãos ou indexada por texto. Também é o padrão honesto dentro do lxml e parsel, onde é o que realmente executa.
A maioria dos scraper em funcionamento mistura os dois por campo, em vez de escolher um lado, e parsel aceita ambos contra a mesma árvore, então a decisão é por seletor em vez de por projeto.
Conclusão
CSS e XPath respondem à mesma pergunta para os casos ordinais, e os vinte títulos retornaram idênticos de ambos. As diferenças que importam são mais estreitas do que a estrutura usual sugere: XPath pode percorrer para cima e corresponder texto, CSS não pode; CSS é mais rápido em um navegador e mais lento em lxml, onde compila para XPath primeiro; e :contains funciona em um mecanismo enquanto falha no outro, que é a fonte da maior parte da confusão "funciona no meu scraper, não no console".
Escolha por seletor, mantenha a forma portátil quando uma consulta precisa rodar em ambos os lugares e decodifique a resposta antes de culpar qualquer uma das linguagens por um caractere danificado.
Pronto para testar seletores contra páginas que renderizam antes de você consultá-las? Comece com o plano gratuito do Scrapeless e aponte qualquer sintaxe para um DOM ao vivo.
FAQ
Q: XPath é mais rápido do que seletores CSS?
Depende do mecanismo. No lxml e parsel, XPath é mais rápido porque CSS é compilado em XPath antes da execução — o intervalo medido aqui foi de 309,1 ms contra 487,1 ms em 2.000 iterações da mesma extração. Em um navegador, querySelectorAll é um caminho rápido nativo e CSS vence. De qualquer forma, a diferença é microssegundos por chamada, muito abaixo do custo da solicitação HTTP.
Q: Os seletores CSS podem corresponder ao texto do elemento?
Não em um navegador. document.querySelectorAll("a:contains('x')") lança um SyntaxError, porque :contains() foi descartado antes que a especificação Selectors se estabilizasse. O cssselect do Python ainda o implementa, então a mesma consulta funciona no parsel. Para um seletor que se comporta de forma idêntica em ambos, use //a[contains(., 'x')].
Q: Um seletor CSS pode selecionar um elemento pai?
Não. CSS seleciona apenas para baixo, então não há parent:: ou ancestor:: equivalente. XPath lida com ambos, e na página de teste //h3/ancestor::article correspondeu a todos os 20 cartões. A pseudo-classe :has() permite que você filtre um elemento por seus descendentes, o que cobre parte da mesma intenção, mas ainda retorna o elemento externo em vez de percorrer para cima a partir de um interno.
Q: Por que meu seletor funciona no DevTools, mas não retorna nada em Python?
Duas causas habituais. A página renderiza seu conteúdo com JavaScript, então o HTML que seu cliente HTTP recebeu nunca continha os nós que o navegador mais tarde construiu — o seletor está correto e o DOM não está lá. Ou o seletor usa uma extensão apenas para navegador ou apenas para biblioteca. Compare o corpo da resposta bruta com o que o inspetor mostra antes de mudar o seletor.
Q: Devo usar CSS ou XPath no Scrapy?
Ambos, por campo. O Scrapy expõe response.css() e response.xpath() sobre o mesmo seletor parsel, e eles podem ser encadeados juntos. Use CSS para correspondências de classe e atributo diretas e mude para XPath para correspondência de texto ou percurso para cima, em vez de contorcer uma regra CSS para se ajustar.
Q: Seletores XPath funcionam em todos os navegadores?
Sim, através de document.evaluate em vez de querySelectorAll. É uma interface DOM separada, e eixos são totalmente suportados — parent:: e ancestor:: retornaram cada um 20 nós na execução acima. O auxiliar $x() no Chrome DevTools envolve a mesma interface para uso no console.
Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.



