为什么几乎所有的 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
        });
    }
}

使用这个代码片段,它会运行像这样

  1. page.goto(this.url) 进入特定URL
  2. 的页面
  3. page.waitForSelector('.book-container') 这里没有异步,所以它会尝试立即获取 .book-container 元素(当然,它不会在那里,因为页面可能仍在加载 due某些网络问题)
  4. 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再解释一遍。

  1. page.goto(this.url) 转到特定 URL 的页面并等待页面加载
  2. page.waitForSelector('.book-container') 等待 .book-container 元素出现在 HTML
  3. 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 与它所连接的浏览器进行通信一样)时,有两种选择:

  1. fs.readFileSync一样阻塞整个进程并等待响应。
  2. 使用 promise 或回调来获得响应通知并处理其他事情,如 fs.readFile(通过回调或 fs.promises)和 Puppeteer 所做的那样。

第一个选项是一个糟糕的选择,唯一的优点是更容易编写语法。阻塞线程等待资源就像订购比萨饼,然后在比萨饼到达之前什么都不做。等待的时候不妨看看书或给植物浇水。

从历史上看,回调最初是在 Node.js 中编写并发代码的唯一方法。最终,promises 和 then 出现了,它们更好,但仍然带来了可读性负担。随着 async/await 的出现,编写读起来像同步代码的异步代码不再困难。像 fs__Sync 别名异步 API 函数这样的同步 API 是历史产物。 Puppeteer 不提供 page.waitForSelectorSyncpage.$evalSync 等是正常的

现在,认为 Puppeteer 的异步 API 在一个简单的 straight-line 脚本中毫无意义是可以理解的,因为您的 Node 进程在等待响应时没有任何其他事情可做,但必须每次调用的类型 await 是 API.

可用设计选项中危害最小的

即使脚本是单个 straight-line 代码序列,也不 await 承诺不是一个选项。没有 await,operations/results 的顺序变得不确定,因为每个承诺 运行 并发,独立于其他承诺。这种交错在顺序代码中是 ,但在需要并发的情况下是一个有用的工具。

对于几乎所有调用都访问外部资源的异步 API 的作者,就像 Puppeteer 的情况一样,选项是:

  1. 编写并维护 API 的两个版本,同步版本和异步版本。据我所知,没有图书馆这样做——这是一个主要的痛苦,几乎没有好处,而且有很大的滥用空间。
  2. 编写和维护一个同步的 API 只是为了满足简单的用例,代价是使库对于任何关心并发的人来说几乎无法使用。显然,这是一个糟糕的设计,就像强迫所有订购比萨饼的人(在上面的 real-world 示例中)在它到达之前什么都不做。
  3. 编写和维护一个异步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 调用是异步的,并有助于弄清哪个代码在哪个进程中执行。