我有一个 URL 数组可以从中抓取数据:
urls = ['url','url','url'...]
这就是我正在做的事情:
urls.map(async (url)=>{
await page.goto(url);
await page.waitForNavigation({ waitUntil: 'networkidle' });
})
这似乎不等待页面加载并很快访问所有 URL(我什至尝试使用
page.waitFor
)。
我想知道我是否做了一些根本错误的事情,或者不建议/支持这种类型的功能。
map
、forEach
、reduce
等,在继续进行迭代的迭代器的下一个元素之前,不会等待其中的异步操作。
在执行异步操作时,有多种方法可以同步遍历迭代器的每个项目,但在这种情况下,我认为最简单的方法是简单地使用普通的
for
运算符,它会等待操作完成。
const urls = [...]
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
await page.goto(`${url}`);
await page.waitForNavigation({ waitUntil: 'networkidle2' });
}
正如您所期望的,这将访问一个又一个的网址。如果您对使用await/async串行迭代感到好奇,您可以看一下这个答案:https://stackoverflow.com/a/24586168/791691
“接受的答案”显示了如何一次一个地连续访问每个页面。但是,当任务“令人尴尬地并行”时,您可能希望同时访问多个页面,也就是说,抓取特定页面不依赖于从其他页面提取的数据。
可以帮助实现这一目标的工具是Promise.allSettled
,它可以让我们立即发出一堆承诺,确定哪些是成功的并收获结果。
举一个基本的例子,假设我们想在给定一系列 id 的情况下抓取 Stack Overflow 用户的用户名。序列号:
const puppeteer = require("puppeteer"); // ^19.6.3
let browser;
(async () => {
browser = await puppeteer.launch();
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++) {
await page.goto(`${baseURL}/${i}`, {
waitUntil: "domcontentloaded"
});
const sel = ".flex--item.mb12.fs-headline2.lh-xs";
const el = await page.waitForSelector(sel);
usernames.push(await el.evaluate(el => el.textContent.trim()));
}
console.log(usernames);
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
并行代码:
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
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}`, {
waitUntil: "domcontentloaded"
});
const sel = ".flex--item.mb12.fs-headline2.lh-xs";
const el = await page.waitForSelector(sel);
const text = await el.evaluate(el => el.textContent.trim());
await page.close();
return text;
})))
.filter(e => e.status === "fulfilled")
.map(e => e.value);
console.log(usernames);
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
请记住,这是一种技术,而不是保证所有工作负载速度提高的灵丹妙药。需要进行一些实验才能在给定的特定任务和系统上创建更多页面的成本与网络请求的并行化之间找到最佳平衡。
这里的示例是人为设计的,因为它不与页面动态交互,因此没有像涉及网络请求和每页阻塞等待的典型 Puppeteer 用例那样大的增益空间。
当然,请注意网站施加的速率限制和任何其他限制(运行上面的代码可能会激怒 Stack Overflow 的速率限制器)。
对于为每个任务创建
page
成本过高的任务,或者您希望对并行请求分派设置上限,请考虑使用
任务队列或结合上面显示的串行和并行代码来分块发送请求。
这个答案展示了这个不可知论者的通用模式。 可以扩展这些模式来处理某些页面依赖于其他页面的数据的情况,形成依赖关系图。
参见这个答案,它说明了一种常见模式,在主页上抓取一系列链接,然后从每个子页面抓取数据。
另请参阅 在 forEach 循环中使用 async/await 这解释了为什么在该线程中使用
map
如果您发现自己无限期地等待您的承诺,建议的解决方案是使用以下方法:
其他人没有提到的是,如果您使用同一个页面对象获取多个页面,则将其超时设置为 0 至关重要。否则,一旦获取了默认 30 秒的页面,它将超时。