NodeJS 在Puppeteer中重用浏览器和页面示例?

yxyvkwin  于 2023-05-06  发布在  Node.js
关注(0)|答案(2)|浏览(186)

出于性能和资源方面的原因,我尝试重用puppeteer浏览器和页面的同一个示例。我的代码结构如下:

async function createBrowser(id) {
    let bench = new Date().getTime();
    browser = await puppeteer.launch({headless:false, executablePath: browserPath});
    let pages = await browser.pages();
    let page = pages[0];
    let cookiesString = await fs.readFile(cookiePath);
    let cookies = JSON.parse(cookiesString);
    await page.setCookie(...cookies);
    console.log("\x1b[1m", `[${new Date().toLocaleTimeString()}] Browser opened successfully in ${(new Date().getTime() - bench)}ms.`, "\x1b[0m");
    worker(id, page);
}

我的工人/scraper逻辑:

async function worker(id, page) {
    console.log(page);
    let bench = new Date().getTime();
    let url = 'https://www.mypagetoscrape.xyz';
    let response = await page.goto(url, {waitUntil: 'networkidle0',});
    // further logic goes here

然后我第一次执行代码时,一切都很顺利,但是几分钟后,当我第二次调用worker函数时,我再也不能处理response了--尽管console.log(page)仍然打印整个页面对象。
例如,如果我想获取response.status(),则错误消息为TypeError: Cannot read properties of null (reading 'status')
有人能解释一下我做错了什么,以及如何正确地重用相同的浏览器和页面示例吗?关闭浏览器和页面,然后再重新打开它们(在执行worker逻辑之前),会延迟大约200-300毫秒的时间,并导致更多的CPU和RAM负载,所以我真的想避免它。
非常感谢!

hl0ma9xz

hl0ma9xz1#

根据Puppeteer goto()文档(截至最新版本v20.1.1),如果没有一些变通方法,您想要实现的结果是不可能实现的。
注意:后藤要么抛出错误,要么返回主资源响应。唯一的例外是导航到about:blank或导航到具有不同散列的相同URL,这将成功并返回null。
然而,这只是意味着await page.goto()响应将返回null,而不是导航本身无法工作。
只要.goto()调用被解析,您就可以简单地接受导航完成,然后继续运行您自己的逻辑。
如果您确实需要获取初始response对象,则应该使用不同的方法。在这个Github问题中有一些建议:https://github.com/puppeteer/puppeteer/issues/2479
其中一个似乎可以工作的是这个,它禁用该高速缓存,将默认导航超时设置为0,并通过在第一个page.waitForResponse()分辨率中获取响应来访问响应。

page.setCacheEnabled(false)
  await page.setDefaultNavigationTimeout(0);
  response = await page.goto(url, {waitUntil: "networkidle0"});

  if (response === null) {
    response = await page.waitForResponse(() => true);
  }
iswrvxsc

iswrvxsc2#

我不确定我是否完全理解了你的用例,我也无法重现你的错误,所以很难给出准确的建议。
一般来说,这里有一个简单的单例browser/page组合管理器:

const puppeteer = require("puppeteer"); // ^19.7.5

const pageManager = (opts => {
  const browser = puppeteer.launch(opts);
  let page;
  return {
    newPage: async () => {
      await page?.close();
      page = await (await browser).newPage();
      // any arbitrary fixed page initialization here, or add a callback
      return page;
    },
    page: () => page,
    close: async () => (await browser).close(),
  };
})({headless: true});

(async () => {
  const url = "https://www.example.com";
  const page = await pageManager.newPage();
  await page.goto(url, {waitUntil: "domcontentloaded"});
  const el = await page.$("::-p-text(Example)");
  console.log(await el.evaluate(el => el.textContent));
})()
  .catch(err => console.error(err))
  .finally(() => pageManager.close());

重要的是确保在发生错误时关闭浏览器。否则,您的脚本可能会永远挂起,耗尽资源并创建僵尸进程。
下面是另一种更直接的方法,但不提供重新启动页面的方法:

const browser = puppeteer.launch();
const page = browser.then(async browser => {
  const page = await browser.newPage();
  // any arbitrary fixed page initialization here, or add a callback
  return page;
});
const browserManager = { // the public interface
  page: () => page,
  close: async () => (await browser).close(),
};

(async () => {
  const url = "https://www.example.com";
  const page = await browserManager.page();
  await page.goto(url, {waitUntil: "domcontentloaded"});
  const el = await page.$("::-p-text(Example)");
  console.log(await el.evaluate(el => el.textContent));
})()
  .catch(err => console.error(err))
  .finally(() => browserManager.close());

希望所有的管理代码都可以隐藏在一个模块中。
其他方法是可能的。
如果您使用Express,请记住创建和销毁页面应该是相当轻量级的,并且您实际上不能与不同的客户端共享此单例页面。如果你真的不想创建新的页面,考虑一个页面池,路由处理程序可以为任务“租用”页面,然后将它们返回到池中。puppeteer-cluster可能是一个很好的选择。但我会避免过早的优化,我对你的实际用例进行了大量的推测。
注意networkidle0非常慢,所以如果你需要优化,我会先看看你的主线Puppeteer工作。

相关问题