为什么几乎所有的 Puppeteer 调用都是异步的?
Why are almost all Puppeteer calls asynchronous?
我在 JavaScript 学习过同步和异步。我打算用 Puppeteer 做一个爬虫程序。
Puppeteer中有很多爬行的代码示例。
但是,我有一个问题:为什么他们在基本的 Puppeteer 示例脚本中使用异步?
我不能在 Puppeteer 中使用同步编程吗?是否有我不知道的问题使得异步成为必要?
如果我不使用多线程(多爬)好像没什么用
async
对数据fetching/crawling非常重要。您可以想象这种情况,您有 1 个元素是 book-container
,但在 book-container
内,它将有 book
数据稍后在 UI 上通过 API 获取。
const scraperObject = {
url: 'http://book-store.com',
scraper(browser){
let page = browser.newPage();
page.goto(this.url);
page.waitForSelector('.book-container');
page.waitForSelector('.book');
//TODO: save book data after this
});
}
}
使用这个代码片段,它会运行像这样
page.goto(this.url)
进入特定URL 的页面
page.waitForSelector('.book-container')
这里没有异步,所以它会尝试立即获取 .book-container
元素(当然,它不会在那里,因为页面可能仍在加载 due某些网络问题)
page.waitForSelector('.book')
同样,它尝试立即获取书籍数据(即使 book-container
尚未进入 HTML)
为了解决这个问题,我们应该对 HTML.
中准备好的元素进行异步 WAIT
const scraperObject = {
url: 'http://book-store.com',
async scraper(browser){
let page = await browser.newPage();
await page.goto(this.url);
await page.waitForSelector('.book-container');
await page.waitForSelector('.book');
//TODO: save book data after this
});
}
}
用async/await
再解释一遍。
page.goto(this.url)
转到特定 URL 的页面并等待页面加载
page.waitForSelector('.book-container')
等待 .book-container
元素出现在 HTML
page.waitForSelector('.book')
等到.book
元素出现在HTML(可以理解为API的数据响应了)
对于初学者,我建议阅读 How the single threaded non blocking IO model works in Node.js。此线程激发了回调和 promise-based Node 为实现并发而提供的模型。
每当 Node 进程需要访问 out-of-process 资源,例如文件系统或网络套接字(就像 Puppeteer 与它所连接的浏览器进行通信一样)时,有两种选择:
- 像
fs.readFileSync
一样阻塞整个进程并等待响应。
- 使用 promise 或回调来获得响应通知并处理其他事情,如
fs.readFile
(通过回调或 fs.promises
)和 Puppeteer 所做的那样。
第一个选项是一个糟糕的选择,唯一的优点是更容易编写语法。阻塞线程等待资源就像订购比萨饼,然后在比萨饼到达之前什么都不做。等待的时候不妨看看书或给植物浇水。
从历史上看,回调最初是在 Node.js 中编写并发代码的唯一方法。最终,promises 和 then
出现了,它们更好,但仍然带来了可读性负担。随着 async
/await
的出现,编写读起来像同步代码的异步代码不再困难。像 fs
的 __Sync
别名异步 API 函数这样的同步 API 是历史产物。 Puppeteer 不提供 page.waitForSelectorSync
、page.$evalSync
等是正常的
现在,认为 Puppeteer 的异步 API 在一个简单的 straight-line 脚本中毫无意义是可以理解的,因为您的 Node 进程在等待响应时没有任何其他事情可做,但必须每次调用的类型 await
是 API.
可用设计选项中危害最小的
即使脚本是单个 straight-line 代码序列,也不 await
承诺不是一个选项。没有 await
,operations/results 的顺序变得不确定,因为每个承诺 运行 并发,独立于其他承诺。这种交错在顺序代码中是 ,但在需要并发的情况下是一个有用的工具。
对于几乎所有调用都访问外部资源的异步 API 的作者,就像 Puppeteer 的情况一样,选项是:
- 编写并维护 API 的两个版本,同步版本和异步版本。据我所知,没有图书馆这样做——这是一个主要的痛苦,几乎没有好处,而且有很大的滥用空间。
- 编写和维护一个同步的 API 只是为了满足简单的用例,代价是使库对于任何关心并发的人来说几乎无法使用。显然,这是一个糟糕的设计,就像强迫所有订购比萨饼的人(在上面的 real-world 示例中)在它到达之前什么都不做。
- 编写和维护一个异步API,让不关心特定程序并发的客户不得不在所有调用前面编写
await
。这就是 Puppeteer 所做的。
顺便说一下,浏览器处于一个单独的进程中这一事实往往会给 Puppeteer 初学者带来各种各样的困惑。例如,数据在每次调用 page.evaluate
(和 family)时被序列化和反序列化(转换为字符串)这一事实意味着您不能在 inter-process 差距。如果不将它们作为参数传递给 evaluate
调用,则无法从 evaluate
回调的主体访问在 Node 中定义的变量,并且这些变量需要能够正确响应 JSON.stringify()
(即可序列化)。
在此 post 前 13 小时,有人问 node.js puppeteer "document is not defined" -- 他们试图访问浏览器进程的 document
节点内部的对象。
如果您使用的是 Windows,请尝试 运行 运行一个不会关闭浏览器的简单 Puppeteer Node 脚本,然后查看您的任务管理器。在 Linux 上,您可以 运行 ps -a
。您会看到有一个 Chromium 浏览器和一个 Node 进程。这两个进程通过套接字进行通信,其延迟比 intra-process 通信高得多,并且涉及操作系统的网络堆栈。如果 Puppeteer 的 API 是同步的,那么每个 Puppeteer 调用都会提供并发机会。
了解 inter-process 差距对于 Puppeteer 的成功至关重要,因为它激发了为什么 API 调用是异步的,并有助于弄清哪个代码在哪个进程中执行。
我在 JavaScript 学习过同步和异步。我打算用 Puppeteer 做一个爬虫程序。
Puppeteer中有很多爬行的代码示例。
但是,我有一个问题:为什么他们在基本的 Puppeteer 示例脚本中使用异步?
我不能在 Puppeteer 中使用同步编程吗?是否有我不知道的问题使得异步成为必要?
如果我不使用多线程(多爬)好像没什么用
async
对数据fetching/crawling非常重要。您可以想象这种情况,您有 1 个元素是 book-container
,但在 book-container
内,它将有 book
数据稍后在 UI 上通过 API 获取。
const scraperObject = {
url: 'http://book-store.com',
scraper(browser){
let page = browser.newPage();
page.goto(this.url);
page.waitForSelector('.book-container');
page.waitForSelector('.book');
//TODO: save book data after this
});
}
}
使用这个代码片段,它会运行像这样
page.goto(this.url)
进入特定URL 的页面
page.waitForSelector('.book-container')
这里没有异步,所以它会尝试立即获取.book-container
元素(当然,它不会在那里,因为页面可能仍在加载due某些网络问题)page.waitForSelector('.book')
同样,它尝试立即获取书籍数据(即使book-container
尚未进入 HTML)
为了解决这个问题,我们应该对 HTML.
中准备好的元素进行异步 WAITconst scraperObject = {
url: 'http://book-store.com',
async scraper(browser){
let page = await browser.newPage();
await page.goto(this.url);
await page.waitForSelector('.book-container');
await page.waitForSelector('.book');
//TODO: save book data after this
});
}
}
用async/await
再解释一遍。
page.goto(this.url)
转到特定 URL 的页面并等待页面加载page.waitForSelector('.book-container')
等待.book-container
元素出现在 HTMLpage.waitForSelector('.book')
等到.book
元素出现在HTML(可以理解为API的数据响应了)
对于初学者,我建议阅读 How the single threaded non blocking IO model works in Node.js。此线程激发了回调和 promise-based Node 为实现并发而提供的模型。
每当 Node 进程需要访问 out-of-process 资源,例如文件系统或网络套接字(就像 Puppeteer 与它所连接的浏览器进行通信一样)时,有两种选择:
- 像
fs.readFileSync
一样阻塞整个进程并等待响应。 - 使用 promise 或回调来获得响应通知并处理其他事情,如
fs.readFile
(通过回调或fs.promises
)和 Puppeteer 所做的那样。
第一个选项是一个糟糕的选择,唯一的优点是更容易编写语法。阻塞线程等待资源就像订购比萨饼,然后在比萨饼到达之前什么都不做。等待的时候不妨看看书或给植物浇水。
从历史上看,回调最初是在 Node.js 中编写并发代码的唯一方法。最终,promises 和 then
出现了,它们更好,但仍然带来了可读性负担。随着 async
/await
的出现,编写读起来像同步代码的异步代码不再困难。像 fs
的 __Sync
别名异步 API 函数这样的同步 API 是历史产物。 Puppeteer 不提供 page.waitForSelectorSync
、page.$evalSync
等是正常的
现在,认为 Puppeteer 的异步 API 在一个简单的 straight-line 脚本中毫无意义是可以理解的,因为您的 Node 进程在等待响应时没有任何其他事情可做,但必须每次调用的类型 await
是 API.
即使脚本是单个 straight-line 代码序列,也不 await
承诺不是一个选项。没有 await
,operations/results 的顺序变得不确定,因为每个承诺 运行 并发,独立于其他承诺。这种交错在顺序代码中是
对于几乎所有调用都访问外部资源的异步 API 的作者,就像 Puppeteer 的情况一样,选项是:
- 编写并维护 API 的两个版本,同步版本和异步版本。据我所知,没有图书馆这样做——这是一个主要的痛苦,几乎没有好处,而且有很大的滥用空间。
- 编写和维护一个同步的 API 只是为了满足简单的用例,代价是使库对于任何关心并发的人来说几乎无法使用。显然,这是一个糟糕的设计,就像强迫所有订购比萨饼的人(在上面的 real-world 示例中)在它到达之前什么都不做。
- 编写和维护一个异步API,让不关心特定程序并发的客户不得不在所有调用前面编写
await
。这就是 Puppeteer 所做的。
顺便说一下,浏览器处于一个单独的进程中这一事实往往会给 Puppeteer 初学者带来各种各样的困惑。例如,数据在每次调用 page.evaluate
(和 family)时被序列化和反序列化(转换为字符串)这一事实意味着您不能在 inter-process 差距。如果不将它们作为参数传递给 evaluate
调用,则无法从 evaluate
回调的主体访问在 Node 中定义的变量,并且这些变量需要能够正确响应 JSON.stringify()
(即可序列化)。
在此 post 前 13 小时,有人问 node.js puppeteer "document is not defined" -- 他们试图访问浏览器进程的 document
节点内部的对象。
如果您使用的是 Windows,请尝试 运行 运行一个不会关闭浏览器的简单 Puppeteer Node 脚本,然后查看您的任务管理器。在 Linux 上,您可以 运行 ps -a
。您会看到有一个 Chromium 浏览器和一个 Node 进程。这两个进程通过套接字进行通信,其延迟比 intra-process 通信高得多,并且涉及操作系统的网络堆栈。如果 Puppeteer 的 API 是同步的,那么每个 Puppeteer 调用都会提供并发机会。
了解 inter-process 差距对于 Puppeteer 的成功至关重要,因为它激发了为什么 API 调用是异步的,并有助于弄清哪个代码在哪个进程中执行。