Puppeteerを使ったスクレイピング

どうも、大曲です。

3月から子会社から本社に戻ってきました。
最近では開発より組織の整備!?とか何か良く分かんないコトやっています。
良い意味の何でも屋を目指せるように頑張ります。

今回はpuppeteerを使ったスクレピングが便利だったので紹介します。

やったコト

puppeteerを使って実現した処理は以下の通りです。

  • スクレイピング先のaタグのリンク取得
  • スクレイピング先で発生したリクエストの取得

全体のコードはGitHubにあります。
ブログでは書き方をいくつかピックアップして紹介します。

https://github.com/oomatomo/puppeteer-api

最近のスクレイピング事情(個人的な感想)

昔:HTMLペライチ(裏でPHPとかHTMLを生成していた)
今:AngularとかReactとかVue.js(ブラウザ側でDOMの構築..etc)

昔はHTMLさえ取得できれば、そこから必要な情報をフィルタリングして取得できました。
しかし、今はフロントエンドの技術向上に伴いブラウザ側でDOM構築するコトが多いため
HTMLを取得後にJavaScriptが実行されないと必要な情報の取得が難しくなってきています。

Puppeteerとは

https://github.com/GoogleChrome/puppeteer

ヘッドレスChromeを使うためのAPIを提供するライブラリです。
面白いなと思ったのが、Chromeの開発ツールのAPIまで用意されている点です。
開発ツールのConsoleやNetWorkまで使えるのはありがたい。

f:id:AdwaysEngineerBlog:20180628145118p:plain

サンプルコードは以下の通りです。楽でっせ。

const puppeteer = require('puppeteer');

(async () => {
  // ブラウザの起動
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // ページへのアクセス
  await page.goto('https://example.com');
  // スクショを取る
  await page.screenshot({path: 'example.png'});
  // リソースの解放
  await browser.close();
})();

あとはAPIの戻り値の多くが非同期処理のPromiseが返すので、活用すればエラーの管理も楽だなと思いました。
ScalaでFutureとかに慣れていれば活用できると思います。今回のコードではそんなに活用していません。
API一覧ページ

API化までの流れ

デバイスの指定(UserAgentの変更)

DeviceDescriptors
↑ Chromeでデフォルトで選択できる項目があるので便利です。

// 選択するパターン
const devices = require('puppeteer/DeviceDescriptors');
await page.emulate(devices['iPhone X']);

page.setUserAgent
また、UserAgentの文字列を設定する事もできます。

// 全部設定するパターン
await page.setUserAgent('Mozilla/5.0 ..')

ブラウザ内に対してJavaScriptを実行する

これはスクレイピングではよくやる処理だと思います。
ブラウザ内の要素を取得したり、色々出来ます。

page.evaluate(Function, args)

// Promiseなのでawaitが必要
const contentLinks = await page.evaluate(() => {
  const links = [];
  // 普通のJavaScriptの処理:aタグでhrefの要素を取得している
  const nodes = document.querySelectorAll('a[href]');
  nodes.forEach(node => {
    links.push(node.getAttribute('href'));
  })
  return links;
});

ちなみにevaluateでなくても$evalでも出来ます。
要素の抽出系では$$eval$$$とか色々あるみたいです。(詳しくは調べていないです)
page.$eval

const searchValue = await page.$eval('#search', el => el.value);

リクエストの種類ごとに制御する

実際にブラウザが要求したリクエストの制御をします。
これを行うことで特定のドメインにはリクエストしないや画像などの重いリクエストは無視するなどが出来ます。

event: 'request'

リクエストのイベントは3種類あります。
request, requestfailed, requestfinishedです。

今回はリクエスト処理前に制御したいのでrequestを使います。

// requestは通常読み込みのみなので書き換えることを許可する設定をします
await page.setRequestInterception(true);

page.on('request', request => {
  const reqUrl = request.url();
  // example.com以外のリクエストの場合は無視する
  if (reqUrl.includes('example.com')) {
    // 実際にリクエストを要求する
    // 引数のoverrideを使えば、リクエス内容の上書きも可能みたいです
    // https://github.com/GoogleChrome/puppeteer/blob/v1.5.0/docs/api.md#requestcontinueoverrides
    request.continue();
  } else {
    // リクエスト結果を勝手に定義する
    request.respond({ status: 200, body: 'not match domain'});
  }
});

Expressを使ってAPI化

Node系ではExpressしか使ったことないので、これを使ってAPI化しました。
Expressのリクエストの処理の中で(async () => {})を呼びました。
正直この書き方があっているか分かりませんが、動いたのでおkにしました。

app.get('/content', function (req, res) {
  ...
  const puppeteer = require('puppeteer');
  (async () => {
    const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
    ...
    await browser.close();
    res.json({ origin: paramUrl.origin, links: contentLinks });
  })();
});

Docker化

Dockerで日本語対応のHeadless Chrome + puppeteerを立ち上げ

Docker化はこちらの記事を参考にしました。

システムでの使い分け

Puppeteer + Express はあくまでスクレイピング用のAPIとしてしか使っていません。
Expressの中でPuppeteerの処理の結果をMySQLにデータを登録したりしていません。

直近、作ったシステムではPythonを使うコトが多いのでPythonからAPIを呼び出すようにしています。

ざっくりとした構成は以下の通りです。

f:id:AdwaysEngineerBlog:20180628145133p:plain

まとめ

Puppeteerは使いやすいですので皆さん使ってみてください。

個人的にはpage.waitFor系のメソッドなど気になるメソッドがいくつかあるので引き続き触ってみたいなと思います。
トリガー的な感じでwaitしてくれるのかな〜ワクワク。
page.waitFor..