こんにちは、開発本部のバックエンドエンジニアの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がおすすめされます。

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
INPUT | localhost:3000に構築したReactJSで書いたSPAページをクロールします。 |
OUTPUT | localhost: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を参考できます。

クロールする結果がどのように違うのかを下に記述します。
- 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で開発するのは簡単で、初心者にとって本当に勉強しやすいと思いました。