見出し画像

【Node.js】スクレイピングの基本をまとめてみた


はじめに


こんにちは、SHIFT のアジャイル推進部に所属している Matsuura です。
現在はデリバリ改革部で開発の基礎について学んでいます。
先日初めてスクレイピングに取り組んだので、今回はその際に調べた内容を含め初学者向けにスクレイピングについてご紹介します。

スクレイピングとは


スクレイピングとは、ウェブサイトからデータを自動的に抽出する技術のことです。これにより、大量のデータを手動で収集する手間を省くことができます。スクレイピングは、データ分析、価格比較、リサーチなど、さまざまな用途で利用されています。

スクレイピングの流れ


スクレイピングは大まかに以下の流れで実施します。

  1. ターゲットサイトの URL を取得する

  2. ターゲットサイトに HTTP リクエストを送信して HTML データを取得する

  3. HTML を解析して必要な情報を抽出する

  4. 目的に沿ってデータを整形する

スクレイピングツール


スクレイピングを行うためのツールやライブラリについて、代表的なものをいくつか紹介します。

axios

axios は、HTTP リクエストを送信するためのライブラリです。これを使ってターゲットサイトから HTML データを取得します。以下のような特徴を持ちます。

  • Promise ベースのライブラリであり、非同期処理を簡単に扱うことができる

  • ブラウザと Node.js の両方で動作

  • リクエストとレスポンスのインターセプトが可能

jsdom

jsdom は、Node.js 環境で DOM を操作するためのライブラリです。取得した HTML を DOM として扱い、必要なデータを抽出するのに役立ちます。以下のような特徴を持ちます。

  • ブラウザのように DOM を操作可能

    • DOM:HTML や XML 文書の構造を表現するためのオブジェクトモデルで、文書をツリー構造として表現する。DOM ツリーの中の各ノードを DOM 要素といい、タグ名(div, p, a など)、属性(id, class など)、テキスト内容、子要素を持つ。

  • JavaScript の実行が可能

  • HTML の解析が高速

cheerio

cheerio は Node.js 環境で動作する HTML と XML を解析・操作するためのライブラリです。以下のような特徴を持ちます。

  • JQuery と同じように css セレクタを使って要素を選択できるため軽量で高速。

    • jQuery:JavaScript のライブラリで、HTML ドキュメントの操作、イベント処理、アニメーション、Ajax 通信などを簡単に行うためのツール

  • シンプルで一貫性のある DOM モデルで動作するため、解析、操作、レンダリングが効率的

  • ほぼすべての HTML または XML ドキュメントを解析。ブラウザーとサーバー環境の両方で動作する。

puppeteer

puppeteer はヘッドレスブラウザ(ブラウザの UI を表示せずに操作する)を操作するためのライブラリです。Google が開発しており、Chrome や Chromium を操作するための API を提供します。特徴は以下の通りです。

  • ヘッドレスブラウザを操作

    • UI を持たないため、効率的にウェブページを操作できる

  • JavaScript で動的に生成されたコンテンツもスクレイピング可能

    • JavaScript によって動的に生成されるコンテンツも含めてスクレイピングできる。これにより、静的な HTML だけでなく、動的なウェブアプリケーションからもデータを取得可能。

  • スクリーンショットや PDF を生成可能

実際にスクレイピングしてみた


それでは「axios+jsdom」「axios+cheerio」「puppeteer」の 3 パターンについて、実際にスクレイピングをやってみます。

課題

要件

  • 対象画面のテーブルからダミーの個人情報を取得する

  • 取得したデータは連想配列に整形し以下のように json 形式でコンソールに出力する

// 出力結果例
  [
    { '名前': '山田 太郎', '住所': '東京都', '性別': '男性', '年齢': '30' },
    { '名前': '佐藤 花子', '住所': '大阪府', '性別': '女性', '年齢': '25' },
    { '名前': '鈴木 次郎', '住所': '北海道', '性別': '男性', '年齢': '40' },
    { '名前': '高橋 美咲', '住所': '福岡県', '性別': '女性', '年齢': '35' },
    { '名前': '田中 一郎', '住所': '愛知県', '性別': '男性', '年齢': '28' },
    { '名前': '中村 さくら', '住所': '京都府', '性別': '女性', '年齢': '22' },
    { '名前': '小林 健太', '住所': '神奈川県', '性別': '男性', '年齢': '33' },
    { '名前': '加藤 美穂', '住所': '兵庫県', '性別': '女性', '年齢': '27' },
    { '名前': '渡辺 翔太', '住所': '千葉県', '性別': '男性', '年齢': '31' },
    { '名前': '伊藤 由美', '住所': '静岡県', '性別': '女性', '年齢': '29' }
 ]

対象画面のソースコード

  • 今回の画面は以下のソースコードを使用しました。

// target.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>スクレイピング練習用</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header>
      <h1>スクレイピング練習用</h1>
    </header>
    <main>
      <section>
        <h2>名簿</h2>
        <table id="table1">
          <thead>
            <tr>
              <th>名前</th>
              <th>住所(都道府県)</th>
              <th>性別</th>
              <th>年齢</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>山田 太郎</td>
              <td>東京都</td>
              <td>男性</td>
              <td>30</td>
            </tr>
            <tr>
              <td>佐藤 花子</td>
              <td>大阪府</td>
              <td>女性</td>
              <td>25</td>
            </tr>

            // 一部省略

            <tr>
              <td>伊藤 由美</td>
              <td>静岡県</td>
              <td>女性</td>
              <td>29</td>
            </tr>
          </tbody>
        </table>
      </section>
    </main>
  </body>
</html>
// style.css
body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
  display: flex;
  flex-direction: column;
  align-items: center;
}
header {
  background-color: #333;
  color: #fff;
  padding: 10px 0;
  text-align: center;
  width: 100%;
}
main {
  padding: 20px;
  background-color: #fff;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  max-width: 800px;
  width: 100%;
  text-align: center;
  margin-top: 20px;
}
section {
  margin-bottom: 20px;
}
h1,
h2 {
  margin: 0 0 10px;
}
img {
  display: block;
  margin: 0 auto;
}
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 10px;
}
table,
th,
td {
  border: 1px solid #ddd;
}
th,
td {
  padding: 8px;
  text-align: left;
}
th {
  background-color: #f2f2f2;
}

axios+jsdom

  • axios を使用し HTTP リクエストを送信した後、jsdom を用いて DOM を操作します。

 // main.js
 import axios from "axios";
 import { JSDOM } from "jsdom";

 async function main() {
   try {
     // 指定されたURLからデータを取得
     const response = await axios.get("対象サイトのurl");
     // 取得したHTMLデータをパース
     const dom = new JSDOM(response.data);
     // テーブル要素を取得
     const table = dom.window.document.querySelector("#table1");
     const output = [];

     for (let i = 1; i < table.rows.length; i += 1) {
       const row = table.rows[i];
       const rowData = {
         名前: row.cells[0].textContent,
         住所: row.cells[1].textContent,
         性別: row.cells[2].textContent,
         年齢: row.cells[3].textContent,
       };
       output.push(rowData);
     }
     console.log(output);
   } catch (err) {
     console.error("Error in main:", err);
   }
 }
 main();
  • ポイント

    • const dom = new JSDOM(response.data):取得した HTML データを JSDOM を使ってパースし、DOM オブジェクトを生成

    • dom.window.document.querySelector("#table1")

      • dom.window.document:仮想的な window オブジェクトの document プロパティ。ウィンドウに含まれている文書への参照を返す。

        • windowオブジェクト:ブラウザのウィンドウやフレームを表すオブジェクト。ブラウザ操作のための関数やオブジェクトを含む。

        • documentプロパティ:window オブジェクトのプロパティ。HTML 文書を表す。

      • querySelector:CSS セレクタを使って DOM ツリー内の最初の要素を選択

    • table.rows/cells :表の全ての行(<tr>要素)/セル(<td>要素)を含む HTMLCollection を返す

      • HTMLCollection:特定の条件に一致する複数の HTML 要素をまとめて扱うためのオブジェクト。最新の DOM ツリーを参照しているため、DOM ツリーに変更が加えられると、その変更が HTMLCollection に反映される。配列のようにインデックスでアクセス可能。

axios+cheerio

  • axios を使用し HTTP リクエストを送信した後、 cheerio で HTML をパースして操作します。

// main.js
import axios from "axios";
import { load } from "cheerio";

async function main() {
  try {
    // 指定されたURLからデータを取得
    const response = await axios.get("対象サイトのurl");
    // 取得したHTMLデータをパース
    const $ = load(response.data);

    const output = [];
    // 各行のデータを取得しoutput配列に格納
    $("#table1 tr").each((index, element) => {
      if (index === 0) return; // ヘッダーをスキップ
      const rowData = {
        名前: $(element).find("td").eq(0).text(),
        住所: $(element).find("td").eq(1).text(),
        年齢: $(element).find("td").eq(2).text(),
        性別: $(element).find("td").eq(3).text(),
      };
      output.push(rowData);
    });
    console.log(output);
  } catch (err) {
    console.error("Error in main:", err);
  }
}
main();
  • ポイント

    • const $ = load(response.data): load(response.data)はHTML 文字列をパースして Cheerio オブジェクトを返す。

    • $("#table1 tr"): id="table1"のテーブル内の全ての行(<tr>)を選択

    • $(element).find("td").eq(0).text()

      • $(element):element は HTML の DOM 要素、今回は各<tr>要素を指す

      • find("td"):element の子要素の中から、<td>タグを持つ全ての要素を検索

      • eq().text():指定されたインデックスに一致する要素のテキスト内容を取得

pupettir

  • axios を使用し HTTP リクエストを送信した後 cheerio で HTML をパースして操作する

// main.js
import puppeteer from "puppeteer";

async function main() {
  try {
    // ブラウザの起動
    const browser = await puppeteer.launch();
    // 新しいページを開く
    const page = await browser.newPage();
    // 指定されたURLに移動
    await page.goto("対象サイトのurl");
    // テーブルの行を取得
    const output = await page.$$eval("#table1 tr", (rows) =>
      Array.from(rows)
        .slice(1)
        // 各行をマップしてオブジェクトに変換後セルを取得
        .map((row) => {
          const cells = row.querySelectorAll("td");
          return {
            名前: cells[0].innerText,
            住所: cells[1].innerText,
            年齢: cells[2].innerText,
            性別: cells[3].innerText,
          };
        })
    );
    console.log(output);
    // ブラウザを閉じる
    await browser.close();
  } catch (err) {
    console.error("Error in main:", err);
  }
}
main();
  • ポイント

    • puppeteer.launch():Puppeteer は実際のブラウザを操作するため、ブラウザの起動が必要

    • browser.newPage():新しいページ(タブ)を開くことで、特定の URL にアクセスして操作を行うことができる

    • browser.close():リソースを解放し、メモリの無駄遣いを防ぐため終了後はブラウザを閉じる

    • page.$$eval:指定したセレクタに一致する全ての要素を取得し、関数を実行

    • querySelectorAll:指定された CSS セレクタをに一致する DOM ツリー内の全てのの要素を選択。ここでは<td>を取得。

3 つの手法の比較

time コマンドで実行時間を測定した結果は表の通りとなりました。

  • 「axios + cheerio」を用いた場合が最も高速となりました。cheerio により軽量でシンプルな DOM 操作が可能で、静的 HTML の解析には最適です。

  • 次に速いのが「axios + jsdom」でした。cheerio と比較するとやや重い傾向にあります。しかし、HTML ドキュメントの各要素(タグ)を JavaScript のオブジェクトとして扱い、そのプロパティやメソッドを使って操作できるため HTML の構造を理解しやすく感じました。

  • 最後に最も遅いのが puppeteer でした。cheerio や jsdom ではブラウザを起動せずに、サーバーサイドで HTML や DOM の操作を行います。一方、puppeteer では画面を表示しないヘッドレスブラウザではあるものの、実際のブラウザを起動して操作します。そのため、他のスクレイピングツールと比べてリソース消費が多く時間がかかり、コードも若干複雑になります。よって、今回のようなシンプルな静的なスクレイピングには不向きだといえます。

おわりに


この記事では初学者向けにスクレイピングの基本と代表的なツールについて紹介しました。
今回は静的なサイトのスクレイピングのみを行いましたが、今後は動的なコンテンツなどより高度なスクレイピングについても挑戦してみたいです。
この記事が初心者の方々にとってスクレイピングの導入時の参考となれば幸いです。

参考リンク


執筆者プロフィール:Shino Matsuura
株式会社SHIFT アジャイル推進部所属。現在は、デリバリ改革部で、開発の基礎を勉強中。

お問合せはお気軽に

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

PHOTO:UnsplashMohammad Rahmani