スクレイピングツールをChrome拡張機能で開発する

## はじめに
Webサイトから情報を取得するとき、スクレイピングのプログラムを書いて情報を取得することがあるかと思います。しかし、一度しか使わないのにプログラムを書くのは面倒なので、汎用的にいろいろなWebサイトで使用できるツールが欲しくなりました。
Chrome拡張機能であればいろいろなWebサイトで使用することができ、またWebアプリと違ってCORS制約なども無く使用できると考えたため、作ってみました。

実際に使用できるプログラムはGitHub上に置いておきますので、以下からご確認ください。

https://github.com/t-ujiie-g/scrapingExtension

拡張機能の取り込み方などは以下の記事などが参考になるかと思います。

https://qiita.com/komlabo/items/5b99d3598f6664621dae

## 実装内容
### 完成イメージ/使用方法
どのWebサイトでも使用できるイメージで作成しています。使用したいときに以下のようなチェックボックスで有効化するイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/62e40d50-e377-535e-e2b9-e29a4c1d9898.png)

実際の使用イメージとして、suumoから物件情報を取得する例を記載します。
「選択モード開始」ボタンを押すと、Webサイトの要素にホバーすると赤枠が表示されるようになり、クリックするとその要素を選択できます。
選択された要素は赤枠表示されるとともに、画面下のテーブルに表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/cb36d6ef-1950-735d-f703-7f4fd8563ada.png)


「一括選択」ボタンを押すと、選択した要素と同じクラスを持つ要素を一括で選択できます。また、「↑」「↓」を押すとその方向にスクロールしながら情報を取得できるので、SNSなどの無限スクロールにも対応可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/ea51cb25-11d8-d595-7c26-5cf86140c580.png)


「コピー」ボタンを押すと選択された要素のテキスト情報をクリップボードにコピーできるので、Excelやスプレッドシートなどに貼り付けて使用することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/f262f466-171c-62c8-d89f-49b5e2c99125.png)


他の例として、Amazonのレビューを取得してみると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/63715f02-b171-89f9-1937-f601d67d9240.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/0aff76d1-4aab-3115-ece8-13a3827efb49.png)

定期的に取得したり、たくさんの情報をまとめて取得することには向きませんが、手軽に情報を取得するには便利かなと思います。

### プログラムの解説
プログラム全体については[GitHub](https://github.com/t-ujiie-g/scrapingExtension)をご確認ください。ここではメインの処理を担うcontent.jsのいくつかを解説します。

```js
      // 選択モードの変数
      let selecting = false;
      let selectedElement = null;
      let originalBorder = '';
      const selectedElementsSet = new Set(); // 追加: 選択された要素を追跡するセット
      let scrolling = false; // 追加: スクロール中かどうかを追跡するフラグ
      
      // 選択モードのイベントリスナーを追加
      shadow.querySelector('#selectButton').addEventListener('click', () => {
        selecting = !selecting;
        const selectButton = shadow.querySelector('#selectButton');
        if (selecting) {
          selectButton.textContent = '選択モード解除';
        } else {
          selectButton.textContent = '選択モード開始';
          if (selectedElement) {
            selectedElement.style.border = originalBorder;
            selectedElement = null;
          }
        }
      });
      
      // ホバー時の赤枠表示
      document.addEventListener('mouseover', (event) => {
        if (selecting) {
          if (popupContainer.contains(event.target)) {
            return; // 拡張機能の要素を無視
          }
          if (selectedElement) {
            selectedElement.style.border = originalBorder;
          }
          selectedElement = event.target;
          originalBorder = selectedElement.style.border;
          selectedElement.style.border = '2px solid red';
        }
      });
      
      // クリック時の要素確定
      document.addEventListener('click', (event) => {
        if (selecting) {
          if (popupContainer.contains(event.target)) {
            return; // 拡張機能の要素を無視
          }
          event.preventDefault();
          event.stopPropagation();
          selecting = false;
          shadow.querySelector('#selectButton').textContent = '選択モード開始';
      
          // テーブルに選択された要素を追加
          const tableBody = shadow.querySelector('#selectedElementsTable tbody');
          const newRow = document.createElement('tr');
          const newDataCell = document.createElement('td');
      
          newDataCell.textContent = selectedElement.textContent.trim();
      
          newRow.appendChild(newDataCell);
          tableBody.appendChild(newRow);
      
          // 一括選択ボタンとコピーボタンを有効化
          shadow.querySelector('#bulkSelectButton').disabled = false;
          shadow.querySelector('#copyButton').disabled = false;
      
          // 矢印ボタンを有効化
          shadow.querySelector('#scrollUpButton').disabled = false;
          shadow.querySelector('#scrollDownButton').disabled = false;
      
          // リセットボタンを有効化
          shadow.querySelector('#resetButton').disabled = false;
      
          // 選択された要素をセットに追加
          selectedElementsSet.add(selectedElement);
        }
      }, true);
```
ここで選択モードを実装しています。ボタンを押すと
+ ホバーすると赤枠が表示される
+ クリックすると選択される

といったことが実施されます。合わせて、クリック時にはその他のボタンの有効化（初期はdisabled）も実施しています。


```js
      // 一括選択ボタンのイベントリスナーを追加
      shadow.querySelector('#bulkSelectButton').addEventListener('click', () => {
        if (selectedElementsSet.size === 0) {
          alert('最初に要素を選択してください。');
          return;
        }
      
        const tableBody = shadow.querySelector('#selectedElementsTable tbody');
      
        selectedElementsSet.forEach(selectedElement => {
          const className = selectedElement.className;
          if (!className) {
            alert('選択された要素にはクラス名がありません。');
            return;
          }
      
          const elements = document.getElementsByClassName(className);
      
          for (let element of elements) {
            if (selectedElementsSet.has(element)) {
              continue; // 既に選択されている要素はスキップ
            }
            element.style.border = '2px solid red';
      
            // 新しい行を追加
            const newRow = document.createElement('tr');
            const newDataCell = document.createElement('td');
            newDataCell.textContent = element.textContent.trim();
            newRow.appendChild(newDataCell);
            tableBody.appendChild(newRow);
      
            // 選択された要素をセットに追加
            selectedElementsSet.add(element);
          }
        });
      });
```
このあたりで一括選択の実装をしています。
選択された要素と同じクラスを持つ要素を全て選択する、といった内容です。
※同じクラスを持つもの、という条件で実装したので、そもそもクラスを持たない要素はうまく取得できないことがあります。


```js
      // コピー機能のイベントリスナーを追加
      shadow.querySelector('#copyButton').addEventListener('click', () => {
        const tableBody = shadow.querySelector('#selectedElementsTable tbody');
        const rows = tableBody.querySelectorAll('tr');
        let clipboardText = '';
      
        rows.forEach(row => {
          const cells = row.querySelectorAll('td');
          const rowText = Array.from(cells).map(cell => cell.textContent).join('\t');
          clipboardText += rowText + '\n';
        });
      
        navigator.clipboard.writeText(clipboardText).then(() => {
          showToast(shadow); // トーストメッセージを表示
        }).catch(err => {
          console.error('クリップボードへのコピーに失敗しました: ', err);
        });
      });

      // トーストメッセージを表示する関数
      function showToast(shadow) {
        const toast = shadow.querySelector('#toast');
        toast.className = 'show';
        setTimeout(() => {
          toast.className = toast.className.replace('show', '');
        }, 2000); // 2秒後にトーストメッセージを非表示にする
      }
```
ここでコピーする機能を実装しています。コピー時はトーストメッセージが2秒ほど表示される仕様としました。
コピーではなくCSVファイル出力等も考えたのですが、単にクリップボードにコピーの方が様々なものに対して貼り付けをすぐに行えるので、個人的にはコピーの方が便利と感じています。

## まとめ
今回はじめてChrome拡張機能を開発してみました。想像よりも手軽に作れる印象を受けました。
Chrome拡張機能として開発する方が理に適っているものもあると感じたので、今後は選択肢の1つとして持っておきたいと思います。

なお、今回は簡単に使えることを念頭に置いて開発していますので、細かい制御や機能は実装できていません。もし興味がある方は自分好みにカスタムして使っていただければと思います。
ありがとうございました。