Branding Engineer Tech Blog

株式会社Branding Engineerプロダクト部のブログになります。

Puppeteerを使ってスクレイピングサーバーを構築した話

Branding Engineerプロダクト部の鈴木です。

記念すべき第一回目の記事では、スクレイピングWebサーバーを構築した話を書こうと思います。


[アジェンダ]

1、スクレイピングWebサーバーを構築した背景
2、使っている技術について
3、永続的に運用していくために

スクレイピングWebサーバーを構築した背景


弊社ではエンジニアと企業様がより良い関係を築けるように、
Web上で良質なアウトプットをするエンジニアを登録情報や記事の内容を元に
スコアリング、カルチャー分析を行っています。


その時のエンジニアのユーザー情報を取得する際に、
今回お話するスクレイピングWebサーバーを使っています。

dig-g.com


使っている技術について


Webサーバーには、Node.jsの軽量フレームワークであるExpressを、
スクレイピングには様々な機能があるGoogle ChromのPuppeteerを使用しています。


github.com



github.com



Puppeteerの特徴を列挙すると

  • Ajaxを使っているWebページの解析ができる
  • SPAの解析が簡単
  • スクリーンショットが簡単(PDFも可)
  • フォームのsubmitなどが簡単

などが挙げられます。


使い方は

(以下Node.js 8.12.0、npm 6.4.1で行なっています。)

// インストール
npm install puppeteer 
// 読み込み
const puppeteer = require('puppeteer');

// 基本的に非同期処理でおこなう
(async () => {
  const browser = await puppeteer.launch({headless: true, args: ['--lang=ja,en-US,en']});
  const page = await browser.newPage();
  await page.goto('https://b-engineer.co.jp/');
  // スクリーンショット
  await page.screenshot({path: 'logo_be_01.png'});

  await browser.close();
})();

上記の通りとても簡単です。


サーバーの構成は、

RailsアプリケーションからExpressサーバーにリクエストを飛ばすと、
ExpressサーバーがPuppeteerを使って各Webサイトをスクレイピングするようにしています。


アクセスがあれば予め定義したPuppeteerのメソッドを使い情報を取得するようにしてます。

// Expressにアクセスがあればhoge.getメソッドを実行する。
app.get('hoge/:username', async (req, res, next) => {
  const data = await hoge.get(req.params['username'], browser, page).catch((e) => {
    return {"error":true};
  });
  res.json(data);
});

永続的に運用していくために

このスクレイピングWebサーバーを運用していく中で、
Puppeteerがメモリを食いつぶしてしまい、Expressサーバーが落ちてしまうことが
ありました。

この状況に陥ったときチェックしたい項目が2つあります

①slabキャッシュの確認

1つめは、slabキャッシュの確認です。
slabキャッシュについての說明は今回省きますが、
Puppeteerをまわし続けるとslabが蓄積し続けることがあるので、
不用意に使わないように設定を変更する必要があります。

cat /proc/meminfo
sudo slabtop -o

↑でslabのdentry領域を確認してください。

もしメモリをたくさん使ってるプロセスがないのにメモリが使われている場合は

sudo su 
echo 2 >> /proc/sys/vm/drop_caches
*dentry領域を使わないように*
vi ~/.bashrc
export TMPDIR=/dev/shm

を行う必要があります。

参考:https://dev.classmethod.jp/cloud/aws/dentry-cache/

②プロセスを増やさないように

2つ目はプロセス管理についてです。(こちらのほうが重要です)
Puppeteerは正しく終了しないとプロセスがゾンビ化してしまうことがあります。

そのためグローバルブラウザを使い、
リクエストのたびにブラウザを立ち上げないようにする必要があります。

悪い例

こちらの例では、リクエストの度にPuppeteerをlaunchしているため、
処理が正常に終了しない場合徐々にプロセスが溜まっていきます。

さらにこの場合では、ログインしなければスクレイピングできないページを取得する場合に
何度もログインしてしまい処理が遅くなってしまいます。

app.get('hoge/:username', async (req, res, next) => {
  const data = await hoge.get(req.params['username']).catch((e) => {
    return {"error":true};
  });
  res.json(data);
});



module.exports = {
  get: async function(username) {
  
    // WARNING: このままだと何度もlaunchしてしまう。
    const browser = await puppeteer.launch({headless: true, args: ['--lang=ja,en-US,en']});
    const page = await browser.newPage();
    await page.goto('https://b-engineer.co.jp/');

    // ユーザーの基本情報を取得しレスポンスはJSON形式で返します
    const userInfo =  await page.evaluate(() => {
      const params = {};
      return params;
    });
    
    
    // closeしても、プロセスが残る場合があります。
    await browser.close();

    return userInfo;
  }
};

良い例

そのためブラウザを立ち上げるためには、グローバルブラウザを使うようにします。

以下のコードは少しトリッキーですがグローバルブラウザを使用するために、
●サーバーを非同期で起動する
●SIGTERMシグナルが送られたらプロセスをkillする
ようにしています。

(async() => {

  // 全てのリクエストをグローバルブラウザで対応する
  const browser = await puppeteer.launch({headless: true, args: ['--lang=ja,en-US,en']});
  const page = await browser.newPage();

  app.set('port', config.port);
  const server = http.createServer(app);

  server.listen(port, ()=> {
    console.log(`Server runnning at port ${port}(${env})`);
  })

  app.get('hoge/:username', async (req, res, next) => {
    const data = await hoge.get(req.params['username'], browser, page).catch((e) => {
      return {"error":true};
    });
    res.json(data);
  });

  // プロセスがkillされた時にブラウザを閉じるように
  process.on('SIGTERM', async () => {
    await browser.close();
    process.exit(0);
  });
})();

補足:

このスクレイピングWebサーバーはforeverを使って起動させていますが、
foreverのプロセス終了のシグナルはデフォルトだとSIGKILLなので(SIGKILL時に非同期処理はできない)
以下のようにシグナルを変更して起動させます。

forever start --killSignal=SIGTERM hoge.js




以上、簡単ではありますが弊社で作成したスクレイピングサーバーに関してお話させていただきました。
Puppeteerはとても便利だと思うので是非使ってみてくださいね。

github.com



最後になりますが、弊社では新たにエンジニア職を募集しています。
興味がある方は是非お話しましょう!


www.wantedly.com