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

このステップガイドでは、Puppeteerを使って静的および動的なウェブサイトをスクレイピングする方法を紹介します。
3 min read
web scraping with puppeteer

Puppeteerはブラウザテストと自動化のライブラリで、ウェブスクレイピングにも適しています。Axiosやcheerioのようなシンプルなツールに比べ、Puppeteerは、開発者が動的コンテンツ、(つまりユーザーのアクションに基づいて変化するコンテンツ)をスクレイピングすることを可能にします。

つまり、JavaScriptを使ってコンテンツを読み込むウェブアプリケーション(シングルページアプリケーション)のスクレイピングに使えるのです。ここで行おうとしているのは、まさにこのことです。

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


このチュートリアルでは、Puppeteerを使って静的、動的データ(つまり、Bright Dataのブログの投稿タイトルやリンク)をスクレイピングする方法を説明します。

セットアップ


チュートリアルを始める前に、お使いのコンピュータにNode.jsがインストールされていることを確認する必要があります。公式のダウンロードページからダウンロードできます。

次に、プロジェクト用に新しいディレクトリを作成して、以下のコマンドでそのディレクトリに移動します。

mkdir puppeteer_tutorial 
cd puppeteer_tutorial 
npm init -y 

次に、以下のコマンドでPuppeteerをインストールします。

npm i puppeteer --save

このコマンドで、ライブラリが使用する専用ブラウザもダウンロードされます。

静的サイトのスクレイピング


他のウェブスクレイピングツールと同様、PuppeteerでもウェブページのHTMLコードをスクレイピングできます。

以下に、Puppeteerを使ってBright Dataのブログから投稿の最初のページをスクレイピングする手順を示します。

index.jsファイルを作成して、Puppeteerをインポートします。

const puppeteer = require('puppeteer');

次に、Puppeteerの実行に必要な定型文を挿入します。

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null
  });

  const page = await browser.newPage();
  await page.goto('https://brightdata.com/blog');

  // all the web scraping will happen here  

  await browser.close();

})();

この関数はブラウザを開き、スクレイピングするページに移動して、ブラウザを閉じます。

あとはページからデータをスクレイピングするだけです。

Puppeteerで、HTMLデータにアクセスする最も簡単な方法は、page.evaluateメソッドを使うことです。Puppeteerには、要素を取得するのに便利な$メソッドと$$メソッドがありますが、page.evaluateからすべてのデータを取得する方が簡単です。

Bright Dataのブログでは、すべてのブログ投稿データは、brd_post_entryというクラスの<a>タグで囲まれています。投稿のタイトルは、brd_post_titleクラスの<h3>要素内にあります。投稿へのリンクは、brd_post_entryhref値です。

これらの値を抽出するpage.evaluate関数は、以下のようになります。

  const data = await page.evaluate( () => {

    let data = [];
    const titles = document.querySelectorAll('.brd_post_entry');

    for (const title of titles) {
      const titleText = title.querySelector('.brd_post_title').textContent;
      const titleLink = title.href;

      const article = { title: titleText, link: titleLink };
      data.push(article);
    }

    return data;

  })

最後に、データをコンソールに出力できます。

  console.log(data);

スクリプト全体は以下の通りです。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null
  });

  const page = await browser.newPage();
  await page.goto('https://brightdata.com/blog');

  const data = await page.evaluate(() => {

    let data = [];
    const titles = document.querySelectorAll('.brd_post_entry');

    for (const title of titles) {
      const titleText = title.querySelector('.brd_post_title').textContent;
      const titleLink = title.href;

      const article = { title: titleText, link: titleLink };
      data.push(article);
    }

    return data;

  })

  console.log(data);

  await browser.close();

})();

ターミナルでnode index.jsを呼び出して実行します。このスクリプトで、投稿のタイトルとリンクのリストが返されるはずです。

[
  {
    title: 'APIs for Dummies: Learning About APIs',
    link: 'https://brightdata.com/blog/web-data/apis-for-dummies'
  },
  {
    title: 'Guide to Using cURL with Python',
    link: 'https://brightdata.com/blog/how-tos/curl-with-python'
  },
  {
    title: 'Guide to Scraping Walmart',
    link: 'https://brightdata.com/blog/how-tos/guide-to-scraping-walmart'
  },
…

動的コンテンツのスクレイピング


静的コンテンツのスクレイピングは、単純なツールで簡単に行えるシンプルな作業です。ありがたいことに、Puppeteerは、クリック、入力、スクロールなど、さまざまなアクションを行うことができます。これらすべてを使って、動的なページと対話し、ユーザーのアクションをシミュレートできます。

このようなライブラリを使った一般的なウェブスクレイピングのタスクは、ページ上の特定のデータセットを検索することです。例えば、Puppeteerを使用して、Puppeteerに関するすべてのBright Dataの投稿を検索したいと思うかもしれません。

以下に、その方法を示します。

ステップ1:クッキーを受け入れる


Bright Dataのブログにアクセスすると、クッキーのバナーが表示されることがあります。

それに対処するためには、以下のコードを使って、すべて受け入れるボタンをクリックする必要があります。

  await page.waitForSelector('#brd_cookies_bar_accept', {timeout: 5000})
    .then(element => element.click())
    .catch(error => console.log(error));

コードの先頭行で、#brd_cookies_bar_acceptを持つ項目が表示されるまで5秒間待機します。2行目で、その要素をクリックします。3行目で、クッキーバーが表示されなくてもスクリプトがクラッシュしないようにします。

Puppeteerでの待機は、前のアクションが実行されるまでの一定の待機時間を設定するのではなく、待機する条件を指定することにより行われることに留意してください。前者を暗黙的待機、後者を明示的待機と呼びます。

Puppeteerでの明示的待機は、実行上の問題につながるため、使用しないことが強く推奨されています。待機時間を明示的に指定すると、長すぎる(効率的でない)か短すぎる(スクリプトが正しく実行されない)のどちらかになります。

ステップ2:投稿を検索する


その後、スクリプトは検索アイコンをクリックする必要があります。「Puppeteer」と入力し、検索アイコンを再度押して検索を行います。

これは以下のコードで実行できます。


await page.click('.search_icon');

  await page.waitForSelector('.search_container.active');
  const search_form = await page.waitForSelector('#blog_search');
  await search_form.type('puppeteer');

 await page.click('.search_icon');

  await new Promise(r => setTimeout(r, 2000));

この例は、クッキーバナーの例と同様に動作します。ボタンをクリックした後、検索コンテナが表示されるまで待つ必要があります。そのため、このコードでは.search_container.activeのCSSセレクタに一致する要素を待機します。

さらに最終的には、項目を読み込むために2秒の停止を加える必要があります。Puppeteerでは明示的な待機は推奨されませんが、今は他に良い選択肢がありません。

大半のウェブサイトでは、URLの変更が起こった場合、waitForNavigationメソッドを使用できます。新しい要素が現れたら、waitForSelectorメソッドを使えばよいのです。一部の要素が更新されるかどうかを把握するのは少し難しく、この記事の範囲外です。

自分で試してみたい場合は、このStack Overflowの回答が役に立つことでしょう。

ステップ3:投稿を収集する


投稿を検索した後、静的ページのスクレイピングにすでに使用したコードを使用して、ブログ投稿のタイトルを取得できます。

  const data = await page.evaluate( () => {

    let data = [];
    const titles = document.querySelectorAll('.brd_post_entry');

    for (const title of titles) {
      const titleText = title.querySelector('.brd_post_title').textContent;
      const titleLink = title.href;

      const article = { title: titleText, link: titleLink };
      data.push(article);
    }

    return data;

  })

  console.log(data);

スクリプトの全コードは以下の通りです。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: null
  });

  const page = await browser.newPage();
  await page.goto('https://brightdata.com/blog');

  const cookie_bar_accept = await page.waitForSelector('#brd_cookies_bar_accept');
  await cookie_bar_accept.click();
  await new Promise(r => setTimeout(r, 500));

  await page.click('.search_icon');

  await page.waitForSelector('.search_container.active');
  const search_form = await page.waitForSelector('#blog_search');
  await search_form.type('puppeteer');

  await page.click('.search_icon');

  await new Promise(r => setTimeout(r, 2000));

  const data = await page.evaluate( () => {

    let data = [];
    const titles = document.querySelectorAll('.brd_post_entry');

    for (const title of titles) {
      const titleText = title.querySelector('.brd_post_title').textContent;
      const titleLink = title.href;

      const article = { title: titleText, link: titleLink };
      data.push(article);
    }

    return data;

  })

  console.log(data);

  await browser.close();

})();

もっとうまくやれるでしょうか?


Puppeteerでウェブスクレイピングスクリプトを作成することは可能ですが、理想的ではありません。Puppeteerはテスト自動化のために作られているため、ウェブスクレイピングを行うには少し不便です。

例えば、お使いのスクリプトで規模拡大と効率化を実現したい場合、ブロックされずにスクレイピングできることが重要です。そのために、ユーザーとスクレイピングするウェブサイトの間にプロキシ(ゲートウェイ)を使うことができます。Puppeteer はプロキシの使用をサポートしていますが、プロキシネットワークを自分で探して契約する必要があります(詳しくは、Bright DataとのPuppeteerプロキシ統合をご覧ください)。

さらに、Puppeteerを並列使用に最適化するのは容易ではありません。大量のデータをスクレイピングしたいのであれば、パフォーマンスを最適化するための努力が必要になります。

こうした欠点があるため、Puppeteerは趣味で使う小規模なスクリプトには適しているものの、これを使用する場合、運用の規模を拡大するにはかなりの時間がかかります。

もっと使いやすいものが欲しいのであれば、Bright Dataのようなウェブデータプラットフォームを選ぶとよいでしょう。これにより、企業はスクレイピング専用に作られたScraping Browser(Puppeteer/Playwright互換)のような使いやすいツールを使って、ウェブから大量の構造化データを収集できます。

まとめ

この記事では、Puppeteerを使用して静的および動的なウェブページをスクレイピングする方法を説明しました。

Puppeteerは、項目をクリックしたり、テキストを入力したり、JavaScriptを実行したりと、ブラウザで実行可能なほとんどのアクションを実行できます。また、暗黙的待機を使用するため、Puppeteerで書かれたスクリプトは高速で簡単に記述できます。

ただし、いくつかの問題が発生する可能性もあります。Puppeteerはウェブスクレイピング用の最も効率的なツールではなく、そのドキュメントは初心者には適していません。また、Puppeteerの使い方に精通していない場合、Puppeteerを使ってスクレイピングの規模を拡大するのは困難です。

自分でデータをスクレイピングするのにうんざりしていませんか?事前に収集された、またはカスタマイズ済みのデータセットを入手してください。