Voltar ao blogue
Guias
Mihai Maxim16 de dezembro de 20229 min de leitura

A melhor folha de dicas sobre XPath. Como escrever facilmente seletores poderosos.

A melhor folha de dicas sobre XPath. Como escrever facilmente seletores poderosos.

Uma folha de dicas sobre XPath?

Alguma vez precisou de escrever um seletor CSS que fosse independente da classe? Se a resposta for não, bem, pode considerar-se sortudo. Se a resposta for sim, então a nossa folha de dicas sobre XPath é o que precisa. A web está cheia de dados. Empresas inteiras dependem de juntar alguns deles para oferecer novos serviços ao mundo. As APIs são de grande utilidade, mas nem todos os sítios Web têm APIs abertas. Por vezes, terá de obter o que precisa da maneira antiga. Terá de construir um "scraper" para o sítio Web. Os sítios Web modernos contornam a recolha de dados renomeando as suas classes CSS. Como resultado, é melhor escrever selectores que dependam de algo mais estável. Neste artigo, aprenderá a escrever selectores com base na disposição dos nós DOM da página.

O que é o XPath e como é que o posso experimentar?

XPath é o acrónimo de XML Path Language. Utiliza uma notação de caminho (como nos URLs) para fornecer uma forma flexível de apontar para qualquer parte de um documento XML. 

O XPath é usado principalmente no XSLT, mas também pode ser usado como uma forma muito mais poderosa de navegar pelo DOM de qualquer documento de linguagem semelhante a XML usando XPathExpression, como HTML e SVG, em vez de depender dos métodos Document.getElementById() ou Document.querySelectorAll(), das propriedades Node.childNodes e de outros recursos do DOM Core. XPath | MDN (mozilla.org)

Uma notação de caminho?

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Nothing to see here</title>
</head>
<body>
    <h1>My First Heading</h1>
    <p>My first paragraph.</p>
    <div>
        <h2>My Second Heading</h2>
        <p>My second paragraph.</p>
        <div>
            <h3>My Third Heading</h3>
            <p>My third paragraph.</p>
        </div>
    </div>
</body>
</html>
Existem dois tipos de caminhos: relativo e absoluto
O caminho único (ou caminho absoluto) para O meu terceiro parágrafo é /html/body/div/div/p
O caminho relativo para o meu terceiro parágrafo é //body/div/div/p
Para o meu segundo título => //body/div/h2
Para o meu primeiro parágrafo => //body/p
Repara que estou a usar //body. Os caminhos relativos usam // para saltar diretamente para o elemento desejado.
The usage of //<path> also implies that it should look for all occurrences of <path> in the document, regardless of what came before <path>.

For example, //div/p returns both My second paragraph. and My third paragraph.

Pode testar este exemplo no seu browser para ter uma melhor visão geral!

Cole o código num ficheiro .html e abra-o com o seu browser. Abra as ferramentas de desenvolvimento e prima control + F. Cole o localizador XPath na pequena barra de entrada e prima enter.

Também pode obter o XPath de qualquer etiqueta clicando com o botão direito do rato na etiqueta no separador Elementos e selecionando "Copiar XPath"

Página HTML simples com títulos e o DevTools a destacar um elemento de parágrafo no DOM
Menu de contexto das ferramentas de desenvolvimento a apresentar a opção «Copiar XPath»
O DevTools apresenta um campo de introdução de expressões XPath para localizar elementos

Reparem como estou a alternar entre "O meu segundo parágrafo" e "O meu terceiro parágrafo".

Also, another important thing to know is that it is not necessary for a path to contain // in order to return multiple elements. Let's see what happens when I add another <p> in the last <div>.
Painel «Elementos» do DevTools com o campo de pesquisa «Procurar por cadeia de caracteres, seletor ou XPath»
/html/body/div/div/p já não é um caminho absoluto.

Se me seguiu até aqui, parabéns, está no caminho certo para dominar o XPath. Agora está pronto para mergulhar nas coisas divertidas.

Os suportes quadrados

Pode utilizar os parênteses rectos para selecionar elementos específicos.

O DevTools mostra uma consulta XPath que seleciona um parágrafo filho específico num exemplo de HTML
 In this case, //body/div/div[2]/p[3] only selects the last <p> tag.

Atributos

Também pode utilizar atributos para selecionar os seus elementos.

//body//p[@class="not-important"] => select all the <p> tags that are inside a <body> tag and have the "not-important" class.
//div[@id] => select all the <div> tags that have an id attribute.
//div[@class="p-children"][@id="important"]/p[3] => select the third <p> that is within a <div> tag that has both class="p-children" and id="important"
//div[@class="p-children" and @id="important"]/p[3] => same as above
//div[@class="p-children" or @id="important"]/p[3] => select the third <p> that is within a <div> that has class="p-children" or id="important"
Repare que @ marca o início de um atributo

Funções

O XPath fornece um conjunto de funções úteis que pode utilizar dentro dos parênteses rectos.

position() => returns the index of the element
Ex: //body/div[position()=1] selects the first <div> in the <body>
last() => returns the last element
Ex: //div/p[last()] selects all the last <p> children of all the <div> tags
count(element) => returns the number of elements
Ex: //body/count(div) returns the number of child <div> tags inside the <body> 
node() or * => returns any element
Ex: //div/node() and //div/*=> selects all the children of all the <div> tags
text() => returns the text of the element
Ex: //p/text() returns the text of all the <p> elements
concat(string1, string2) => junta string1 com string2
contains(@attribute, "value") => returns true if @attribute contains "value" 
Ex:
 //p[contains(text(),"I am the third child")] selects all the <p> tags that have the "I am the third child" text value.
starts-with(@attribute, "value") => devolve true se @attribute começar por "value" 
ends-with(@attribute, "value") => devolve true se @attribute terminar em "value" 
substring(@attribute,start_index,end_index)] => devolve a subcadeia do valor do atributo com base em dois valores de índice
Ex.:
//p[substring(text(),3,12)="sou o terceiro"] => devolve true se text() = "Sou o terceiro filho"
normalize-space() => funciona como text(), mas remove os espaços finais
Ex.: normalize-space("example") = "example"
string-length() => returns the length of the text
Ex: //p[string-length()=20] returns all the <p> tags that have the text length of 20

As funções podem ser um pouco complicadas de lembrar. Felizmente, The Ultimate Xpath Cheat Sheet fornece exemplos úteis:

//p[text()=concat(substring(//p[@class="not-important"]/text(),1,15), substring(text(),16,20))]
//p[text()=<expression_return_value>] will select all the <p> elements that have the text value equal to the return value of the condition.
//p[@class="not-important"]/text() returns the text values of all the <p> tags that have class="not-important".
If there is only one <p> tag that satisfies this condition, then we can pass the return_value to the substring function.
substring(valor_de_retorno,1,15) devolverá os primeiros 15 caracteres da cadeia de caracteres valor_de_retorno.
substring(text(),16,20) devolverá os últimos 5 caracteres do mesmo 
text() value that we used in //p[text()=<expression_return_value>].
Finally, concat() will merge the two substrings and create the return value of <expression_return_value>.

Aninhamento de trajectórias

O XPath suporta o aninhamento de caminhos. Isso é fixe, mas o que quero dizer exatamente com aninhamento de caminhos?

Vamos tentar algo novo: /html/body/div[./div[./p]]
You can read it as "Select all the <div> sons of the <body> that have a <div> child. Also, the children must also be parents to a <p> element."
If you don't care about the father of the <p> element, you can write: /html/body/div[.//p]
This now translates to "Select all the div children of the body that have a <p> descendant"
Página web de exemplo à esquerda e painel «Elementos» do DevTools à direita para navegar no DOM
Neste exemplo particular, /html/body/div[./div[./p]] e /html/body/div[.//p] produzem o mesmo resultado.
Por esta altura, tenho a certeza que está a perguntar-se o que se passa com aqueles pontos em ./ e .// 
O ponto representa o elemento self. Quando utilizado num par de parênteses, faz referência à etiqueta específica que os abriu. Vamos aprofundar um pouco mais.
In our example, /html/body/div returns two divs:
<div class="no-content"> and <div class="content">
/html/body/div[.//p] traduz-se por:

    /html/body/div[1][/html/body/div[1]//p]
e /html/body/div[2][/html/body/div[2]//p]
/html/body/div[2][/html/body/div[2]//p] é verdadeiro, por isso devolve /html/body/div[2] 
In our case, the dot ensures that /html/body/div and /html/body/div//p refer to the same <div>
Agora vamos ver o que teria acontecido se não tivesse acontecido.
/html/body/div[/html/body/div//p] would return both 
<div class="no-content">  and <div class="content">
Porquê? Porque /html/body/div//p é verdadeiro tanto para /html/body/div[1] como para /html/body/div[2].
/html/body/div[/html/body/div//p] actually translates to "Select all the div children of the <body> if /html/body/div//p is true. 
/html/body/div//p is true if the body has a <div> child, and that child has a <p> descendent". In our case, this statement is always true.

É uma pena que outras folhas de dicas do Xpath não mencionem nada sobre o aninhamento. Considero-o fantástico. Permite-lhe analisar o documento em busca de diferentes padrões e voltar para devolver outra coisa. A única desvantagem é que escrever consultas desta forma pode tornar-se difícil de seguir. A boa notícia é que existem outras formas de o fazer.

Os eixos

Pode utilizar eixos para localizar nós relativamente a outros nós de contexto.

Vamos explorar algumas delas.

Os quatro eixos principais

//p/ancestor::div => selects all the divs that are ancestors of <p>
How I read it: Get all the <p> tags, for each <p> look through its ancestors. If you find <div> tags, select them.
//p/parent::div => selects all the <div> tags that are parents of <p> 
How I read it: Get all the <p> tags and of all their parents, if the parent is a <div>, select it.
//div/child::p=> selects all the <p> tags that are children of <div> tags.
How I read it: Get all the <div> tags and their children, if the child is a <p>, select it.
//div/descendant::p => selects all the <p> tags that are descendants of <div> tags.
How I read it: Get all the <div> tags and their descendants, if the descendant is a <p>, select it.

Agora é altura de reescrever a expressão anterior:

/html/body/div[./div[./p]] é equivalente a /html/body/div/div/p/parent::div/parent::div
Mas /html/body/div[.//p] NÃO é equivalente a /html/body/div//p/ancestor::div
A boa notícia é que podemos ajustá-lo um pouco.
/html/body/div//p/ancestor::div[last()] é equivalente a /html/body/div[.//p]
Painel «Elements» do DevTools com uma barra de pesquisa para introduzir seletores ou expressões XPath

Outros eixos importantes

O DevTools apresenta os elementos irmãos no DOM com o campo de pesquisa XPath visível
//p/following-sibling::span => for each <p> tag, select its following <span> siblings.
//p/preceding-sibling::span => for each <p> tag, select its preceding <span> siblings.
O DevTools apresenta o separador «Ouvintes de eventos» com o seletor ou o campo de pesquisa XPath
//title/following::span => selects all the <span> tags that appear in the DOM after the <title>.
In our example, //title/following::span selects all the <span> tags in the document.
//p/preceding::div => selects all the <div> tags that appear in the DOM before any <p> tag. But it ignores ancestors, attribute nodes and namespace nodes.
In our case, //p/preceding::div only selects <div class="p-children"> and <div class="no_content">.
Most of the <p> tags are in <div class="content">, but this <div> is not selected because it is a common ancestor for them. As I mentioned, the 
preceding axe ignores ancestors.
<div class="p-children"> is selected because it is not an ancestor for the <p> tags inside <div class="p-children" id="important">

Resumo

Parabéns, conseguiu! Acrescentou uma nova ferramenta à sua caixa de ferramentas de seleção! Se estiver a criar um scraper web ou a automatizar testes web, esta ficha de referência do XPath vai dar-lhe jeito! Se procura uma forma mais simples de percorrer o DOM, está no sítio certo. Seja como for, vale a pena experimentar o XPath. Quem sabe, talvez descubra ainda mais casos de utilização para ele. O conceito de web scraping parece-lhe interessante? Pode contactar-nos aqui: WebScrapingAPI - Contacto. Se quiser fazer web scraping, teremos todo o prazer em ajudá-lo ao longo do processo. Entretanto, considere experimentar o WebScrapingAPI - Produto gratuitamente.

Sobre o autor
Mihai Maxim, Desenvolvedor Full Stack na WebScrapingAPI
Mihai MaximDesenvolvedor Full Stack

Mihai Maxim é um programador Full Stack na WebScrapingAPI, contribuindo em todas as áreas do produto e ajudando a criar ferramentas e funcionalidades fiáveis para a plataforma.

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.