Skip to content

Seletores: CSS e XPath

Um seletor é a string que você entrega ao tab.query() (e ao selector= nos modelos de extração) para apontar para um elemento. O Pydoll fala duas linguagens de seletor, CSS e XPath, e escolhe o motor certo para você: se a string começa com / ou ./, ela roda como XPath, caso contrário como um seletor CSS. Esta página ensina o suficiente das duas para encontrar qualquer coisa em uma página.

Você só precisa de seletores para o query(). O método find() recebe atributos simples no lugar (veja Encontrando elementos); recorra a um seletor quando quiser uma relação que o find() não consegue expressar.

Experimente: digite um seletor abaixo e os elementos correspondentes se acendem. Ele roda o mesmo querySelectorAll / XPath que o navegador roda, então o que corresponde aqui corresponde na sua automação.

import asyncio

from pydoll.browser.chromium import Chrome


async def main():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://en.wikipedia.org/wiki/Python_(programming_language)')

        # CSS: o título do artigo, pelo id dele
        title = await tab.query('#firstHeading')
        print(await title.text)

        # XPath: o primeiro link cujo href menciona python.org
        link = await tab.query("//a[contains(@href, 'python.org')]")
        print(link.get_attribute('href'))

asyncio.run(main())

As duas consultas acima passaram pela mesma chamada de query(). O Pydoll viu o // inicial na segunda e a tratou como XPath.

Quando usar cada um

Na maior parte do tempo o CSS é suficiente, e ele se lê de forma mais natural. Recorra ao XPath quando precisar de algo que o CSS não consegue fazer.

  • CSS seleciona por id, classe, tag, atributo e posição, e se move para baixo e para os lados pela página. É a linguagem mais curta e mais familiar.
  • XPath faz tudo isso e também corresponde a texto visível, sobe para cima até um pai ou ancestral, e expressa condições como "a linha que contém este texto". Se você precisa encontrar um elemento pelo texto dele ou navegar de um filho de volta para um contêiner, isso é trabalho de XPath.

Uma regra aproximada: comece no CSS, mude para XPath no momento em que você se pegar querendo dizer "o elemento cujo texto é X" ou "o pai de Y".

Referência de CSS

Os trechos abaixo assumem um tab já iniciado. Passe find_all=True para qualquer um deles para obter uma lista em vez da primeira correspondência.

Selecionar por id, classe e tag

await tab.query('div')             # o primeiro <div>
await tab.query('#username')       # elemento com id="username"
await tab.query('.submit-btn')     # o primeiro elemento com class="submit-btn"
await tab.query('.btn.primary')    # elemento com as duas classes
await tab.query('input')           # o primeiro <input>

Combinadores

Os combinadores descrevem relações entre elementos.

await tab.query('nav a')           # qualquer <a> dentro de um <nav>, em qualquer profundidade
await tab.query('nav > a')         # <a> que é filho direto de <nav>
await tab.query('h1 + p')          # <p> imediatamente após um <h1>
await tab.query('h1 ~ p')          # o primeiro <p> que segue um <h1> como irmão

Seletores de atributo

await tab.query('input[required]')            # tem o atributo
await tab.query("input[type='email']")        # atributo igual a um valor
await tab.query("a[href^='https://']")        # valor começa com
await tab.query("img[src$='.png']")           # valor termina com
await tab.query("a[href*='wikipedia']")       # valor contém

Pseudo-classes

As pseudo-classes selecionam por posição ou estado.

await tab.query('li:first-child')             # o primeiro <li> entre seus irmãos
await tab.query('li:nth-child(2)')            # o segundo <li>
await tab.query('tr:nth-child(odd)', find_all=True)  # toda linha ímpar
await tab.query('input:checked')              # um checkbox ou radio marcado
await tab.query('button:not([disabled])')     # um botão sem o atributo disabled

Referência de XPath

Caminhos

await tab.query('//div')           # qualquer <div>, em qualquer lugar
await tab.query('//nav/a')         # <a> que é filho direto de um <nav>
await tab.query('//nav//a')        # <a> em qualquer lugar dentro de um <nav>
await tab.query('(//div)[1]')      # o primeiro <div> no documento
await tab.query('//ul/li[last()]') # o último <li> em um <ul>

Corresponder por atributos e texto

É aqui que você precisa de XPath. O CSS não consegue selecionar por texto visível; o XPath consegue.

await tab.query("//input[@type='email']")            # atributo igual a
await tab.query("//input[@type='text' and @required]")  # duas condições
await tab.query("//button[text()='Submit']")         # texto exato
await tab.query("//p[contains(text(), 'welcome')]")  # texto parcial
await tab.query("//a[starts-with(@href, 'https://')]")  # atributo começa com

Normalize o texto antes de corresponder

O texto renderizado costuma carregar espaços em branco perdidos. //button[normalize-space(text())='Submit'] colapsa sequências de espaços e apara as pontas, então ele corresponde mesmo quando o HTML tem indentação irregular.

Eixos: mova-se em qualquer direção

Um eixo diz em qual direção viajar a partir do nó atual. Essa é a vantagem do XPath: você pode subir até um pai ou atravessar até um irmão, o que o CSS não consegue.

Eixo Direção Encontra
parent:: para cima o pai imediato
ancestor:: para cima qualquer ancestral, em qualquer profundidade
following-sibling:: para os lados irmãos depois deste nó
preceding-sibling:: para os lados irmãos antes deste nó
child:: para baixo filhos diretos
descendant:: para baixo qualquer descendente

Abreviações que você verá com frequência: //div/p é //div/child::p, @id é attribute::id, e .. é parent::node().

await tab.query("//input[@name='email']/parent::div")   # sobe até o div que envolve
await tab.query('//button/ancestor::form')              # sobe até o form que contém
await tab.query("//label[text()='Email:']/following-sibling::input")  # o input ao lado de um label

Exemplos trabalhados

Estes usam o formulário de exemplo abaixo. Ele mostra os padrões que você encontra com mais frequência em páginas reais: encontrar um elemento pelo texto ao lado dele, e caminhar de um controle até a linha dele.

<form id="signup">
  <div class="field">
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required>
    <span class="error" style="display:none;">Invalid email</span>
  </div>
  <div class="field">
    <input type="checkbox" id="newsletter" name="newsletter">
    <label for="newsletter">Subscribe to the newsletter</label>
  </div>
  <button type="submit">Save</button>
  <button type="button">Cancel</button>
</form>

Encontrar um input pelo label dele

Você conhece o texto do label, não o id do input. Encontre o label, depois dê um passo para o lado até o input:

email = await tab.query("//label[text()='Email:']/following-sibling::input")

Encontrar a mensagem de erro ao lado de um campo

error = await tab.query("//input[@id='email']/following-sibling::span[@class='error']")
if await error.is_visible():
    print('Email was rejected')

is_visible() informa se o elemento está de fato sendo exibido, o que importa aqui porque o span começa oculto.

Diferenciar os dois botões

O botão de envio é o que tem type='submit', então você nunca depende da posição dele:

save = await tab.query("button[type='submit']")          # o CSS já basta aqui
save = await tab.query("//button[text()='Save']")        # ou corresponda pelo texto do label

Ler o label de um checkbox

O atributo for amarra um label ao controle dele, então você pode pular direto para ele:

label = await tab.query("//label[@for='newsletter']")
print(await label.text)   # "Subscribe to the newsletter"

Caminhar de um controle até a linha dele

Em uma tabela, muitas vezes você tem um botão e quer a linha em que ele está. Consulte a partir do elemento com um XPath que sobe pela árvore:

delete = await tab.query("//tr[@data-product-id='101']//button[@class='delete']")

row = await delete.query('./ancestor::tr')
print(row.get_attribute('data-product-id'))   # "101", get_attribute não é aguardado com await

get_attribute() lê um valor de forma síncrona a partir do elemento que você já localizou, então ele não leva await.

Monte seletores a partir de variáveis

Quando o valor pelo qual você corresponde vem do seu programa, monte a string com uma f-string. Escape quaisquer aspas no valor para que elas não quebrem a expressão:

async def row_for(tab, product_name):
    safe = product_name.replace("'", "\\'")
    return await tab.query(f"//tr[td[text()='{safe}']]")


laptop_row = await row_for(tab, 'Laptop')

Mantenha os seletores estáveis

Escolha atributos que um redesign dificilmente vai mexer, e apoie-se na expressão mais simples que funciona.

# estável: nomes e ids sobrevivem a mudanças de layout
await tab.query('#signup')
await tab.query("[data-testid='save-button']")
await tab.query("input[name='email']")

# frágil: cadeias baseadas em posição quebram quando a marcação muda
await tab.query('div > div > div:nth-child(3) > input')

O CSS é marginalmente mais rápido que o XPath para buscas simples, mas a diferença é de milissegundos por consulta e raramente vale a pena otimizar. Escolha o seletor que se lê com clareza e sobrevive a mudanças de página.

Próximos passos