接受的答案显示了如何一次一个地连续访问每个页面。但是,当任务非常并行时,您可能希望同时访问多个页面,也就是说,抓取特定页面不依赖于从其他页面提取的数据。
一个可以帮助实现这一目标的工具是Promise.allSettled
让我们一次触发一堆 Promise,确定哪些是成功的并收获结果。
举一个基本的例子,假设我们想为给定一系列 id 的 Stack Overflow 用户抓取用户名。
串行码:
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({dumpio: false});
const [page] = await browser.pages();
const baseURL = "https://stackoverflow.com/users";
const startId = 6243352;
const qty = 5;
const usernames = [];
for (let i = startId; i < startId + qty; i++) {
try {
await page.goto(`${baseURL}/${i}`);
usernames.push(await page.$eval(
".profile-user--name",
el => el.children[0].innerText
));
}
catch (err) {}
}
console.log(usernames.length);
await browser.close();
})();
并行代码:
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({dumpio: false});
const baseURL = "https://stackoverflow.com/users";
const startId = 6243352;
const qty = 5;
const usernames = (await Promise.allSettled(
[...Array(qty)].map(async (_, i) => {
const page = await browser.newPage();
await page.goto(`${baseURL}/${i + startId}`);
return page.$eval(
".profile-user--name",
el => el.children[0].innerText
);
})))
.filter(e => e.status === "fulfilled")
.map(e => e.value)
;
console.log(usernames.length);
await browser.close();
})();
请记住,这是一种技术,而不是保证所有工作负载速度提高的灵丹妙药。需要进行一些实验才能在给定特定任务和系统上创建更多页面的成本与网络请求的并行化之间找到最佳平衡。
此处的示例是人为设计的,因为它没有与页面动态交互,因此没有像典型的 Puppeteer 用例那样有很大的收益空间,该用例涉及网络请求和每个页面的阻塞等待。
当然,请注意速率限制和站点施加的任何其他限制(运行上面的代码可能会激怒 Stack Overflow 的速率限制器)。
对于创建page
每个任务非常昂贵的任务,或者您想设置并行请求调度的上限,请考虑使用任务队列或结合上面显示的串行和并行代码以分块发送请求。这个答案显示了这个 Puppeteer 不可知论者的通用模式。
这些模式可以扩展以处理某些页面依赖于其他页面的数据的情况,形成依赖图。
另请参阅将 async/await 与 forEach 循环一起使用,它解释了为什么在此线程中使用的原始尝试map
无法等待每个承诺。