Geonode logo
Geonode Team

Geonode Team

Atualizado: 7 de outubro de 2026

Publicado: 2 de setembro de 2026

XPath por classe: guia com exemplos

A seleção por classe é a ação mais comum que se realiza com um seletor, e é aquilo que o XPath faz pior. A expressão que toda a gente escreve primeiro — `//div[@class='card']` — corresponde apenas aos elementos cujo atributo «class» seja exatamente essa cadeia de caracteres, pelo que não inclui `class="card featured"`. A correção óbvia, `contains(@class, 'card')`, corresponde também a `card-large` e `discard`. Nenhuma das duas está correta. Eis o que está correto, além de quando se deve utilizar um seletor CSS em vez disso.

A nossa posição é fácil de explicar: somos a Geonode e vendemos proxies, o que não tem nada a ver com a seleção de classes. A única observação relevante é que os seletores baseados em classes são os mais frágeis, e essa fragilidade parece, do ponto de vista externo, exatamente um problema de rede — o pedido é bem-sucedido, a página é carregada, mas a extração não devolve nada ou devolve algo errado. Se estiver a depurar um scraper que deixou de funcionar, verifique se os nomes das classes mudaram antes de verificar qualquer aspeto relacionado com a forma como a página foi obtida. Num site que utilize um pipeline de compilação moderno, estes podem mudar em cada implementação.

O problema com o atributo «class»

class contém uma lista de tokens separados por espaços, e o XPath não reconhece esse conceito de lista. Para ele, trata-se de uma única cadeia de caracteres.

<div class="card featured large">...</div>

Para o XPath, @class é a cadeia de caracteres "card featured large". Não existe uma forma integrada de perguntar «card é um dos tokens?», que é exatamente a questão a que um seletor CSS responde nativamente com .card.

Essa lacuna dá origem aos dois modos de falha.

A correspondência exata é demasiado restritiva:

//div[@class='card']

Corresponde apenas a class="card", sem mais nada e sem espaços em branco adicionais. Não encontra class="card featured", class="featured card" e class=" card ". Numa página real, não encontrará a maior parte do que pretendia.

A correspondência de subcadeias é demasiado flexível:

//div[contains(@class, 'card')]

Corresponde corretamente a class="card", mas também a class="card-large", class="postcard", class="discard" e class="card-footer". Numa página com qualquer nomenclatura relacionada, devolve um superconjunto do que pediu — e o seu código aceita o primeiro resultado, sem avisar.

A segunda falha é a mais perigosa, porque produz resultados. Algo é devolvido, parece razoável, mas é o elemento errado.

A expressão correta

A solução padrão preenche ambos os lados para que apenas tokens inteiros possam corresponder:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')]

Leia passo a passo:

**normalize-space(@class)

** remove os espaços em branco à esquerda e à direita e reduz as sequências internas de espaços em branco a um único espaço. " card featured "

torna-se "card featured"

.

**concat(' ', ..., ' ')

** envolve o resultado em espaços, resultando em " card featured "

. Agora, cada token está delimitado por um espaço em ambos os lados.

**contains(..., ' card ')

** procura o alvo rodeado por espaços.

Verifique isto em relação aos casos que falharam nas versões simplistas:

| Atributo da classe | Cadeia preenchida | Contém ' card '

? | |---|---|---| | card

| " card "

| Sim | | card featured

| " card featured "

| Sim | | featured card

| " featured card "

| Sim | | card-large

| " card-large "

| Não | | discard

| " discard "

| Não | | postcard footer

| " postcard footer "

| Não | | card

| " card "

| Sim |

Correto em todos os casos. O passo normalize-space()

não é meramente decorativo — sem ele, class="card featured"

com um espaço duplo produziria " card featured "

, que ainda contém " card "

e, por acaso, funciona, mas class="card\nfeatured"

com uma nova linha não funcionaria.

É prolixo. Envolva-o:

def has_class(name):
    return f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')"

tree.xpath(f"//div[{has_class('card')}]")
const hasClass = name =>
  `contains(concat(' ', normalize-space(@class), ' '), ' ${name} ')`;

Qualquer base de código que faça extração séria com XPath acaba por ter alguma versão deste auxiliar. Escrevê-lo uma vez é melhor do que errar o preenchimento numa expressão em cada vinte.

Várias classes

Combine com «and

»:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      and contains(concat(' ', normalize-space(@class), ' '), ' featured ')]

É aqui que a verbosidade se torna verdadeiramente incómoda — são 140 caracteres para expressar o que o CSS diz em div.card.featured

.

Para «qualquer uma das duas classes», utilize «or

»:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      or contains(concat(' ', normalize-space(@class), ' '), ' tile ')]

Para «tem esta classe, mas não aquela»:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      and not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]

No caso de duas ou mais condições de classe, considere seriamente se um seletor CSS seria adequado. div.card.featured:not(.hidden)

representa a mesma lógica com um quinto dos caracteres, e todas as bibliotecas de análise mais comuns suportam seletores CSS a par do XPath. Não existe nenhuma regra que o obrigue a escolher uma única linguagem para todo o ficheiro.

Nomes de classes gerados e com hash

A complicação moderna, que altera as recomendações.

Muitas ferramentas de compilação front-end geram nomes de classes com escopo para evitar colisões — CSS Modules, styled-components, várias bibliotecas de CSS-in-JS. O resultado fica assim:

<div class="ProductCard_container__3xK9p">...</div>
<div class="css-1x9dj2k">...</div>

A parte com o hash altera-se sempre que os estilos do componente mudam, o que, na prática, significa em muitas implementações. Um seletor ancorado ao nome completo deixa de funcionar sem aviso prévio.

Três formas de lidar com isto, por ordem de preferência.

Ancorar no prefixo estável, quando a ferramenta o produzir:

//div[starts-with(@class, 'ProductCard_container__')]

Os CSS Modules produzem, convencionalmente, ComponentName_elementName__hash

, pelo que a parte antes do duplo sublinhado final é estável entre compilações. Isto funciona bem quando se aplica.

**Procure, em vez disso, um atributo «data-*

».** Muitas aplicações incluem data-testid

, data-test

ou atributos semelhantes precisamente para que as ferramentas automatizadas tenham um ponto de fixação estável:

//div[@data-testid='product-card']

Se existir um, utilize-o. É mais estável do que qualquer nome de classe, porque foi escolhido deliberadamente em vez de gerado.

Baseie-se em algo completamente diferente. Estruture em relação a um título, conteúdo de texto ou um tipo de elemento. O //h2[normalize-space()='Featured']/following-sibling::div[1]

não se importa com os nomes das classes.

E no caso totalmente opaco — css-1x9dj2k

sem qualquer componente estável — a seleção baseada em classes simplesmente não é viável, e fingir o contrário resulta num scraper que avaria semanalmente. Procure dados estruturados na página ou a API que a própria página chama.

Ordem das classes e espaços em branco

Duas coisas que confundem as pessoas e sobre as quais vale a pena ser preciso.

A ordem das classes no atributo não tem significado. class="card featured" e class="featured card" são equivalentes para o navegador e para o CSS. A sintaxe «padded-concat» lida com ambas corretamente; a correspondência exata não lida com nenhuma delas de forma fiável. Se te vires a escrever uma expressão que dependa da ordem, isso é um sinal de que algo está errado.

Os espaços em branco podem ser qualquer coisa. Tabulações e novas linhas são separadores válidos num atributo de classe e aparecem em HTML formatado manualmente:

<div class="card
            featured">

normalize-space() agrupa tudo, e é por isso que a expressão o inclui. Uma expressão que utilize concat(' ', @class, ' ') sem normalização irá falhar nessa marcação, e a falha é invisível numa página renderizada.

As maiúsculas e minúsculas são importantes. Os nomes de classe distinguem maiúsculas de minúsculas em documentos HTML analisados como XHTML e são tratados sem distinção de maiúsculas e minúsculas no HTML em modo padrão para correspondência CSS — mas a comparação de cadeias de caracteres no XPath distingue sempre maiúsculas de minúsculas. Se uma página misturar Card e card , a expressão com preenchimento tratá-los-á como diferentes. O XPath 1.0 não possui a função lower-case() , pelo que a solução alternativa é translate() com caracteres alfabéticos explícitos; nesse caso, a expressão torna-se verdadeiramente ilegível e um seletor CSS é claramente a melhor escolha.

Obter o antepassado ou descendente de uma classe

A seleção de classes é, normalmente, um meio e não um fim. Existem dois padrões que abrangem a maior parte dos casos.

De uma classe para algo dentro dela:

//div[contains(concat(' ',normalize-space(@class),' '),' card ')]//span[@class='price']

Ou, em código que já contenha o elemento «card», utilize uma expressão relativa — sendo essencial o ponto inicial:

for card in tree.xpath(f"//div[{has_class('card')}]"):
    price = card.xpath(".//span[@class='price']/text()")

.//span

pesquisa dentro do elemento «card». //span

pesquisa todo o documento a partir da raiz, o que devolve o primeiro preço na página para cada elemento «card». Isto produz resultados uniformes, plausíveis, mas errados, e é um dos erros mais comuns no código de extração.

De um elemento até ao seu contentor:

//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]

O [1]

é importante: ancestor::

é um eixo inverso, pelo que a posição 1 corresponde ao antepassado correspondente mais próximo, em vez do mais externo. Sem ele, obtêm-se todos os antepassados correspondentes e, com contentores aninhados, isso raramente é o que se pretende.

Notas específicas sobre as bibliotecas

A sintaxe é a mesma em todo o lado; a API circundante não é, e algumas diferenças entre as bibliotecas causam confusão que poderia ser evitada.

lxml (Python). Suporta ambas as linguagens, e cssselect é a escolha mais prática para trabalhos em aula:

from lxml import html
tree = html.fromstring(source)

tree.cssselect('div.card.featured')                  # clear
tree.xpath(f"//div[{has_class('card')}]")            # when inside a larger expression

cssselect converte CSS para XPath internamente, pelo que as duas são equivalentes em termos de capacidade para os seletores que suportam. Note-se que se trata de um pacote separado do próprio lxml e que necessita de ser instalado.

BeautifulSoup (Python). Não tem qualquer suporte a XPath — um facto que surpreende quem vem de outros ecossistemas. Oferece select() para seletores CSS e a sua própria API find_all(class_='card') , que realiza a correspondência correta de tokens de forma nativa. Se precisar especificamente de XPath, terá de utilizar o lxml.

Selenium. Aceita tanto By.XPATH como By.CSS_SELECTOR , e utiliza o motor do navegador para cada um. Isso significa apenas XPath 1.0, e significa que o suporte a CSS corresponde ao que o navegador suporta — incluindo :has() .

Playwright. Deteta automaticamente o tipo de localizador a partir da string, pelo que tanto page.locator('div.card') como page.locator('//div[@id="x"]') funcionam sem prefixo. Também oferece localizadores baseados em texto — page.getByText() , page.getByRole() — que cobrem grande parte do que as pessoas anteriormente precisavam da correspondência de texto do XPath e são mais legíveis do que qualquer uma das duas linguagens.

Scrapy. Fornece .css() e .xpath() nos seletores, e estes encadeiam-se, o que é verdadeiramente útil:

for card in response.css('div.card'):
    price = card.xpath(".//dt[normalize-space()='Price']/following-sibling::dd[1]/text()").get()

CSS para a passagem estrutural, XPath para a pesquisa de rótulos, na mesma cadeia de expressões. Repare no . inicial no XPath — os seletores encadeados do Scrapy têm a mesma armadilha «raiz versus relativo» que tudo o resto.

Consola do navegador. $x('//div[@class="card"]') avalia o XPath nas ferramentas de desenvolvimento do Chrome e do Firefox, e document.querySelectorAll('div.card') trata do CSS. Vale a pena dedicar dez segundos a testar uma expressão aqui antes de a inserir no código, com a ressalva de que o DOM do navegador é pós-JavaScript e o do seu analisador não é — uma expressão que funciona na consola pode não encontrar nada no HTML bruto.

Quando utilizar CSS em vez disso

Dito de forma simples, porque, para esta tarefa específica, a resposta é normalmente sim.

Utilize CSS quando os seus critérios forem classes. div.card, div.card.featured, div.card:not(.hidden) — todas mais claras, mais curtas e corretas por predefinição. O CSS interpreta o atributo «class» como uma lista de tokens, que é exatamente o que falta ao XPath.

Utilize o XPath quando a classe for secundária e o verdadeiro critério for algo que o CSS não consiga expressar — sobretudo conteúdo de texto. //div[contains(@class,'card')][.//span[contains(., 'Sold out')]] necessita do XPath para a condição de texto, e a parte da classe vem por acréscimo.

Misture-os. Todas as bibliotecas de análise sintática mais comuns suportam ambas as opções. O lxml tem cssselect a par de xpath; o Selenium aceita ambas as estratégias de localização; o Playwright aceita ambas. Utilizar CSS para a seleção estrutural e XPath para as condições de texto não é um compromisso, é a combinação que produz o código mais curto e legível.

O único caso em que se recorre à seleção de classes com XPath é quando é necessário utilizá-la dentro de uma expressão XPath mais ampla e não é possível mudar de linguagem a meio da expressão. Essa é uma restrição real e é por isso que a expressão existe — mas é mais restrita do que a quantidade de correspondências de classes com XPath que se encontra na prática.

Perguntas frequentes

Como seleciono um elemento por classe no XPath?

Utilize //div[contains(concat(' ', normalize-space(@class), ' '), ' card ')]. O preenchimento garante que apenas tokens de classe completos correspondam. A expressão simples @class='card' não deteta elementos com classes adicionais, e contains(@class,'card') também corresponde a card-large e discard.

Por que é que contains(@class, 'name') corresponde aos elementos errados?

Porque se trata de um teste simples de subcadeia, sem noção de limites de palavras. O atributo «class» é uma lista separada por espaços, mas o XPath vê uma única cadeia de caracteres, pelo que contains(@class,'card') corresponde a qualquer nome de classe que contenha esses quatro caracteres em qualquer lugar.

Como seleciono um elemento com duas classes no XPath?

Combine duas condições «padded-concat» com «and». Funciona e tem cerca de 140 caracteres. Com duas ou mais condições de classe, um seletor CSS — div.card.featured — é significativamente mais claro, e a maioria das bibliotecas de análise permite a sua utilização.

A ordem das classes importa no XPath?

Não com a expressão «padded-concat», que corresponde a um token independentemente de onde este apareça no atributo. Importa, no entanto, na comparação exata, o que é uma das várias razões pelas quais @class='card featured' é uma má escolha. A ordem das classes não tem significado no HTML, pelo que um seletor que dependa dela é um erro à espera de acontecer.

Como lido com nomes de classe gerados aleatoriamente?

Ancore no prefixo estável com starts-with() quando a ferramenta o produzir, utilize um atributo data-testid se a aplicação o fornecer, ou ancore em algo completamente diferente, como texto ou estrutura. Para nomes hash totalmente opacos, sem qualquer parte estável, a seleção baseada em classes não é viável.

O que é melhor para a seleção por classe: o XPath ou o CSS?

O CSS, sem dúvida. Trata o atributo «class» como uma lista de tokens, que é o que ele é, pelo que «div.card» é mais curto e correto. O XPath necessita de uma expressão de setenta caracteres para expressar o mesmo. Utilize o XPath quando também precisar de algo que o CSS não consiga fazer, como, por exemplo, corresponder texto.

Como encontro um elemento pai por classe no XPath?

//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]. O [1] seleciona o antepassado correspondente mais próximo, uma vez que ancestor:: é um eixo inverso, em que a posição 1 significa o mais próximo, em vez do mais externo.

Por que razão o meu XPath relativo devolve o mesmo valor para todos os elementos?

Porque utilizou // em vez de .// dentro do ciclo. Um // no início da expressão pesquisa a partir da raiz do documento, independentemente do contexto, pelo que cada iteração encontra a primeira correspondência em toda a página. .// pesquisa dentro do elemento atual, que era o que pretendia.

Conclusão

É na seleção de classes que o XPath revela a sua idade. O atributo «class» é uma lista de tokens e o XPath não dispõe de operações com tokens; por isso, expressar «tem esta classe» requer preencher a cadeia de caracteres com espaços e procurar um token preenchido — uma expressão de setenta caracteres para o que o CSS diz em nove.

Aprenda esta expressão, porque vai precisar dela em expressões XPath mais complexas, e coloque-a numa função auxiliar para a escrever uma vez em vez de vinte vezes. O passo normalize-space() não é opcional: a marcação real contém linhas novas e tabulações nos atributos de classe, e uma expressão não normalizada falha com elas de forma imperceptível.

Mas a conclusão mais útil é aquela que evita essa expressão idiomática. Se os teus critérios de seleção forem classes e nada mais, escreve um seletor CSS. Todas as bibliotecas convencionais suportam ambos; misturá-los num único ficheiro é normal, e escolher a ferramenta certa para cada expressão produz código mais curto que um colega consegue ler.

E trata os nomes de classe como a âncora menos estável disponível. Os nomes gerados e com hash alteram-se na implementação; mesmo os escritos manualmente alteram-se sempre que alguém reformula o estilo de um componente. Um «data-testid», o texto de um título ou a relação de um elemento com algo identificável irão todos perdurar mais do que o nome da classe — e o modo de falha quando uma classe desaparece não é um erro, é uma saída silenciosa, bem formada e vazia.