Uma nota sobre quem escreveu isto e porque é que isso pouco importa neste contexto: somos a Geonode e vendemos proxies a pessoas que realizam trabalhos de extração de dados. A escolha do seletor não tem qualquer influência sobre o que vendemos, e a única semelhança digna de menção é de natureza diagnóstica — um seletor que não corresponde a nada e um pedido que foi bloqueado parecem idênticos do ponto de vista externo, mas são problemas completamente distintos. Se a página foi carregada e o seu seletor não encontrou nada, esta referência é o local certo e nada sobre a forma como a página foi obtida precisa de ser alterado.
Seletores básicos
| Seletor | Corresponde a |
|---|---|
* | Todos os elementos |
div | Todos os elementos div |
.card | Elementos com a classe card |
#main | O elemento com o id main |
.card.featured | Elementos com ambas as classes |
div.card | Elementos div com a classe card |
div, p | Elementos div e elementos p |
Vale a pena debruçar-se sobre dois destes casos.
.card.featured sem espaço significa ambas as classes num único elemento. .card .featured com um espaço significa um elemento com a classe featured dentro de um elemento com a classe card. Um único carácter altera completamente o significado, e este é o erro ortográfico mais comum em toda esta área.
div, p é uma lista de seletores, não uma relação. Corresponde a tudo em qualquer um dos grupos, e cada parte é avaliada de forma independente — por isso, div, p.note corresponde a todas as divs e parágrafos com a classe note, e não às divs com a classe note.
Combinadores
Estes expressam relações entre elementos.
| Seletor | Correspondências |
|---|---|
div p | p em qualquer ponto dentro de um div (descendente) |
div > p | p que seja um filho direto de um div |
h2 + p | O p imediatamente a seguir a um h2 (irmão adjacente) |
h2 ~ p | Qualquer p após um h2 com o mesmo pai (irmão geral) |
O combinador de descendentes é um espaço, o que torna fácil introduzi-lo acidentalmente e fácil não o reparar durante a leitura.
> é mais preciso e, normalmente, mais robusto. div > p não irá corresponder a um parágrafo que alguém envolva num <section> no próximo mês — o que pode parecer uma desvantagem, mas muitas vezes é o contrário, porque um seletor que, silenciosamente, começa a corresponder a mais elementos do que o pretendido é pior do que um que deixa de corresponder e o avisa.
+ e ~ são os combinadores de irmãos e apenas procuram para a frente. Não existe um combinador de «irmão anterior» no CSS; :has() proporciona esse efeito indiretamente, conforme explicado abaixo.
Seletores de atributos
Frequentemente, a opção mais estável disponível, uma vez que os atributos são normalmente escolhidos de forma deliberada.
| Seletor | Corresponde a |
|---|---|
[href] | Elementos com um atributo «href» |
[href="/about"] | Valor exato |
[href^="/docs"] | Valor começa por |
[href$=".pdf"] | Valor termina em |
[href*="download"] | Valor contém |
[class~="card"] | O valor é uma lista separada por espaços que contém a palavra card |
[lang|="en"] | O valor é en ou começa por en- |
[data-state="open" i] | Correspondência sem distinção entre maiúsculas e minúsculas |
Dois deles são pouco utilizados.
[class~="card"] é exatamente equivalente a .card — o operador ~= faz a correspondência de palavras separadas por espaços, que é o que o atributo «class» necessita. Saber que existe é útil principalmente para compreender o que .card realmente faz e para construir seletores programaticamente quando se tem um nome de atributo numa variável.
O sinalizador i torna a correspondência insensível a maiúsculas e minúsculas, o que é verdadeiramente útil para valores de atributos cuja grafia varia ao longo de um site.
Especificamente para a extração, os atributos data-* merecem uma forte preferência em relação às classes. Um [data-testid="product-card"] foi escolhido por um programador para ser um ponto de fixação estável; um nome de classe pode ser regenerado por uma ferramenta de compilação em cada implementação.
Pseudoclasses: Estruturais
| Seletor | Correspondências |
|---|---|
:first-child | Um elemento que é o primeiro filho do seu pai |
:last-child | O último filho |
:only-child | Um elemento sem irmãos |
:nth-child(3) | O terceiro filho |
:nth-child(2n) | Filhos com números pares |
:nth-child(2n+1) | Filhos com números ímpares |
:nth-child(-n+3) | Os três primeiros |
:nth-last-child(2) | O segundo a contar do fim |
:first-of-type | O primeiro elemento do seu tipo entre os irmãos |
:nth-of-type(2) | O segundo do seu tipo |
:empty | Um elemento sem filhos, incluindo texto |
:root | A raiz do documento, normalmente html |
As variantes de «-of-type» são mais importantes do que as pessoas imaginam. p:first-child corresponde a um parágrafo apenas se este for o primeiro filho do seu pai — por isso, se um título vier primeiro, não corresponde a nada. p:first-of-type corresponde ao primeiro parágrafo, independentemente do que o preceda. Para extração, a segunda opção é quase sempre a que se pretende.
:nth-child(-n+3) para «os três primeiros» é a expressão idiomática que vale a pena memorizar da sintaxe An+B, juntamente com :nth-child(n+4) para «a partir do quarto». A combinação destas duas expressões permite definir um intervalo: :nth-child(n+2):nth-child(-n+5) seleciona os elementos filhos do segundo ao quinto.
Pseudoclasses: Lógicas
O grupo que mais mudou nos últimos anos.
| Seletor | Correspondências |
|---|---|
:not(.hidden) | Elementos sem a classe hidden |
:is(h1, h2, h3) | Qualquer um dos seletores listados |
:where(h1, h2, h3) | Igual a :is(), mas com especificidade zero |
:has(> img) | Elementos que contenham um descendente ou irmão correspondente |
:is() e :where() encurtam listas repetitivas. :is(article, section) > h2 substitui article > h2, section > h2, e a poupança acumula-se rapidamente com listas mais longas. A única diferença entre eles é a especificidade: :is() assume a especificidade do seu argumento mais específico, enquanto :where() contribui sempre com zero.
:not() aceita uma lista de seletores nos navegadores modernos, pelo que :not(.a, .b) funciona e significa «nenhum». O comportamento anterior, com um único argumento, exigia o encadeamento de :not(.a):not(.b).
:has() é o mais significativo. A documentação da MDN descreve-o como representando «um elemento se qualquer um dos seletores relativos que são passados como argumento corresponder a pelo menos um elemento quando ancorado em relação 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 é «Baseline amplamente disponível», sendo suportado em todos os navegadores desde dezembro de 2023.
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 */
label:has(input:checked) /* a label wrapping a checked input */
Três limitações documentadas: «não pode ser aninhado» dentro de outro :has(), os pseudo-elementos não são válidos no seu interior nem como âncoras para o mesmo, e a sua especificidade segue a mesma regra que :is() e :not() — o argumento mais específico.
Pseudoclasses: State e Form
| Seletor | Correspondências |
|---|---|
:hover, :focus, :active | Estados de interação |
:focus-visible | Foco que deve apresentar um anel visível |
:focus-within | Um elemento que contém um descendente com foco |
:checked | Caixa de seleção marcada, botão de opção ou opção selecionada |
:disabled, :enabled | Estado dos controlos do formulário |
:required, :optional | Atributos de validação do formulário |
:valid, :invalid | Estado de validação de restrições |
:placeholder-shown | Um campo de entrada a apresentar o seu placeholder |
:target | O elemento que corresponde ao fragmento da URL |
:visited, :link | Estados dos links |
A maioria destes são questões de estilo, em vez de questões de extração, com duas exceções. :checked é verdadeiramente útil para ler o estado de um formulário na automatização de testes, e :disabled indica se um controlo é interativo — o que frequentemente explica por que razão um clique não produziu qualquer efeito.
Note-se que :hover e :focus não podem ser consultados de forma útil com querySelectorAll num contexto de scraping, uma vez que nenhum desses estados existe num documento que tenha sido apenas analisado.
Especificidade, em resumo
É relevante quando os seletores competem pela aplicação de estilos e irrelevante para a realização de consultas — vale a pena saber, pois é comum haver confusão entre estes dois contextos.
A especificidade é calculada com base em três valores:
| Componente | Contribuição de |
|---|---|
| IDs | #main |
| Classes, atributos, pseudoclasses | .card, [href], :hover |
| Elementos, pseudoelementos | div, ::before |
Comparando da esquerda para a direita: qualquer número de classes nunca supera um único ID. Os estilos inline superam todos os outros, e !important supera esses, razão pela qual ambos são um último recurso e não uma ferramenta.
Duas notas sobre as práticas modernas. :where() não contribui com qualquer especificidade, o que o torna a escolha certa para valores predefinidos de bibliotecas que os utilizadores devem poder substituir sem dificuldades. E :is(), :not() e :has() assumem todos a especificidade do seu argumento mais específico, pelo que :is(#main, div) é tão específico quanto #main.
No caso de querySelectorAll e scraping, a especificidade não se aplica de todo. O seletor ou corresponde a um elemento ou não; não há qualquer competição.
Padrões de extração
Os seletores que realmente fazem o trabalho num scraper.
Fichas de produtos numa grelha:
div.product-grid > article.product-card
Links para PDFs:
a[href$=".pdf"]
Links externos:
a[href^="http"]:not([href*="example.com"])
Um gancho de teste estável em vez de uma classe:
[data-testid="price"]
Um contentor que contém algo:
tr:has(td.error)
article:has(img)
form:has(input:invalid)
O primeiro parágrafo de um artigo, independentemente do que o precede:
article p:first-of-type
Ignorar uma linha de cabeçalho:
tbody tr:not(:first-child)
Selecionar as linhas dois a onze:
tbody tr:nth-child(n+2):nth-child(-n+11)
Elementos com um token de classe específico, criados programaticamente:
[class~="card"]
Células vazias que pretende excluir:
td:not(:empty)
Escrever seletores que não deixam de funcionar
Uma referência indica o que está disponível. A escolha entre as opções é o que garante a durabilidade, e a diferença entre um seletor que sobrevive a uma reformulação e outro que deixa de funcionar na próxima semana reside inteiramente naquilo de que depende.
Classifique as suas âncoras de acordo com o grau de intencionalidade com que foram escolhidas. Um data-testid existe porque um programador o colocou lá para fins de automatização, pelo que é o elemento mais estável da página. Segue-se o id — geralmente intencional, ocasionalmente gerado. Um elemento semântico ou uma função ARIA é estável porque transmite significado. Um nome de classe escrito manualmente é moderadamente estável. Um nome de classe gerado, como css-1x9dj2k, muda em cada compilação e não vale nada. Os seletores posicionais são os menos estáveis de todos, porque codificam a forma exata da página num determinado momento.
Seja tão específico quanto o significado exigir e nada mais. body > div > div > div.content > p é preciso e frágil: qualquer elemento envolvente adicionado em qualquer ponto dessa cadeia quebra-o. .content p expressa o requisito real e sobrevive à reestruturação. O instinto de adicionar especificidade por segurança produz o oposto de segurança.
Prefira seletores que falhem de forma evidente. Um seletor que corresponda a zero elementos levanta um problema óbvio. Um seletor que corresponda a demasiados elementos devolve silenciosamente o primeiro, o que é plausível, mas possivelmente errado. Se tiver de escolher, opte pela versão que falhará em vez da versão que se desviará — div > p em vez de div p quando a relação for realmente direta.
Mantenha os seletores num único local. Quarenta cadeias de seletores espalhadas por uma base de código são quarenta riscos independentes. Reunidas num único módulo com nomes descritivos, tornam-se um mapa daquilo de que depende, e a atualização após uma alteração no site é um trabalho de uma hora, em vez de um dia de arqueologia.
Verifique a forma do que extrai. Este é o hábito que deteta desvios silenciosos. Se um preço tiver de corresponder a um padrão de moeda, verifique-o. Se uma lista nunca tiver tido menos de vinte itens, trate os casos com menos de vinte como um erro. Sem isto, um seletor que comece a corresponder ao elemento errado produz dados errados, mas bem formados, indefinidamente, e nada levanta uma exceção em nenhum momento.
Guarde o HTML que analisou. Quando um seletor deixa de funcionar, comparar a marcação antiga com a nova mostra-lhe exatamente o que mudou em segundos. Voltar a carregar a página para investigar é mais lento, consome largura de banda e pode apresentar-lhe uma página diferente daquela que realmente falhou.
O que o CSS ainda não consegue fazer
Uma lista curta, em que cada item constitui uma razão válida para recorrer ao XPath.
Correspondência com base no conteúdo de texto. Não existe um :contains() padrão. Foi proposto e abandonado, e nenhum navegador o implementa. Esta é a maior lacuna e a razão pela qual o padrão «encontrar um valor pela sua etiqueta» é domínio do XPath — //dt[normalize-space()='Price']/following-sibling::dd[1] não tem equivalente em CSS.
Navegar até um antepassado arbitrário. :has() permite a seleção condicional de pais — «o div que contém este span» — mas não existe um eixo de antepassados geral. Subir vários níveis até um contentor específico requer o XPath.
Funções de cadeia de caracteres. Sem corte, sem subcadeia, sem conversão de maiúsculas/minúsculas, sem concatenação. O que quer que corresponda, é processado posteriormente na sua linguagem de programação.
Consultar XML com namespaces. O CSS não tem suporte a namespaces digno de nota. Para mapas do site, RSS ou SOAP, utilize o XPath.
Selecionar irmãos anteriores diretamente. :has() proporciona o efeito — h2:has(+ p) seleciona o título — mas não existe um combinador - que espelhe +.
Esta é a lista completa para fins práticos. Tudo o resto que as pessoas recorrem ao XPath para fazer, o CSS agora faz, e geralmente de forma mais legível.
Diferenças entre ambientes que vale a pena verificar
A surpresa mais comum com os seletores é uma funcionalidade existir noutro local que não aquele onde a está a utilizar.
Os navegadores suportam tudo o que está aqui listado, incluindo :has(), :is(), :where() e :not() com vários argumentos.
Os analisadores do lado do servidor variam consideravelmente. O cssselect do Python, utilizado pelo lxml e pelo Scrapy, traduz CSS para XPath — o que funciona bem para os seletores clássicos e menos bem para os mais recentes. O suporte a :has(), em particular, não é universal, e um seletor que funciona na consola de um navegador pode gerar um erro ou, silenciosamente, não devolver nada num analisador.
O BeautifulSoup utiliza o soupsieve para o seu método select(), que tem uma cobertura boa, mas não idêntica, e não oferece qualquer suporte a XPath.
O Playwright e o Selenium utilizam o motor do navegador, pelo que o suporte corresponde ao do navegador — sendo que o Playwright oferece adicionalmente localizadores de texto e de função que abrangem grande parte do que o CSS não consegue expressar.
O hábito prático: teste os seletores no ambiente em que serão executados. A consola do navegador é conveniente, mas testa uma implementação diferente em relação a um DOM pós-JavaScript, o que são duas formas pelas quais pode induzir em erro.
Perguntas frequentes
Qual é a diferença entre .card.featured e .card .featured?
O primeiro, sem espaço, corresponde a um elemento que possua ambas as classes. O segundo, com um espaço, corresponde a um elemento com a classe featured dentro de um elemento com a classe card. Um único espaço altera completamente o significado e este é o erro mais comum nos seletores CSS.
Como seleciono um elemento pai no CSS?
Com :has() — div:has(> span.price) seleciona o div em vez do span. Está amplamente disponível em todos os navegadores desde dezembro de 2023. Não existe um seletor de antepassado geral, pelo que para aceder a um contentor específico vários níveis acima ainda é necessário utilizar o XPath.
Os seletores CSS podem corresponder a texto?
Não. Não existe uma pseudoclasse padrão «:contains()»; foi proposta, mas nunca adotada. A correspondência por conteúdo de texto requer o XPath ou um localizador específico de uma biblioteca, como o «getByText()» do Playwright.
Qual é a diferença entre :nth-child e :nth-of-type?
:nth-child conta todos os filhos do pai; :nth-of-type conta apenas os elementos do mesmo tipo. Assim, p:first-child não corresponde a nada se houver um título em primeiro lugar, enquanto p:first-of-type corresponde ao primeiro parágrafo independentemente disso.
O que faz :is() e em que difere de :where()?
Ambos aceitam uma lista de seletores e correspondem a qualquer um deles, encurtando seletores repetitivos. A única diferença é a especificidade: :is() adota a especificidade do seu argumento mais específico, enquanto :where() contribui sempre com zero — o que torna :where() ideal para valores por predefinição que devem ser fáceis de substituir.
Como funciona a especificidade do CSS?
Os IDs têm prioridade sobre as classes, os atributos e as pseudoclasses, que, por sua vez, têm prioridade sobre os seletores de elementos e pseudoelementos, quando comparados componente a componente. Os estilos inline têm prioridade sobre os seletores e o !important tem prioridade sobre estes. Nada disto se aplica quando se utilizam seletores para consultar, em vez de para aplicar estilos.
Os seletores CSS são melhores do que o XPath para a extração de dados?
Para a seleção por classe, id, atributo e estrutura, sim — são mais curtos e mais claros. Para corresponder conteúdo de texto ou subir até um antepassado arbitrário, o XPath é necessário. A maioria das bibliotecas de scraping suporta ambos, e é normal utilizar cada um quando for mais adequado.
Porque é que o meu seletor funciona no navegador, mas não no meu scraper?
Existem duas causas habituais. O DOM do navegador é pós-JavaScript, enquanto o seu analisador vê HTML bruto, pelo que o elemento pode não existir no que obteve. Além disso, as implementações de CSS do lado do servidor não abrangem todos os seletores modernos — em particular, :has() não é suportado de forma consistente fora dos navegadores.
Conclusão
O núcleo funcional dos seletores CSS é reduzido: tipo, classe e id para a identidade, os quatro combinadores para as relações, seletores de atributos para tudo o que é escolhido deliberadamente e um punhado de pseudoclasses para a posição e o estado.
Três hábitos melhoram os seletores reais mais do que o conhecimento de sintaxe adicional. Prefira > em vez de um espaço vazio quando a relação for genuinamente direta, porque um seletor que pára de corresponder é mais útil do que um que começa a corresponder em demasiados casos. Prefira data-* atributos em vez de nomes de classe, porque as classes são regeneradas por ferramentas de compilação e os atributos são escolhidos por pessoas. E prefira :nth-of-type em vez de :nth-child quando se referir ao «segundo parágrafo» em vez de «o segundo filho».
:has() é uma adição que vale a pena adotar deliberadamente. A seleção de pais e as condições de irmãos no CSS simples eliminam grande parte do que anteriormente obrigava as pessoas a recorrer ao XPath, e esta funcionalidade já está amplamente disponível há tempo suficiente para se poder confiar nela num navegador. Fora dos navegadores, verifique primeiro o seu analisador — é aí que residem as surpresas que ainda restam.
