在 Puppeteer 中如何点击带有文本的元素?

142

有没有一种方法或解决方案可以点击带有文本的元素?我在API中找不到这样的方法。

例如,我有以下HTML:

<div class="elements">
    <button>Button text</button>
    <a href=#>Href text</a>
    <div>Div text</div>
</div>

我想要点击一个文本被包裹的元素(点击.elements内部的按钮),就像这样:

Page.click('Button text', '.elements')

2
在这里分享答案:https://dev59.com/1VYN5IYBdhLWcg3wNFp4#47829000 - Md. Abu Taher
你找到答案了吗? - Shubham Batra
如果有帮助的话,我想点击的页面已经加载了jQuery,所以我可以使用evaluate方法来执行jQuery代码。 - brandito
我不喜欢发表“先看我的答案”的评论,但是在 Puppeteer >= 18.0.0 版本中,前几个得到大量赞同的答案已经过时了。我的答案展示了比 XPath 更简单的方法。 - undefined
11个回答

184

简短回答

这个XPath表达式将查询一个包含文本“Button text”的按钮:

const [button] = await page.$x("//button[contains(., 'Button text')]");
if (button) {
    await button.click();
}

要同时尊重包围按钮的<div class="elements">,请使用以下代码:

const [button] = await page.$x("//div[@class='elements']/button[contains(., 'Button text')]");

解释

为了解释在某些情况下使用文本节点(text())是错误的,让我们看一个例子:

<div>
    <button>Start End</button>
    <button>Start <em>Middle</em> End</button>
</div>

首先,让我们来看一下使用contains(text(),'Text')的结果:

  • //button[contains(text(),'Start')]会返回两个节点(如预期)
  • //button[contains(text(),'End')]只会返回一个节点(第一个),因为text()返回一个包含两个文本(Start End)的列表,但是contains只会检查第一个文本
  • //button[contains(text(),'Middle')]不会返回任何结果,因为text()不包括子节点的文本

这里是适用于contains(.,'Text')的XPath表达式,它可以作用于元素本身及其子节点:

  • //button[contains(.,'Start')]将返回两个按钮
  • //button[contains(.,'End')]再次返回两个按钮
  • //button[contains(.,'Middle')]将返回一个按钮(最后一个按钮)

所以在大多数情况下,在XPath表达式中使用.而不是text()更有意义。


2
有没有一种适用于所有元素类型的方法?我无法确定文本是否在按钮、段落、div、span等元素内。 - Andrea Bisello
9
您可以使用 //*[...] 替代。 - Thomas Dondorf
如果“按钮文本”在数组中,这个能行吗? - user303749
似乎这个不起作用:字符串'//button[contains(., "Message")]'不是一个有效的XPath表达式。 - Jim

109

您可以使用XPath选择器以 page.$x(expression)

const linkHandlers = await page.$x("//a[contains(text(), 'Some text')]");

if (linkHandlers.length > 0) {
  await linkHandlers[0].click();
} else {
  throw new Error("Link not found");
}

查看此代码片段中的 clickByText,包含完整示例。它会处理引号转义,这在XPath表达式中有些棘手。


很棒 - 我尝试对其他标签进行操作,但无法使其工作。(li,h1,...)你会怎么做? - Rune Jeppesen
4
//a[contains替换为//*[contains,以选择任何元素,而非仅限锚点(a)元素。 - Unixmonkey

34

您还可以使用page.evaluate()来单击已通过文本内容筛选的document.querySelectorAll()获取的元素:

await page.evaluate(() => {
  [...document.querySelectorAll('.elements button')].find(element => element.textContent === 'Button text').click();
});

或者,您可以使用page.evaluate()通过document.evaluate()和相应的 XPath 表达式基于元素的文本内容来单击元素:

await page.evaluate(() => {
  const xpath = '//*[@class="elements"]//button[contains(text(), "Button text")]';
  const result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);

  result.iterateNext().click();
});

19

快速解决方案,使您能够使用高级CSS选择器,例如“:contains(text)”

因此,使用这个,您可以轻松地

const select = require ('puppeteer-select');

const element = await select(page).getElement('button:contains(Button text)');
await element.click()

当尝试使用以下代码时const el = await select(page).getElement('[data-testid="ContextualLayerRoot"] [role="menuitem"] div:contains("Instagram Feed")');,出现了“Evaluation failed: ReferenceError: Sizzle is not defined”的错误。该问题仍然在Github上保持开放状态。 - Sayed

7
解决方案是:
(await page.$$eval(selector, a => a
            .filter(a => a.textContent === 'target text')
))[0].click()

3
考虑将 filter(...)[0] 替换为 find(...) - ggorlen

7
Puppeteer 19.7.1新增了"p"(伪)选择器,因此text/已被::-p-text取代。例如:
const el = await page.waitForSelector("::-p-text(Button text)");

伪元素可以与CSS选择器结合使用,例如
const el = await page.$(".container button::-p-text(Button text)");

在 Puppeteer >= 18.0.0 中,选择器具有 text/ 前缀:
const el = await page.waitForSelector("text/Button text");

关于XPath的具体问题:

鉴于OP的用例似乎是在目标字符串"Button text"上进行精确匹配,<button>Button text</button>,使用text()似乎比不太精确的contains()方法更为正确。

尽管Thomas提出了一个很好的观点,即在存在子元素时使用contains可以避免假阴性,但使用text()可以避免当按钮是<button>Button text and more stuff</button>这样的情况时出现假阳性,这种情况同样可能发生。最好随手准备这两个工具,这样您就可以根据具体情况选择更合适的方法。

const xp = '//*[@class="elements"]//button[text()="Button text"]';
const [el] = await page.$x(xp);
await el?.click();

请注意,许多其他答案都忽略了需要使用.elements父类的要求。
另一个XPath函数是[normalize-space()="Button text"],它可以“从字符串中删除前导和尾随空格,将一系列空格字符替换为单个空格”,在某些情况下可能非常有用。
此外,通常很方便使用waitForXPath,它等待并返回与XPath匹配的元素,如果在指定的超时时间内找不到,则会抛出异常:
const xp = '//*[@class="elements"]//button[text()="Button text"]';
const el = await page.waitForXPath(xp);
await el.click();

5

以下是我的解决方案:

let selector = 'a';
    await page.$$eval(selector, anchors => {
        anchors.map(anchor => {
            if(anchor.textContent == 'target text') {
                anchor.click();
                return
            }
        })
    });

我会使用 find 或者 for .. of 循环,而不是在这里使用 map。在这种情况下,map 会为所有的 anchors 分配并丢弃一个 undefined 数组。此外,return 是误导性的:即使找到目标,map 也会继续执行。只有在你要对返回值进行操作时才使用 map - ggorlen

2

目前没有支持文本选择器或组合符号的 CSS 选择器语法,我的解决方案是:

await page.$$eval('selector', selectorMatched => {
    for(i in selectorMatched)
      if(selectorMatched[i].textContent === 'text string'){
          selectorMatched[i].click();
          break;//Remove this line (break statement) if you want to click on all matched elements otherwise the first element only is clicked  
        }
    });

为避免创建全局变量,应将 for(i in selectorMatched) 改为 for (const i in selectorMatched)。同时,避免使用 for .. in 循环数组。该语法适用于对象。相反,建议使用 for (const element of [...selectorMatched]) - ggorlen
@ggorlen 实际上应该是 for (const element of selectorMatched) {},不需要使用扩展操作符。更好的做法是将 selectorMatched 改为 elements,然后使用 elements.find(e => e.textContent === 'text string')?.click(),这样更符合函数式编程的风格。 - undefined

1

使用Puppeteer 12.0.1,以下方法适用于我:

await page.click("input[value='Opt1']"); //where value is an attribute of the element input
await page.waitForTimeout(1000);

await page.click("li[value='Nested choice 1']"); //where value is an attribute of the element li after clicking the previous option
await page.waitForTimeout(5000);

-1
我必须执行以下操作: await this.page.$eval(this.menuSelector, elem => elem.click());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接