[速習] Puppeteer ~ヘッドレスChromeでテスト&スクレイピング

第4章スクレイピングの実装 ~人気サービス「note」タイムラインを取得する

本特集は、ヘッドレスChromeの紹介に始まり、PuppeteerのAPIの解説や、実践的なE2Eテストの実現方法を説明してきました。最終章では、これまでに学んだ集大成として、実在するサイトのスクレイピングを行います。具体的には、人気サービスnote⁠ノート)注1のタイムラインをスクレイピングし、最新の20件のノートを取得するプログラムを解説します。

本章では、画像の読み込みのブロックや無限スクロールへの対応など、実用的なテクニックを紹介していきます。ですがその前に、スクレイピングに関する重要な注意点をお伝えします。

スクレイピングに関する注意点

スクレイピングとは、プログラムを使ってサイトにアクセスし、そのサイトのコンテンツから欲しい情報を引き出す行為です。そのサイトがインターネットに公開されていたとしても、ブラウザではなくプログラムからアクセスしてもよいのか、確認を取る必要があります。

また、複数のページにアクセスする場合は、連続してアクセスすることで、サイトに負荷をかけすぎてしまう恐れもあります。そのため、アクセスの間隔を空けるよう気を付けなければなりません。

そこで、最低限注意すべき利用規約とrobots.txt、およびユーザーエージェントによる連絡先の指定方法について解説します。なお、ユーザーエージェントUser Agentとは、ブラウザやクローラの情報を申告するためのHTTPヘッダです。

利用規約を守る

サイトによって、プログラムからのアクセスや、抜き出した情報の再利用を利用規約で禁止している場合があります。そのようなサイトに対してスクレイピングを行うと、アクセスがブロックされるだけでなく、プログラムが悪質な場合は、法的に訴えられる可能性もあります。具体例として、Amazonの利用規約には下記の一文が書かれており、スクレイピングは固く禁止されています。

この利用許可には、アマゾンサービスまたはそのコンテンツの転売および商業目的での利用、製品リスト、解説、価格などの収集と利用、アマゾンサービスまたはそのコンテンツの二次的利用、第三者のために行うアカウント情報のダウンロードとコピーやそのほかの利用、データマイニング、ロボットなどのデータ収集・抽出ツールの使用は、一切含まれません。

そのほか、Facebookの利用規約でも、スクレイピングを禁止する項目が書かれています。

弊社から事前の許可を得ることなく、自動化された手段を用いて製品のデータにアクセスしたり取得したりすることや、アクセス許可のないデータへのアクセスを試みることを禁止します。

スクレイピングを行うサイトについては、同様の利用規約が書かれていないか、必ず事前に確認しなければなりません。noteの利用規約については後述します。

robots.txtを守る

robots.txtには、プログラムに対して、どのURLへのアクセスが許可されているのか、または禁止されているのかが書かれています。robots.txtは必ず、サイトのルートディレクトリに設置されるという決まりがあります。代表的なサイトでは、表1に書かれているURLから誰でもアクセスできます。

表1 代表的なサイトのrobots.txt
サイト名 robots.txtのURL
Google https://www.google.co.jp/robots.txt
Amazon https://www.amazon.co.jp/robots.txt
Facebook https://facebook.com/robots.txt
Apple https://www.apple.com/robots.txt

たとえば、URLのパスが「/mypage」から始まる場合を除き、すべてのページのスクレイピングを許可したいとします。その場合は、robots.txtを下記のように記述して、そのドメインのルートディレクトリに設置します。

# 対象となるプログラムのユーザーエージェント
User-agent: *
# アクセスを禁止するURLのパス
Disallow: /mypage
# アクセスを許可するパス
Allow: /

この例では、User-agent: *となっているので、すべてのユーザーエージェントに対しての指示になっています。DisallowAllowの両方が適用されるURLの場合は、より限定的に指定されているほうが優先されます。先ほどの例では、⁠/mypage」以下にアクセスされた場合、アクセスの禁止ルールのみが適用されます。

実在するサイトの具体例

より理解を深めるために、実在するサイトの具体例を見てみましょう。Googleのrobots.txtは、下記の2行から始まっています。

User-agent: *
Disallow: /search

Googleの検索結果ページのパスは、⁠/search」から始まります。そのため、プログラムを使ってGoogle検索を実行することは、robots.txtによって禁止されていることがわかります。このルールを無視してスクレイピングを行うと、Googleへのアクセスがブロックされてしまう恐れがあります。Google検索のスクレイピングはやめておいたほうがよいでしょう。

次に、今回スクレイピングを行うnoteのrobots.txtを確認します。すべてのユーザーエージェント向けのルールは下記のようになっています(執筆時現在⁠⁠。

User-agent: *
Disallow: /*/message
Disallow: /*/terms/specified
Disallow: /*/menu/*
Disallow: /admin/*
Disallow: /_nourlname*
Disallow: /settings/*
Disallow: /library/*

スクレイピング対象のタイムラインのパスは「/」なので、このページへのスクレイピングは禁止されていないようです。

なお、robots.txtのより詳細な仕様については、Googleによるリファレンスが参考になるでしょう。

ユーザーエージェントを指定する

スクレイピングを行う場合は、ユーザーエージェントに連絡先を入れることをお勧めします。そうすることで、万が一サイト管理者が異常を検知した場合に、スクレイピングを行った開発者と連絡をとることが可能になります。連絡先には、URLやメールアドレスを書くとよいでしょう。

たとえば、Googleのクローラ(Googlebot)は、ユーザーエージェントにhttp://www.google.com/bot.htmlというURLが含まれています。このURLにアクセスすると、Googleクローラの説明記事が掲載されています。なお、Googleクローラの完全なユーザーエージェントは下記のとおりです。

Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

Puppeteerを使った指定方法

Puppeteerでユーザーエージェントを指定するためには、page.setUserAgent()を使用します。下記のコードでは、メールアドレスを含むユーザーエージェントを指定したあとに、サイトにアクセスしています。なお、ユーザーエージェントに含まれる「自分のメールアドレス」については、みなさんのものを指定してください。

// ユーザーエージェントを指定する
await page.setUserAgent(
  'WDB109 Puppeteer (自分のメールアドレス)'
);
// サイトにアクセスする
await page.goto('https://example.com');

このように、ユーザーエージェントの指定は、必ずサイトにアクセスする前に実行してください。そうしないと、最初のページに対して、デフォルトのユーザーエージェントでアクセスしてしまいます。

デフォルトのユーザーエージェント

デフォルトのユーザーエージェントは、ヘッドレスモードで起動しているか否かによって異なります。ヘッド「フル」モードの場合は、通常のChromeと同じユーザーエージェントになります。しかし、ヘッドレスモードで起動している場合は、下記のようにユーザーエージェントに「HeadlessChrome」の文字列が含まれます。

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/72.0.3617.0 Safari/537.36

人気サービス「note」とは

本章では、誰でも文章、写真、音楽、動画などのコンテンツを投稿できるWebサービスnoteのスクレイピングを行います。noteに投稿したコンテンツは、ブログやSNSSocial Networking Serviceのように無料で公開できるだけでなく、ユーザーどうしで売り買いできることが、サービスの大きな特徴です図1⁠。

図1 noteのタイムライン
図1

本特集を執筆するにあたり、noteの運営元であるピースオブケイクに問い合わせたところ、スクレイピングの題材として使用することを快諾していただきました。

掲載コードに関する注意点

本章の掲載コードは、noteの利用規約およびrobots.txtを一切侵害しておりません(執筆時点2019年1月現在⁠⁠。しかし、これらの利用制限については、今後更新される可能性があります。利用制限を侵害すると、サイトの運営元やほかの利用者に迷惑をかけてしまう可能性があります。そのため、本章の掲載コードを実行するときは、その都度利用規約およびrobots.txtの内容を確認し、みなさんの責任のもと実行してください。

また、今後サイトリニューアルなどによって、プログラムが動作しなくなる可能性もあります。その場合は、本特集で学んだことを応用し、適宜コードの修正を行ってください。

スクレイピングの目的

本章の目的は、noteのタイムラインをスクレイピングし、最新の20件のノートを取得することです。タイムラインとは、ログイン後noteを開いたときに最初に表示されるページを指します。このページでは、自分がフォローしているユーザーやマガジンのノートが、新着順に表示されます。なお、ブログと比較すると、ノートは一つ一つの記事、マガジンは記事のカテゴリのようなものだと考えて差し支えはないでしょう。

筆者は普段からnoteを利用していますが、自分だけのタイムラインをチャットツールに連携したいと考えていました。しかし、noteのタイムラインは執筆時点ではSPAになっており、スクレイピングを行うためにはブラウザの自動化が不可欠です。そこで、チャットツールとの連携は本特集のテーマから外れるため扱いませんが、noteのタイムラインをスクレイピングの題材として選択しました。

アカウントの登録

noteで自分だけのタイムラインを表示するためには、ログインするためのアカウントが必要です。アカウントは一度登録するだけなので、自動化する必要はありません。まだアカウントを持っていない方は、通常のブラウザで新規登録ページを開き、案内に従って登録を完了してください図2⁠。

図2 noteの新規登録
図2

なお、登録に使用したメールアドレスおよびパスワードはログインの自動化で使用するため、控えておく必要があります。

スクレイピングの流れ

アカウントの登録が完了したら、さっそくスクレイピングの流れを見ていきましょう。今回は、noteのログイン後に表示されるタイムラインから、最新の20件のノートを取得するまでを解説します。

セットアップ

まずはじめに、基本的なセットアップを開始します。手順はこれまでと変わらず、適当なディレクトリを作成し、そのディレクトリ内でインストールコマンドを実行するだけです。

端末1 パッケージ管理を開始する
$ npm init -y
端末2 Puppeteerをパッケージにインストール
$ npm install --save [email protected]

基本のプログラムを保存する

次に、先ほど作成したディレクトリ直下に、index.jsというファイル名で下記のプログラムを保存します。

// ライブラリを読み込む
const puppeteer = require('puppeteer');
(async () => {
  const options = process.env.DEBUG
    ? {devtools: true} // デバッグ実行時のオプション
    : {}; // 通常時のオプション
  // ブラウザを立ち上げる
  const browser = await puppeteer.launch(options);
  // ブラウザのタブを開く
  const page = await browser.newPage();
  // ユーザーエージェントを指定する
  await page.setUserAgent(
    `WDB109 Puppeteer (${process.env.NOTE_EMAIL})`
  );
  // ログインフォームにアクセスする
  await page.goto('https://note.mu/login');
  // デバッグを開始する
  await page.evaluate(() => { debugger; });
  // ブラウザを閉じる
  await browser.close();
})();

プログラムの内容は、noteのログインフォームでデバッグを開始するだけの単純なものです。ほとんどが第3章までに学んだ内容ですが、本章で学んだばかりのpage.setUserAgent()を使ったユーザーエージェントの指定も行われています。

今回は2ヵ所で環境変数にアクセスしています。1ヵ所は起動オプションの切り替えで、もう1ヵ所はユーザーエージェントに含まれるメールアドレスです。DEBUGを使ってデバッグ実行を行うのは、毎回起動オプションを変更する手間を省くためです。

一方、NOTE_EMAILを通じてメールアドスを受け渡すのは、プログラムが漏洩したときに、メールアドレスが悪用されるリスクを防ぐためです。また、この環境変数は、のちほどnoteのログインにも使用します。

プログラムを実行する

実際に下記コマンドを実行し、コードが問題なく動作することを確認しましょう。

端末3 macOS/Linuxの場合
$ DEBUG=true \
NOTE_EMAIL=登録したメールアドレス \
node index.js
端末4 Windowsの場合
> set DEBUG=true
> set NOTE_EMAIL=登録したメールアドレス
> node index.js

すると、noteのログインフォームが表示されたところで、JavaScriptの実行が一時停止されます図3⁠。デバッグモードが開始したら、下記のJavaScriptをコンソールで実行し、指定したユーザーエージェントに書き換わっていることを確認しましょう。

図3 ユーザーエージェントの確認
図3
// ユーザーエージェントを確認する
navigator.userAgent

ユーザーエージェントが問題なく書き換わっていれば、デバッグ実行を終了し、このプログラムを終了してください。

画像の読み込みをブロックする

ブラウザを自動化してスクレイピングする場合、必要のないファイルのダウンロードに時間がかかってしまう場合があります。今回のスクレイピングでは、タイムラインを動的に生成するためにJavaScriptを読み込む必要はありますが、画像ファイルを読み込む必要はありません。

そのような場合は、page.setRequestInterception()を用いて不要なリクエストをブロックすることが効果的です。このメソッドの引数にtrueを渡して実行すると、requestイベントを受け取ったときに、request.continue()request.abort()およびrequest.respond()の3つのメソッドが有効化されます。

requestイベントは、下記のコードで受け取ることができます。

// リクエストのフィルタリングを有効化する
await page.setRequestInterception(true);
// リクエストイベントを受け取る
page.on('request', request => {
  // さまざまな処理を行う
  // ...
});

リクエストを書き換える

先ほど有効化されたメソッドを用いることで、リクエストを許可したり、中断するだけでなく、もとのリクエストやレスポンスを書き換えることも可能になります。

// リクエストを許可する
request.continue();
// リクエストをブロックする
request.abort();
// リクエストを書き換える
request.continue({
  // ボディの上書き
  postData: { foo: 'bar },
  // ヘッダの上書き
  headers: { origin: 'https://note.mu/login' },
});
// レスポンスを書き換える
request.respond({
  // ステータスコードの上書き
  status: 404,
  // コンテンツタイプの上書き
  contentType: 'text/plain',
  // ボディの上書き
  body: 'Not Found!',
});

この機能を用いて、テスト用のサーバをモックしたり、特定のアクセス解析ツールへのリクエストをブロックすることにも応用できます。下記のプログラムは、request.resourceType()メソッドを使ってコンテンツタイプを判定し、画像ファイルへのリクエストだけをブロックするコードになっています。

await page.setRequestInterception(true);
page.on('request', request => {
  if (request.resourceType() === 'image') {
    // リクエストをブロックする
    request.abort();
  } else {
    // リクエストを許可する
    request.continue();
  }
});

このプログラムのポイントは、page.setUserAgent()と同じように、page.goto()よりも先に実行することです。そうすることで、最初にアクセスするページから、リクエストを書き換えることができます。

プログラムを実行する

実際に、このコードをpage.goto()の直前に埋め込んで、再びプログラムを実行してみましょう。

すると、気が付きにくいかもしれませんが、noteのロゴが読み込まれないようになっていることがわかります図4⁠。これは、ロゴが画像ファイルになっており、先ほどのコードによってリクエストがブロックされているためです。

図4 画像の読み込みをブロック
図

ログインフォームに入力する

次に、ログインを実行します。ログインするアカウントは、事前に登録したものを使用します。フォームの送信に必要なコードは、第3章までに学んだとおりです。

環境変数にアクセスする

コード中にパスワードを直接書き込んでしまうと、コードが漏洩したときにセキュリティ事故につながる恐れがあります。そこでメールアドレスと同じように、環境変数を通じてパスワードをプログラムに渡しましょう。

// メールアドレスを入力する
await page.type(
  'input[name=login]',
  process.env.NOTE_EMAIL
);
// 環境変数を使ってパスワードを入力する
await page.type(
  'input[name=password]',
  process.env.NOTE_PASSWORD
);
// 複数の非同期処理を並列で実行する
await Promise.all([
  // タイムラインが表示されるまで待機する
  page.waitFor('.feed'),
  // ログインする
  page.click('button')
]);

なお、タイムライン上の一つ一つのノートは、⁠feed」というクラス属性を持っています。そこでpage.waitFor()を使って、ログインが完了してタイムラインが表示されるまで待機するコードになっています。

プログラムを実行する

先ほどのコードを、page.goto()の直下に加えて保存します。次に、実行時のコマンドに、NOTE_PASSWORDの環境変数を加えて実行しましょう。

端末5 macOS/Linuxの場合
$ DEBUG=true \
NOTE_EMAIL=登録したメールアドレス \
NOTE_PASSWORD=登録したパスワード \
node index.js
端末6 Windowsの場合
> set DEBUG=true
> set NOTE_EMAIL=登録したメールアドレス
> set NOTE_PASSWORD=登録したパスワード
> node index.js

ログイン後タイムラインが表示され、デバッグモードが開始されれば、次のステップに進むことができます。

無限スクロールに対応する

それでは、いったいどのような状態でJavaScriptの実行が一時停止されているでしょうか。画面を見てみると、たしかにタイムラインにノート一覧が表示されていることがわかります図5⁠。

図5 タイムラインで一時停止
図5

この状態で、目的であった最新の20件のノートが表示されているか確かめてみましょう。

下記のコードを、デベロッパーツールのコンソールに入力して実行します。

// ノートの件数を取得する
document.querySelectorAll('.feed').length

すると、JavaScriptの実行結果が10であることがわかりました。このことは、最新の10件までのノートしか表示されていないことを意味します。これは、noteのタイムラインが無限スクロールInfinite Scrollで表示されているためです。

ブラウザは、一度にすべてのコンテンツを読み込んでしまうと、画面の描画に時間がかかりすぎてしまいます。無限スクロールとは、このような問題を回避するために、画面をスクロールするたびに少しずつコンテンツを読み込む手法です。

画面下部までスクロールする

この無限スクロールに対応して20件以上のノートを表示するためには、画面下部までスクロールし、新しくノートが表示されるまで待機する必要があります。

ブラウザ上で条件を満たすまで待機するメソッドは、page.waitFor()でした。このメソッドを使って、20件以上のノートが表示されるまで待機するコードは、下記のようになります。

// ブラウザ上で関数を実行し、trueが返るまで待機する
await page.waitFor(() => {
  // ノートの件数が20件以上かチェック
  const notes = document.querySelectorAll('.feed');
  return notes.length >= 20;
});

ブラウザ上で実行されるJavaScriptに、画面の下部までスクロールするためのwindow.scrollTo(0,document.body.scrollHeight);を組み合わせます。

すると、下記のようなコードができあがります。

await page.waitFor(() => {
  // 画面下部までスクロールする
  window.scrollTo(0, document.body.scrollHeight);
  const notes = document.querySelectorAll('.feed');
  return notes.length >= 20;
});

プログラムを実行する

このコードを、先ほどのログインコードの下に追記して実行してみましょう。すると、ノートの件数をチェックするJavaScriptがブラウザ上で実行されるたびに、画面下部にスクロールされるようになりました。

以上で、無限スクロールに対応できました。実際にコードが動いているか、再びプログラムを実行して確かめてみましょう。コンソールでノートの件数を取得し、20以上の数値が得られれば成功です図6⁠。

図6 20件のノートを取得
図

タイムラインからタイトルとURLを取得する

最後にブラウザ上でJavaScriptを実行し、表示されているノートのタイトルとURLの一覧を取得します。結果は、page.$$eval()メソッドを用いて、下記のコードで受け取ることができます。

// ノートのタイトルとURLの一覧を受け取る
const query = '.renewal-p-cardItem__title a';
const notes = await page.$$eval(query, anchors =>
  anchors.map(anchor => ({
    title: anchor.innerText,
    url: anchor.href
  }))
);
// ログに出力する
console.log(notes);

このコードを、先ほどまでデバッグに用いていたawait page.evaluate(() => { debugger; });の代わりに置き換えて保存します。これ以上デバッグは必要ないので、DEBUGの環境変数を渡さずにプログラムを実行します。すると、図7のような配列がログに出力されるはずです。

図7 ノート一覧をログに出力
図7

以上で、noteのタイムラインをスクレイピングし、最新の20件のノートを取得するプログラムが完成しました。

最終的なプログラム

最終的には、下記のようなプログラムが完成しているはずです。プログラム中では、ユーザーエージェントの指定、画像のブロック、無限スクロールへの対応など、実践的なテクニックが数多く使われています。また、本特集では扱いませんが、取得したノート一覧をログに出力するだけでなく、Slackのようなチャットツールに流し込むことで、より実用的なコードにできるでしょう。

// ライブラリを読み込む
const puppeteer = require('puppeteer');
(async () => {
  const options = process.env.DEBUG
    ? {devtools: true} // デバッグ実行時のオプション
    : {}; // 通常時のオプション
  // ブラウザを立ち上げる
  const browser = await puppeteer.launch(options);
  // ブラウザのタブを開く
  const page = await browser.newPage();
  // ユーザーエージェントを指定する
  await page.setUserAgent(
    `WDB109 Puppeteer (${process.env.NOTE_EMAIL})`
  );
  // リクエストのフィルタリングを有効化する
  await page.setRequestInterception(true);
  // リクエストイベントを受け取る
  page.on('request', request => {
    if (request.resourceType() === 'image') {
      // リクエストをブロックする
      request.abort();
    } else {
      // リクエストを許可する
      request.continue();
    }
  });
  // ログインフォームにアクセスする
  await page.goto('https://note.mu/login');
  // メールアドレスを入力する
  await page.type(
    'input[name=login]',
    process.env.NOTE_EMAIL
  );
  // 環境変数を使ってパスワードを入力する
  await page.type(
    'input[name=password]',
    process.env.NOTE_PASSWORD
  );
  // 複数の非同期処理を並列で実行する
  await Promise.all([
    // タイムラインが表示されるまで待機する
    page.waitFor('.feed'),
    // ログインする
    page.click('button')
  ]);
  // ブラウザ上で関数を実行し、trueが返るまで待機する
  await page.waitFor(() => {
  // 画面下部までスクロールする
    window.scrollTo(0, document.body.scrollHeight);
    // ノートの件数が20件以上かチェック
    const notes = document.querySelectorAll('.feed');
    return notes.length >= 20;
  });
  // ノートのタイトルとURLの一覧を受け取る
  const query = '.renewal-p-cardItem__title a';
  const notes = await page.$$eval(query, anchors =>
    anchors.map(anchor => ({
      title: anchor.innerText,
      url: anchor.href
    }))
  );
  // ログに出力する
  console.log(notes);
  // ブラウザを閉じる
  await browser.close();
})();

特集のおわりに

いかがだったでしょうか。本特集の前半では、ヘッドレスChromeとPuppeteerの紹介から、JavaScriptの構文やPuppeteerのAPIのような基本的な解説をしました。そして後半では、E2Eテストや実在するサービスのスクレイピングなど、実践的なプログラムを解説しました。

実のところ、本特集だけではPuppeteerの豊富な機能のほんの一部しか紹介できていません。それでも、ヘッドレスChromeを簡単に自動化でき、多くの分野に適用できる可能性に満ち溢れたツールであることが、少しでも伝わっていれば幸いです。

今後みなさんがPuppeteerを使って、快適なブラウザ自動化を行うことを願っています。

おすすめ記事

記事・ニュース一覧