按类名收集元素,然后单击每个元素 - 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的主要内容,如果未能解决你的问题,请参考以下文章