按类名收集元素,然后单击每个元素 - Puppeteer

Posted

技术标签:

【中文标题】按类名收集元素,然后单击每个元素 - Puppeteer【英文标题】:Collect elements by class name and then click each one - Puppeteer 【发布时间】:2018-07-18 08:08:48 【问题描述】:

使用 Puppeteer,我想获取页面上具有特定类名的所有元素,然后循环并单击每个元素。

使用 jQuery,我可以通过以下方式实现:

var elements = $("a.showGoals").toArray();

for (i = 0; i < elements.length; i++) 
  $(elements[i]).click();

我将如何使用 Puppeteer 实现这一目标?

更新

在下面尝试了 Chridam 的答案,但我无法让它发挥作用(尽管答案很有帮助,所以谢谢你在那里),所以我尝试了以下方法,这很有效:

 await page.evaluate(() => 
   let elements = $('a.showGoals').toArray();
   for (i = 0; i < elements.length; i++) 
     $(elements[i]).click();
   
);

【问题讨论】:

其实用jQuery你可以调用$("a.showGoals").toArray() 感谢 jQuery 提示 :-) 我已经根据我的问题对其进行了更新...对 puppeteer 有什么想法吗?谢谢 以下问题可能重复。看看上面的回复。 ***.com/questions/51782734/… Puppeteer find array elements in page and then click的可能重复 【参考方案1】:

for 循环与Array.map()/Array.forEach() 中迭代 puppeteer 异步方法

由于所有 puppeteer 方法都是异步的,因此我们如何迭代它们并不重要。我对最常用和最常用的选项进行了比较和评分。

为此,我创建了一个 React.Js 示例页面,其中包含许多 React 按钮 here(我称之为 Lot Of React Buttons)。这里 (1) 我们可以设置在页面上呈现多少个按钮; (2)我们可以通过点击激活黑色按钮变为绿色。我认为它与 OP 的用例相同,它也是浏览器自动化的一般情况(如果我们在页面上做某事,我们希望会发生一些事情)。 假设我们的用例是:

Scenario outline: click all the buttons with the same selector
  Given I have <no.> black buttons on the page
  When I click on all of them
  Then I should have <no.> green buttons on the page

有一种保守且相当极端的情况。点击 no. = 132 按钮并不是一项庞大的 CPU 任务,no. = 1320 可能需要一些时间。


我。数组.map

一般来说,如果我们只想在迭代中执行像elementHandle.click 这样的异步方法,但又不想返回一个新数组:使用Array.map 是一种不好的做法。 Map方法的执行要在所有的迭代器完全执行之前完成,因为数组迭代方法是同步执行迭代器的,而puppeteer方法,迭代器是:异步的。

代码示例

const elHandleArray = await page.$$('button')

elHandleArray.map(async el => 
  await el.click()
)

await page.screenshot( path: 'clicks_map.png' )
await browser.close()

专业

返回另一个数组 .map 方法内的并行执行 快

132 个按钮场景结果:❌

持续时间:891 毫秒

通过在 headful 模式下观察浏览器,它看起来可以正常工作,但如果我们检查 page.screenshot 何时发生:我们可以看到点击仍在进行中。这是因为默认情况下无法等待Array.map。脚本有足够的时间来解决所有元素上的所有点击,直到浏览器没有关闭,这只是运气。

1320 按钮场景结果:❌

持续时间:6868 毫秒

如果我们增加同一选择器的元素数量,我们将遇到以下错误: UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an htmlElement,因为我们已经到达await page.screenshot()await browser.close():异步点击仍在进行中,而浏览器已经关闭。


二。 Array.forEach

所有的迭代都将被执行,但 forEach 将在所有迭代完成之前返回,这在许多异步函数的情况下不是可取的行为。就 puppeteer 而言,它与 Array.map 非常相似,除了:Array.forEach 不返回新数组。

代码示例

const elHandleArray = await page.$$('button')

elHandleArray.forEach(async el => 
  await element.click()
)

await page.screenshot( path: 'clicks_foreach.png' )
await browser.close()

专业

.forEach 方法内的并行执行 快

132 个按钮场景结果:❌

持续时间:1058 毫秒

通过在 headful 模式下观察浏览器,它看起来可以正常工作,但如果我们检查 page.screenshot 何时发生:我们可以看到点击仍在进行中。

1320 按钮场景结果:❌

持续时间:5111 毫秒

如果我们使用相同的选择器增加元素的数量,我们将遇到以下错误: UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement,因为我们已经到达 await page.screenshot()await browser.close():异步点击仍在进行中,而浏览器已经关闭。


三。 page.$$eval + forEach

性能最佳的解决方案是bside 的answer 的略微修改版本。 page.$$eval (page.$$eval(selector, pageFunction[, ...args])) 在页面内运行Array.from(document.querySelectorAll(selector)),并将其作为第一个参数传递给pageFunction。它用作 forEach 的包装器,因此可以完美地等待。

代码示例

await page.$$eval('button', elHandles => elHandles.forEach(el => el.click()))

await page.screenshot( path: 'clicks_eval_foreach.png' )
await browser.close()

专业

在 .forEach 方法中使用异步 puppeteer 方法没有副作用 .forEach 方法内的并行执行 极快

132 个按钮场景结果:✅

持续时间:711 毫秒

通过在 headful 模式下观察浏览器,我们可以看到效果是立竿见影的,而且只有在每个元素被点击、每个 promise 都被解决后才会截取屏幕截图。

1320 个按钮场景结果:✅

持续时间:3445 毫秒

就像 132 个按钮的情况一样,非常快。


四。 for...of 循环

最简单的选项,不是那么快并且按顺序执行。在循环未完成之前,脚本不会转到page.screenshot

代码示例

const elHandleArray = await page.$$('button')

for (const el of elHandleArray) 
  await el.click()


await page.screenshot( path: 'clicks_for_of.png' )
await browser.close()

专业

第一眼看到异步行为按预期工作 在循环内按顺序执行 慢

132 个按钮场景结果:✅

持续时间:2957 毫秒

通过在 headful 模式下观察浏览器,我们可以看到页面点击是按严格的顺序发生的,而且屏幕截图是在每个元素都被点击后才截取的。

1320 个按钮场景结果:✅

持续时间:25 396 毫秒

就像 132 个按钮的情况一样工作(但需要更多时间)。


总结

如果您只想执行异步事件并且不使用返回的数组,请避免使用Array.map,请改用 forEach 或 for-of。 ❌ Array.forEach 是一个选项,但您需要将其包装起来,以便下一个异步方法仅在所有承诺都在 forEach 中解决后才开始。 ❌ 如果异步事件的顺序在迭代中无关紧要,则将 Array.forEach$$eval 组合使用以获得最佳性能。 ✅ 如果速度不重要并且异步事件的顺序在迭代中很重要,请使用for/for...of 循环。 ✅

来源/推荐材料

Sebastien Chopin: javascript: async/await with forEach() (codeburst.io) Antonio Val: Making array iteration easy when using async/await (Medium) Using async/await with a forEach loop (***) Await with array foreach containing async await (***)

【讨论】:

【参考方案2】:

使用page.evaluate执行JS:

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => 
    const page = await browser.newPage();
    await page.evaluate(() => 
        let elements = document.getElementsByClassName('showGoals');
        for (let element of elements)
            element.click();
    );
    // browser.close();
);

【讨论】:

所以今天早上试试这个,点击事件似乎没有触发...... 查看我更新后的问题以及我为使其正常工作所做的工作,尽管您的回答应该做同样的事情对吗? 如果你在循环中插入console.log(element),你真的会记录元素吗?【参考方案3】:

要获取所有元素,您应该使用page.$$ 方法,这与常规浏览器API 中的[...document.querySelectorAll](在数组中传播)相同。

然后你可以遍历它(map、for、任何你喜欢的)并评估每个链接:

const getThemAll = await page.$$('a.showGoals')
getThemAll.forEach(async link => 
  await page.evaluate(() => link.click())
)

由于您还想对得到的东西执行操作,我建议使用page.$$eval,它会执行与上述相同的操作,然后运行评估函数,将数组中的每个元素放在一行中。例如:

await page.$$eval('a.showGoals', links => links.forEach(link => link.click()))

为了更好地解释上面的行,$$eval 返回一个链接数组,然后它以links 作为参数执行一个回调函数,然后它通过forEach 方法遍历每个链接,最后执行click各有作用。

也检查official documentation,那里有很好的例子。

【讨论】:

第一个解决方案不正确并且不起作用,因为外部箭头函数应该是 async 函数,因此 await 允许在里面.也应该等待link.click()。而在第二个(clickThemAll)解决方案中,link.click() 之前也缺少await。我会说在答案中解决这些问题,但主要问题是它暗示了一种不好的做法:这种情况不应该由array.map(I)地图 返回另一个具有相同长度的数组,以防点击我们不需要它。 (二)在map里面我们不能按顺序执行异步事件…… ... 如果序列很重要并且您希望 await 按预期工作,这可能会导致许多问题。使用for...of 或常规for 循环(即使forEach 也会导致相同的问题)。 已使用 [].forEach 而不是 [].map 修复。我同意 map 是不必要的,因为我们不需要返回任何东西。另外,我们为什么要担心事件的顺序?这不是 OP 的问题。 我明白了!很高兴知道,谢谢!另外,我认为你的回答不仅在这里而且作为木偶师的例子都会非常有帮助,因为我经常在各处找到像我的帖子这样的不同例子。 哦,你的帖子很大!这周我要读它。已经照顾好你的cmets了,谢谢!【参考方案4】:

page.$$() / elementHandle.click()

你可以使用page.$$()根据给定的选择器创建一个ElementHandle数组,然后你可以使用elementHandle.click()点击每个元素:

const elements = await page.$$('a.showGoals');

elements.forEach(async element => 
  await element.click();
);

注意:记得在async函数中点击await。否则,您将收到以下错误:

SyntaxError: await 仅在异步函数中有效

【讨论】:

以上是关于按类名收集元素,然后单击每个元素 - Puppeteer的主要内容,如果未能解决你的问题,请参考以下文章

Selenium 按类名查找元素

onClick事件更改类名反应js

如何从前一个元素的 NodeList 中获取所有类名?

如果类名是数字,则无法按类名获取元素

为每个具有类名的 div 动态创建按钮,并创建显示/隐藏单击功能

按特定类名选择元素