javascript 为什么几乎所有的Puppeteer调用都是异步的?

k7fdbhmy  于 2023-11-15  发布在  Java
关注(0)|答案(2)|浏览(131)

我学习过JavaScript中的同步和异步,我将使用 puppet 人来制作一个爬行程序。
在Puppeteer中有许多爬网的代码示例。
但是,我有一个问题:为什么他们在基本的 puppet 人示例脚本中使用异步?
“难道人偶师不能用同步编程吗?是不是有什么我不知道的问题需要用异步编程?”
如果我不使用多线程(多爬网),它似乎没有用。

jjhzyzn0

jjhzyzn01#

对于初学者,我建议阅读Node.js中的单线程非阻塞IO模型是如何工作的。该线程激发了Node为实现并发而提供的回调和基于promise的模型。
每当Node进程需要访问进程外资源(如文件系统或网络套接字)时(如Puppeteer与其连接的浏览器通信),有两个选项:
1.阻塞整个进程并等待响应,就像fs.readFileSync一样。
1.使用promise或callback来获得响应通知并处理其他事情,就像fs.readFile(通过callback或fs.promises)和Puppeteer一样。
第一个选项是一个糟糕的选择,唯一的优点是更容易编写语法。阻塞线程等待资源就像订购比萨饼,然后什么也不做,直到比萨饼到达。你还不如在等待的时候看书或浇水。
从历史上看,回调最初是在Node中编写并发代码的唯一方法。最终,promise和then出现了,它们更好,但仍然带来了可读性负担。随着async/await的出现,编写读起来像同步代码的异步代码不再困难(尽管async范例并非没有自己的脚炮,特别是在错误处理方面).同步API(如fs__Sync函数)是异步API的别名,这是历史遗留的产物。page.waitForSelectorSyncpage.$evalSync
现在,可以理解Puppeteer的异步API在简单的直线脚本中毫无意义,因为您的Node进程在等待响应时没有任何其他事情要做,但是必须为每个调用输入await是API可用设计选项中最不邪恶的。
即使脚本是一个单一的直线代码序列,也不能简单地使用promise。没有await,操作/结果的顺序变得不确定,因为每个promise都是并发运行的,彼此独立。这种交错在顺序代码中是无意的,但在需要并发的情况下是一个有用的工具(例如,Express Web服务器调用Puppeteer作为处理请求的一部分),或者当您需要等待与操作顺序不一致的事件时,例如常见的“单击并等待导航”模式:

const navPromise = page.waitForNavigation(); // no await
await page.click("button");
await navPromise;

字符串
对于异步API的作者来说,几乎所有的调用都是访问网络资源,就像Puppeteer一样,选项是:
1.编写和维护两个版本的API,一个同步版本和一个异步版本。Playwright Python可以做到这一点,但这种模式比Node更适合Python架构(Playwright Node只有P2C API)。除了Node的核心fs模块,我想不出任何在Node中维护两个API的包的例子。
1.编写和维护一个同步的API仅仅是为了满足简单的用例,而代价是让库对任何关心并发性的人来说几乎是不可用的。显然,这是一个可怕的设计,就像强迫每个订购比萨饼的人(在上面的真实世界的例子中)在它到达之前什么都不做一样。
1.编写并维护一个异步API,并使那些不关心特定程序中并发性的客户端必须在所有调用之前编写await,即使他们没有利用并发性(或者是利用了,但没有意识到)。
顺便说一句,浏览器是一个独立的进程,这一事实往往会导致 puppet 师初学者的各种困惑。例如,数据被序列化和重新序列化的事实(转换为字符串)在每次调用page.evaluate时(和家族)意味着你不能在跨层传递像DOM节点这样的复杂结构,进程间隙。您无法从evaluate回调的主体访问在Node中定义的变量,除非将它们作为参数传递给evaluate调用,并且这些变量需要能够正确响应JSON.stringify()(也就是说,是可序列化的)。
就在这篇文章发表前13个小时,有人问node.js puppeteer "document is not defined"。他们试图访问Node内部的浏览器进程document对象。
如果你在Windows上,试着运行一个不关闭浏览器的简单Puppeteer Node脚本,然后看看你的任务管理器。在Linux上,你可以运行ps -a。你会看到有一个Chromium浏览器和一个Node进程。这两个进程通过一个套接字进行通信,其具有比帧内更高的延迟,每个Puppeteer调用都提供了一个并发的机会,如果Puppeteer的API是同步的,那么这个机会就会丢失。
理解进程间的差距对于Puppeteer的成功至关重要,因为它激发了API调用为什么是异步的,并有助于澄清哪个代码在哪个进程中执行。

ia2d9nvy

ia2d9nvy2#

async对于数据获取/抓取非常重要。你可以想象一下这种情况,你有一个元素是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. page.waitForSelector('.book-container')这里没有.book-container,所以它会尝试立即获取.book-container元素(当然,它不会在那里,因为页面可能由于一些网络问题而仍然在加载)
  3. page.waitForSelector('.book')类似地,它试图立即获取图书数据(即使book-container还没有在HTML中)
    为了解决这个问题,我们应该让JavaScript为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的数据响应)

相关问题