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

新手上路,请多包涵

使用 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();
   }
});

原文由 Richlewis 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 768
1 个回答

for 循环与 Array.map()/Array.forEach()

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

为此,我在 这里 创建了一个包含大量 React 按钮的 React.Js 示例页面(我将其称为 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 可能需要一些时间。


一、Array.map

一般来说,如果我们只想在迭代中执行像 elementHandle.click 这样的异步方法,但我们不想返回一个新数组:使用 Array.map 是一种不好的做法。 Map 方法执行将在所有迭代器完全执行之前完成,因为 Array 迭代方法同步执行迭代器,但是 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() :2ed—点击浏览器已经关闭时仍在进行中。


二。数组.forEach

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

代码示例

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

elHandleArray.forEach(async el => {
  await el.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回答 稍加修改的版本。 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 循环如果速度不重要并且异步事件的顺序在迭代中确实很重要。 ✅

来源/推荐材料

原文由 theDavidBarton 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题