Node.js
puppeteer

puppeteerを永続化したNode.jsアプリ内で安定稼働させる方法

概要

puppeteerを利用してスクレイピングする際、使用頻度が低ければ毎回puppeteerからchromeを起動すれば良いのですが、使用頻度が高かったり並列処理を行わせようとすると、起動する分だけchromeのプロセスが立ち上がる事になり、リソースを逼迫します。

また上記の問題により、一つのchromeを使い回すとしても、工夫をしないとページのプロセスが異常に溜まってしまったり、ページがクラッシュしてそれ以降そのページでスクレイピングができなくなるという事象が発生したりします。

上記、対策を重ね最近やっと安定稼働するようになったので、そのやり方を共有します。
細々と書いていますが、やっている事は問題があったら愚直に再起動させているというだけです。

puppeteerの起動

以下ログが多分に含まれていますが、単純に起動させるだけです。
初回起動させた後は後述するpage作成に失敗した時のみ起動させます。

//初回起動
(async() => {

  await launchPupepeteer();

})();

//一つのブラウザをグローバルで利用する
var browser;

//puppeteerの起動
async function launchPupepeteer(){

  console.log("puppeteerReLaunchCounter=>"+puppeteerReLaunchCounter);

  if(puppeteerReLaunchCounter > 5){
      console.log("Too many times tried launch puppeteer. Abnormal end.");
      process.exit(1)
  }

  console.log("launch puppeteer")
  browser = await puppeteer.launch(puppeteerLaunchOption);
  console.log("puppeteer is running PID=>"+browser.process().pid+" ENDPOINT=>"+browser.wsEndpoint())

  browser.on('disconnected', async () => {
      console.log("puppeteer disconnected. need relaunch");
  });
}

ページの取得部分

ページを取得します。ページが取得できなかった場合はpuppeteerを再起動します。
また、ページが必要以上に開かれた場合(以下の例だと5ページ)も再起動させます。
万一起動しないと永久ループしてしまう作りになっているので、防止用のカウンターを置いて最悪落とすようにしています。

//起動しない事による永久ループの防止用
var puppeteerReLaunchCounter=0;

async function getPage(){

  if(puppeteerReLaunchCounter > 3){
      console.log("Cannot launch puppeteer. Abnormal end.");
      process.exit(1)
  }

  try{
    var page = await browser.newPage();
    page.setDefaultNavigationTimeout(5000)
    puppeteerReLaunchCounter=0;

    var pages=await browser.pages();
    console.log("pages_count=>"+pages.length);
    if(pages.length > 5){
      throw new Error("Too many pages");
    }

  } catch (err) {
    puppeteerReLaunchCounter++;

    console.log("cannnot create page. try relaunch. err=>"+err.stack);
    await browser.close();
    await launchPupepeteer();

    var page = await getPage();
  }
  return page;
}

全体のソース

Webサーバーとして起動させていますが、実際はバッチとして利用しています。これで並列で4本走らせる事ができるpuppeteerを利用したアプリになります。
後は起動時にforeverで永続化しておけば、ずっと安定稼働するバッチとなるはずです。

const fs = require('fs');
const puppeteer = require('puppeteer');

const puppeteerLaunchOption={ args: ['--no-sandbox', '--disable-setuid-sandbox'],ignoreHTTPSErrors: true };

//一つのブラウザをグローバルで利用する
var browser;

//起動しない事による永久ループの防止用
var puppeteerReLaunchCounter=0;

//初回起動
(async() => {

  await launchPupepeteer();

})();

//ページの取得
async function getPage(){

  if(puppeteerReLaunchCounter > 3){
      console.log("Cannot launch puppeteer. Abnormal end.");
      process.exit(1)
  }

  try{
    var page = await browser.newPage();
    page.setDefaultNavigationTimeout(5000)
    puppeteerReLaunchCounter=0;

    var pages=await browser.pages();
    console.log("pages_count=>"+pages.length);
    if(pages.length > 5){
      throw new Error("Too many pages");
    }

  } catch (err) {
    puppeteerReLaunchCounter++;

    console.log("cannnot create page. try relaunch. err=>"+err.stack);
    await browser.close();
    await launchPupepeteer();

    var page = await getPage();
  }
  return page;
}

//puppeteerの起動
async function launchPupepeteer(){

  console.log("puppeteerReLaunchCounter=>"+puppeteerReLaunchCounter);

  console.log("launch puppeteer")
  browser = await puppeteer.launch(puppeteerLaunchOption);
  console.log("puppeteer is running PID=>"+browser.process().pid+" ENDPOINT=>"+browser.wsEndpoint())

  browser.on('disconnected', async () => {
      console.log("puppeteer disconnected. need relaunch");
  });
}

// httpモジュールを読み込み、インスタンスを生成
var http = require('http');

// HTTPサーバーのイベントハンドラを定義
http.createServer( function (req, res) {

  // ページの作成
  var page = await getPage();

  // puppeteerのPageを使ったバッチ処理

}).listen(3000, '127.0.0.1');