Puppeteerを使ってクローラを作った話

こんにちは、開発本部のバックエンドエンジニアのThai(タイ)です。クローラ改善プロジェクトを行い、Puppeteerを使って新しいクローラを作りました。今回の記事ではPuppeteerで開発したクローラについて紹介したいと思います。

目次

Puppeteerとは

Googleで開発されて、DevToolsプロトコルを介してChromiumやChromeを制御するための高レベルなAPIを提供するNodeライブラリです。

参照: https://devdocs.io/puppeteer/

クローラとは

インターネット上の様々なWebページをスクレイピングするツールです。

なぜPuppeteerを使ってクローラを作るのか?

これまでインターネット上の記事を収集するために、PHP-curlで開発したクローラを使ってきました。PHP-curlは静的なページを問題なくクロールできますが、SPAページ(Single Page Application)はクロールできません。

なぜならJavaScriptフレームワークを使用してレンダリングされたSPAページはJavaScriptを通じてコンテンツの大部分を読み込みます。そのため、SPAページがクロールできるようにレンダリングエンジンを備えたツールを使う必要があります。例えば、

  • PhantomJS
  • Puppeteer
  • Selenium

などです。

以前は、PhantomJSが使われていました。PhantomJSはうまく動いていましたが、2018年6月にPhantomJSの開発が終了してしまったため、古くなってしまいました。一方、PuppeteerとSeleniumは現在もサポートされています。以下の出典によると、最近はPuppeteerとSeleniumはPhantomJSより人気があり、主流になっていることがわかります。そのため、PhantomJSの代わりにSeleniumやPuppeteerがおすすめされます。

出典: npm trends https://www.npmtrends.com/phantomjs-vs-puppeteer-vs-selenium-vs-selenium-webdriver-vs-webdriverio

SeleniumとPuppeteerはどちらもパフォーマンスが高く、テストの自動化とWebスクレイピングのための機能を備えた強力なツールです。Seleniumは、さまざまなブラウザを使用したい場合や、JavaやRubyやPythonなどさまざまな言語に精通している場合に最適です。逆にPuppeteerはJavaScriptしかサポートしていません。PuppeteerはSeleniumのようなテストツールというよりも自動化ツールです。 これが、Webスクレイピングやクロールなどの自動化タスクに適している理由です。PuppeteerはSeleniumより設定が簡単で使いやすくて、開発時間がかかりません。

そのため、今回はPuppeteerを選びました。

Puppeteerで何ができるのか?

  • ページのスクリーンショットとPDFを生成する。
  • SPAをクロールして、プリレンダリングコンテンツを生成する。
  • フォーム送信、UIテスト、キーボード入力などを自動化する。
  • 最新の自動テスト環境を構築でき、最新バージョンのChromeで、最新のJavaScriptとブラウザの機能を使って、テストを直接実行する。
  • サイトのタイムライン・トレースを取得し、パフォーマンス問題の診断に役立てる。

など色々なことができます。

Puppeteerで開発したクローラはどのように使うのか?

わかりやすくするために、Puppeteerクローラを使ってSPAページにおける記事を収集する直感的な例を出します。

サンプルソースコード: https://github.com/NguyenVietThai/puppeteer-crawler-sample

puppeteer-crawler-sample
    |
    |---crawler
    |    |---phpCrawler.php -> PHP-curlクローラ
    |    |---puppCrawler.js -> Puppeteerクローラ
    |    |...
    |
    |---frontend -> ReactJSプロジェクト
    |    |---public
    |    |    |---index.html -> HTMLファイル
    |    |    |---...
    |    |
    |    |---src
    |    |    |---App.tsx -> 記事リストを含むコンポーネント
    |    |    |---...
    |    |
    |    |...  
    |      
    |---README.md
INPUTlocalhost:3000に構築したReactJSで書いたSPAページをクロールします。
OUTPUTlocalhost:3000から4つ記事が収集できます。

上のページを作ったコード(src/App.tsx)

import './App.css';

function App() {
  return (
    <div className="App">
      <div className="App-container">
        <h1>こちらはクロールしたい記事リストです。</h1>
        <ul>
          <li><a className="App-link" href="#news1">1番目の記事</a></li>
          <li><a className="App-link" href="#news2">2番目の記事</a></li>
          <li><a className="App-link" href="#news3">3番目の記事</a></li>
          <li><a className="App-link" href="#news4">4番目の記事</a></li>
        </ul>
      </div>
    </div>
  );
}

export default App;

今回のReactJSプロジェクトのディレクトリには、public/index.htmlというHTMLファイルがあります。このファイルの中に単一の<div id="root">があります。 これはReactアプリケーションがレンダリングされる場所です。

ReactJS内のHTMLファイル (public/index.html)

<!DOCTYPE html>
<html lang="en">
  <head>
    ...省略...
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    ...省略...
  </body>
</html>

上の記事リスト(src/App.tsx)を画面で出すために、Stepが2つあります。

  • Step1: public/index.htmlというHTMLページを表示します。 このStepの<div id="root">は空タグです。
  • Step2: public/index.htmlの中の<div id="root">に上の記事リスト(src/App.tsx)を追加して再度レンダリングします。 このStepの<div id="root">は記事リスト含むタグです。

💬 ReactJSがどのようにレンダリングするのか興味があれば、以下のURLを参考できます。

あわせて読みたい
要素のレンダー – React
要素のレンダー – Reactユーザインターフェース構築のための JavaScript ライブラリ
あわせて読みたい
React Render HTML
React Render HTMLW3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Py...

クロールする結果がどのように違うのかを下に記述します。

  • PHP-curlを使う場合
<?php

$url = 'http://localhost:3000/';
$curl = curl_init();

curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION,true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_ENCODING, "gzip");

$res1 = curl_exec($curl);
$res2 = curl_getinfo($curl);

if($res2["http_code"] !== 200) {
    var_dump("Failed to craw page with status ".$res2["http_code"]);// 終了
    curl_close($curl);
    exit();
}

curl_close($curl);

$html = substr($res1, $res2['header_size']);

// This is content that you crawled from http://localhost:3000
var_dump($html);

PHP-curlコードを使って、https://localhost:3000をクロールすれば下のようなコンテンツを得られます。

PHP-curlでクロールしたら、Step1までしかクロールできなく、<div id="root">という空タグしか取れないので、記事リストが収集できなくなります。

  • Puppeteerを使う場合

インストール方法

npm i puppeteer

Puppeteerの使い方についてのサンプルコード

const puppeteer = require('puppeteer');

(async () => {
    try { 
        // Initial
        const browser = await puppeteer.launch();
        const page = await browser.newPage();

        // Go to url http://localhost:3000/
        const url = 'http://localhost:3000/';
        const response = await page.goto(url, {
            waitUntil: ['load'],
            timeout: 30000
        });

        if (!response) {
            throw new Error(`Cannot access to url ${url}`);
        }

        if (response.status() !== 200) {
            throw new Error(`Failed to access to url ${url} with status = ${response.status()}`);
        }

        console.log("This is a content that was crawled from a website: localhost:3000", await page.content());

        // Get a list of articles
        // const selector = ".App-container a.App-link";
        // await page.waitForSelector(selector);
        // const urls = await page.$$eval(selector, (list) => list.map((a) => a.href));
        // console.log("This is a list of articles that was crawled from a website: localhost:3000", urls);

        browser.close();
    } catch (error) {
        console.log('Error Exception', error);
    }
})();

PHP-curlの代わりにPuppeteerを使ってクロールすれば、Step2の<div id="root">という記事リストがあるエレメントを取れます。(コンテンツが少々長いので、少し省略しました)

PHP-curlでクロールした結果と違って、PuppeteerでクロールしたDOMの中の<div id="root">が空タグでなく、記事リストを含むタグです。この記事リストをクロールしたければ、.App-container a.App-linkというSelectorがあるエレメントを探して記事のURLを収集することができます。このエレメントはJavaScriptでレンダリングされるので、エレメントを完全にレンダリングする時間がかかる可能性があるので、waitForSelectorを使って待ちます。

...省略...
const selector = ".App-container a.App-link";
await page.waitForSelector(selector);
const urls = await page.$$eval(selector, (list) => list.map((a) => a.href));
console.log("This is a list of articles that was crawled from http://localhost:3000", urls);
...省略...

クロールした結果

まとめ

今回の記事ではPuppeteerを使った簡単なクローラの作り方を紹介しました。Puppeteerクローラが作れて、SPAページを収集することができたので、嬉しかったです。Puppeteerで開発するのは簡単で、初心者にとって本当に勉強しやすいと思いました。

この記事を書いた人

開発本部のWebクリッピングチームのバックエンドエンジニア

目次
閉じる