O nosso interesse neste assunto é pequeno, mas vale a pena referi-lo: somos a Geonode e vendemos proxies a pessoas que realizam trabalhos de extração de dados, pelo que os seletores surgem constantemente nas conversas de apoio ao cliente. O que vale a pena referir antes da comparação é que nenhuma das opções influencia o facto de ser bloqueado ou não. Uma questão relacionada com o seletor e uma questão relacionada com a rede parecem idênticas à primeira vista — ambas resultam em «o meu scraper deixou de devolver dados» — mas não têm nada em comum. Se a página chegou intacta e a sua expressão não encontrou nada que correspondesse, trata-se de um problema do seletor e nenhuma decisão relativa à infraestrutura irá resolvê-lo.
A versão resumida
| CSS | XPath | |
|---|---|---|
| Selecionar por classe, id, atributo | Sim, de forma simples | Sim, de forma pouco prática |
| Relações de descendência e filiação | Sim | Sim |
| Selecionar por conteúdo de texto | Não | Sim |
| Navegar para o pai ou antepassado | Em parte, através de :has() | Sim, diretamente |
| Navegar para o irmão anterior | Em parte, através de :has() | Sim, diretamente |
| Seleção posicional | Família «:nth-child()» | «position()», «last()», predicados |
| Funções de cadeia de caracteres | Não | Sim |
| Consultar XML com namespaces | Não | Sim |
| Legibilidade | Melhor | Pior |
| Ferramentas e suporte do navegador | Universal | Universal para a versão 1.0 |
Duas linhas representam a maior parte do peso. O CSS não consegue selecionar com base no conteúdo de texto de todo, o que o exclui do padrão de extração mais comum — encontrar um valor pela etiqueta ao seu lado. E o XPath é mais difícil de ler, o que importa mais do que as pessoas admitem quando um colega tem de manter o seu código um ano depois.
O que o CSS faz melhor
A seleção de classes e atributos, e a diferença é enorme.
div.product-card
a[href^="https://"]
input[type="checkbox"]:checked
ul > li:first-child
section.content p:not(.footnote)
Os equivalentes em XPath são mais longos e, no caso das classes, verdadeiramente desagradáveis:
//div[contains(concat(' ', normalize-space(@class), ' '), ' product-card ')]
//a[starts-with(@href, 'https://')]
//ul/li[1]
O primeiro exemplo é a expressão típica para correspondência de classes, e existe porque @class
é uma única cadeia de caracteres separada por espaços e o XPath não reconhece tokens dentro dela. Um contains(@class, 'product-card')
simples também corresponde a product-card-large
e old-product-card
, pelo que a versão com espaços de preenchimento é a correta. Tem também quatro vezes o comprimento de div.product-card
e é consideravelmente mais difícil de analisar.
Se os teus critérios de seleção forem classes, IDs, atributos e relações estruturais, usa CSS. Isto abrange a grande maioria dos trabalhos reais de seleção, e escolher o XPath para isso significa pagar um custo em termos de legibilidade por funcionalidades que não está a utilizar.
O CSS também dispõe de melhores ferramentas. O inspetor de elementos de todos os navegadores produz seletores CSS de forma nativa, a maioria das estruturas de teste utiliza-os por predefinição e document.querySelectorAll
está disponível em todo o lado sem necessidade de ferramentas auxiliares.
O que o XPath faz melhor
Correspondência de texto, algo que o CSS não consegue fazer de todo:
//button[normalize-space()='Continue']
//a[contains(., 'Download')]
//dt[normalize-space()='Price']/following-sibling::dd[1]
Esse último padrão — encontrar a etiqueta e obter o valor adjacente — é a pedra angular da extração de páginas estruturadas, e não existe nenhuma expressão CSS para o efeito, porque o CSS não tem acesso ao conteúdo de texto. É precisamente esta capacidade que faz com que o XPath continue a ser utilizado na extração de bases de código que, de outra forma, utilizariam CSS em toda a sua extensão.
Navegação por antepassados:
//span[@class='price']/ancestor::div[contains(@class,'card')][1]
Subir a partir de um valor até ao contentor que o contém. :has()
oferece ao CSS uma forma de o fazer, com as limitações abordadas abaixo.
Lógica posicional relativa ao conteúdo:
//h2[normalize-space()='Specifications']/following-sibling::table[1]
A primeira tabela após um título específico. :nth-child()
conta posições entre elementos irmãos; não consegue expressar «após o elemento cujo texto é X».
Funções de cadeia de caracteres. normalize-space()
, substring-before()
, translate()
e as restantes permitem-lhe realizar operações dentro da expressão. normalize-space()
, em particular, é quase essencial, uma vez que o HTML real é formatado e uma comparação exata de texto com "\n In stock\n"
irá falhar.
XML com namespaces. Se estiver a consultar XML em vez de HTML — um mapa do site, um feed RSS, uma resposta SOAP —, o CSS não é a ferramenta adequada. O XPath foi concebido para isso, e o tratamento de namespaces faz parte da sua conceção.
Como o :has() mudou as coisas
A evolução mais significativa nesta comparação, e muitos artigos são anteriores a ela.
A documentação da MDN descreve o seletor :has() como representando «um elemento se qualquer um dos seletores relativos que são passados como argumento corresponder a pelo menos um elemento quando ancorado a este elemento», proporcionando «uma forma de selecionar um elemento pai ou um elemento irmão anterior em relação a um elemento de referência». O seu estado de referência é «amplamente disponível», sendo suportado em todos os navegadores desde dezembro de 2023.
Assim, o CSS pode agora expressar coisas que anteriormente não conseguia:
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
tr:has(td.error) /* a row containing an error cell */
Isto abrange uma parte significativa daquilo para que as pessoas precisavam anteriormente do XPath — seleção de elementos pai e condicionamento a um elemento irmão.
Estão documentadas três limitações que vale a pena conhecer. :has() «não pode ser aninhado dentro de outro :has()». Os pseudo-elementos «não são seletores válidos dentro de :has()» e não são âncoras válidas para o mesmo. E a sua especificidade é «a especificidade do seletor mais específico nos seus argumentos», correspondendo ao comportamento de :is() e :not().
O limite mais importante para a extração não consta dessa lista: :has() continua a não conseguir corresponder a texto. div:has(span) funciona; div:has(span:contains('Price')) não existe, porque :contains() não é um seletor CSS padrão. Foi proposto e abandonado, e os navegadores não o implementam. Assim, o padrão rótulo-valor permanece no domínio do XPath, independentemente de :has().
Traduções lado a lado
A forma mais rápida de compreender as vantagens e desvantagens é ver a mesma intenção expressa das duas formas.
| Intenção | CSS | XPath |
|---|---|---|
| Elemento com uma classe | div.card | //div[contains(concat(' ',normalize-space(@class),' '),' card ')] |
| Elemento com um id | #main | //*[@id='main'] |
| Atributo existe | a[href] | //a[@href] |
| Atributo começa por | a[href^="/docs"] | //a[starts-with(@href,'/docs')] |
| Atributo contém | a[href*="pdf"] | //a[contains(@href,'pdf')] |
| Filho direto | ul > li | //ul/li |
| Qualquer descendente | div p | //div//p |
| Primeiro filho | li:first-child | //li[1] |
| Último filho | li:last-child | //li[last()] |
| Enésimo filho | li:nth-child(3) | //li[3] |
| Próximo irmão | h2 + p | //h2/following-sibling::p[1] |
| Qualquer irmão posterior | h2 ~ p | //h2/following-sibling::p |
| Negação | p:not(.footnote) | //p[not(contains(@class,'footnote'))] |
| Pai de uma correspondência | div:has(> span.price) | //span[contains(@class,'price')]/.. |
| Contém texto | não é possível | //p[contains(., 'Price')] |
| Texto exato | não é possível | //p[normalize-space()='Price'] |
| Valor junto a um rótulo | não é possível | //dt[normalize-space()='Price']/following-sibling::dd[1] |
Ao ler esta tabela, o padrão é claro. Para tudo o que está acima da linha «pai», o CSS é mais curto e mais claro, e optar pelo XPath é optar pela verbosidade sem qualquer vantagem. Para as três linhas na parte inferior, não existe sequer uma coluna de CSS.
Duas traduções merecem uma segunda análise. A linha da classe apresenta o contraste mais acentuado em toda a comparação — nove caracteres contra cerca de setenta — e a versão em XPath não é excessiva por si só, porque a forma abreviada contains(@class,'card') corresponde, de facto, a mais do que card-large e discard. E a linha «primeiro filho» esconde uma armadilha: li:first-child e //li[1] coincidem aqui, mas //li[1] é um predicado aplicado por pai, pelo que seleciona o primeiro li em todas as listas do documento. O CSS é inequívoco; o XPath necessita de (//li)[1] se se pretender apenas um.
Um exemplo misto realista
Eis como isto se traduz na prática, ao extrair uma página de produto.
# CSS for the structural work — clear and adequate
cards = tree.cssselect('div.product-grid > article.product-card')
for card in cards:
name = card.cssselect('h3.product-name')[0].text_content().strip()
image = card.cssselect('img.product-image')[0].get('src')
# XPath for the label-value pairs, which CSS cannot express
price = card.xpath(
".//dt[normalize-space()='Price']/following-sibling::dd[1]"
)[0].text_content().strip()
stock = card.xpath(
".//span[contains(., 'in stock') or contains(., 'out of stock')]"
)
Há três aspetos que vale a pena copiar.
O «.» inicial em cada expressão XPath. «.//dt» pesquisa dentro do cartão atual; «//dt» pesquisaria todo o documento a partir da raiz, devolvendo o primeiro rótulo correspondente na página para cada cartão. Este é um dos erros mais comuns no código de extração mista e produz o mesmo valor para todos os registos — plausível, uniforme e errado.
CSS sempre que o CSS for suficiente. A grelha e a seleção de cartões, o título, a imagem. Escrever isso em XPath aumentaria o comprimento e diminuiria a clareza.
XPath apenas quando necessário. O preço é identificado pelo seu rótulo e o estado do stock pelo seu texto. Nenhum deles é expressável em CSS, e ambos são a razão pela qual o XPath se encontra no ficheiro.
O resultado é um scraper em que cada expressão é tão curta quanto possível, e um leitor consegue ver à primeira vista quais as partes que dependem da estrutura e quais as que dependem do conteúdo — o que também constitui um mapa do que irá deixar de funcionar quando o site for alterado.
Desempenho
Normalmente irrelevante, ocasionalmente decisivo.
O CSS é, em geral, mais rápido nos navegadores, porque os motores otimizam fortemente a correspondência de seletores — trata-se de uma etapa crítica no processo de renderização. O XPath segue um modelo de avaliação mais geral.
A diferença raramente é relevante na escala em que a maioria das pessoas trabalha. Selecionar a partir de uma única página algumas centenas de vezes é insignificante em ambos os casos; a solicitação de rede domina por ordens de magnitude.
Onde é que faz diferença:
Os eixos preceding e following são genuinamente dispendiosos. Eles percorrem todo o documento antes ou depois do nó de contexto. Dentro de um ciclo que abrange muitos nós, isso torna-se quadrático. Se uma expressão XPath for visivelmente lenta, verifique se utiliza algum desses — preceding-sibling e following-sibling estão limitados pelo número de nós irmãos e funcionam bem.
// no início de uma expressão aninhada volta a pesquisar a partir da raiz. //div//span é mais dispendioso do que parece, e .//span dentro de um ciclo sobre divs é o que normalmente se pretende.
:has() pode ser dispendioso em documentos de grande dimensão, uma vez que o motor tem de avaliar o seletor interno para cada candidato. Adequado para extração; algo a ter em conta numa folha de estilo aplicada a uma página de grande dimensão.
Para a análise do lado do servidor com lxml ou similar, a diferença é suficientemente pequena para que a legibilidade seja o fator decisivo.
Problemas de disponibilidade e versão
A armadilha que leva à situação em que «funciona no testador online, mas não no meu código».
Os navegadores implementam o XPath 1.0 através de document.evaluate, e o Selenium utiliza o motor do navegador. As funcionalidades do XPath 2.0 e 3.1 — matches(), replace(), lower-case(), ends-with(), sequências — não estão disponíveis nesse contexto. Um fragmento de código que utilize qualquer uma delas irá falhar.
O lxml implementa o XPath 1.0 na sua API comum, além de extensões EXSLT, incluindo re:test() para expressões regulares. Assim, uma expressão baseada em expressões regulares que funcione em Python não funcionará no Selenium.
O suporte a CSS nas bibliotecas de análise é normalmente feito através de uma camada de tradução que converte CSS para XPath internamente. Isso funciona bem para os seletores comuns e menos bem para os mais recentes — o suporte a :has() em analisadores do lado do servidor varia consideravelmente, e um seletor que funciona num navegador pode não funcionar no seu scraper.
Regra prática: teste os seus seletores no ambiente em que serão executados, e não na consola de um navegador ou numa ferramenta online. As duas surpresas mais comuns são as funções do XPath 2.0 que falham no Selenium e os seletores do tipo «:has()» que falham num analisador de Python.
Escolher na prática
Um procedimento de decisão que resolve praticamente todos os casos.
Comece com CSS. É mais legível, dispõe de melhores ferramentas e é adequado para a seleção por classe, id, atributo e estrutura — o que corresponde à maioria das seleções.
Mude para o XPath quando precisar de fazer a correspondência com base no texto. Esta é a principal razão e é decisiva: nenhuma construção CSS lê conteúdo de texto.
Mude para o XPath para navegação por antepassados para além do que o :has() abrange, particularmente quando precisar de um antepassado específico vários níveis acima, em vez de uma correspondência condicional num pai conhecido.
Mude para o XPath para XML. Os namespaces e as operações relacionadas com a ordem dos documentos são exatamente para isso que foi concebido.
Utilize ambos numa única base de código. Isto é normal e sensato, em vez de ser um compromisso. Um scraper que utilize CSS para os 90% mais simples e o XPath para os 10% mais complexos é mais fácil de manter do que um que se limite a apenas uma das opções.
E um hábito que vale mais do que qualquer uma das escolhas: verifique se existem dados estruturados incorporados antes mesmo de escrever um seletor. Muitas páginas contêm JSON-LD num bloco <script type="application/ld+json"> porque este alimenta funcionalidades de pesquisa, e a análise desses dados é significativamente mais estável do que a análise de marcação renderizada. Resiste a reformulações que quebram todos os seletores da página.
O que nenhum dos dois resolve
Ambos são frágeis da mesma forma, e é esta a parte que custa tempo real.
Os seletores estruturais falham silenciosamente. Alguém insere uma coluna e td[3] passa a corresponder a uma célula diferente. Não é gerado qualquer erro; os seus dados estão errados sem que se perceba. Baseie-se em texto ou identificadores sempre que possível e verifique a forma do que extrai — se um preço deve parecer um preço, verifique-o.
Nenhum deles lida com conteúdo renderizado pelo cliente. Se a marcação for montada por JavaScript após o carregamento, ambos não encontram nada no HTML inicial. Esse é um problema de obtenção de dados que requer um navegador ou a API subjacente, não um problema do seletor.
Nenhum deles sobrevive a uma reformulação por si só. A solução é manter os seletores num único local, para que a atualização após uma alteração demore uma hora em vez de um dia, e armazenar o HTML que analisou para poder comparar o antigo com o novo quando algo deixar de funcionar.
E nenhum deles é afetado pela forma como a página foi obtida. É aqui que entramos: se o documento estiver intacto e a sua expressão não corresponder a nada, nenhuma alteração na infraestrutura altera a resposta.
Perguntas frequentes
O XPath é melhor do que os seletores CSS?
Nenhum dos dois é melhor em geral. O XPath é mais versátil — consegue corresponder texto, navegar até antepassados e utilizar funções de cadeia de caracteres. O CSS é mais legível e tem melhor suporte por parte das ferramentas. Utilize o CSS por predefinição e o XPath para os casos específicos que o CSS não consegue expressar.
Os seletores CSS podem selecionar por texto?
Não. Não existe um seletor CSS padrão para conteúdo de texto — o seletor «:contains()» foi proposto, mas nunca foi adotado, e os navegadores não o implementam. Esta é a maior lacuna em termos de funcionalidades e a principal razão pela qual o XPath continua a ser utilizado em trabalhos de extração.
O :has() substitui o XPath?
Em parte. Permite a seleção de elementos pai e a correspondência condicional entre irmãos no CSS, funcionalidades que anteriormente só eram possíveis com o XPath. Não adiciona a correspondência de texto, pelo que o padrão «rótulo-valor» continua a necessitar do XPath. Além disso, não permite aninhamento e o suporte nas bibliotecas de análise do lado do servidor varia.
O que é mais rápido, o XPath ou o CSS?
O CSS é geralmente mais rápido nos navegadores porque a correspondência de seletores é otimizada para a renderização. A diferença é normalmente irrelevante quando comparada com o tempo de rede. Onde o XPath se torna verdadeiramente lento é nos eixos preceding e following, que percorrem todo o documento.
Posso utilizar o XPath 2.0 no Selenium?
Não. Os navegadores implementam o XPath 1.0 através de document.evaluate, e o Selenium utiliza o motor do navegador. Funções como matches(), lower-case() e ends-with() não estão disponíveis. Esta é a razão habitual pela qual um fragmento de código funciona num testador online e falha num conjunto de testes.
Como seleciono um elemento pai?
No XPath, parent:: ou ... No CSS, :has() fornece uma forma condicional — div:has(> span.price) seleciona o div em vez do span. Para um antepassado específico vários níveis acima, o eixo ancestor:: do XPath é mais direto.
Devo usar CSS ou XPath para web scraping?
Ambos, na mesma base de código. CSS para a seleção de classes, IDs e atributos, o que representa a maior parte. XPath quando for necessário corresponder texto — encontrar um valor pela sua etiqueta é o caso típico e não tem equivalente em CSS.
Qual é mais estável quando um site sofre alterações?
Nenhuma das duas, por natureza — a estabilidade depende do que se utiliza como âncora, e não da linguagem utilizada. A âncora em texto ou num atributo «data-*» resiste a uma reformulação; a âncora na posição não resiste a nada. Ambas as linguagens permitem fazer qualquer uma das opções.
Conclusão
A comparação resume-se a duas assimetrias. O CSS não consegue «ler» texto e o XPath é mais difícil de ler. Tudo o resto são pormenores.
Isso torna a regra de trabalho simples: opte por defeito pelo CSS, porque a maioria das seleções é feita por classe, id ou atributo e o CSS expressa isso de forma clara. Recorra ao XPath no momento específico em que precisar de algo que só ele oferece — acima de tudo, a correspondência de texto, seguida da navegação por antepassados e das funções de cadeias de caracteres. É normal misturá-los, e uma base de código que utilize cada um naquilo em que se destaca é mais fácil de manter do que uma que tenha escolhido apenas um lado. O
:has() reduziu significativamente a diferença e vale a pena adotá-lo onde for adequado: seleção de pais e condições de irmãos em CSS simples, amplamente disponível desde o final de 2023. Tenha apenas em atenção que não lida com texto e que o suporte dos analisadores do lado do servidor é menos uniforme do que o suporte dos navegadores.
Por isso, verifique o seu ambiente antes de confiar em qualquer expressão. O XPath 1.0 nos navegadores e no Selenium, os extras do EXSLT no lxml, o suporte variável a :has() nas bibliotecas de análise — a surpresa mais comum com os seletores não é um erro de sintaxe, mas sim uma funcionalidade que existe noutro local que não aquele onde a está a executar.
