GASを使ったスクレイピングのやり方

# はじめに
この記事は「[Ateam LifeDesign Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ateam-life-design)」で完走賞を狙って25記事書いているうちの3日目の記事です。今年も完走目指して頑張るぞ！

# GASを使ったスクレイピング
今私が見てる部署では比較的スクレイピングを使う人が多いので参考になるかもな？と思い、実践編の最初にスクレイピングを持ってきました。それでは早速見ていきましょう。

# 作るもの
こんなシートを用意してA列に入っているURLにアクセスし、取ってきた情報で必要なものをB列に入れていきます。

![スクリーンショット 2023-11-20 11.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/b5c606eb-0365-623d-a358-9ba86eba72a0.png)

今回スクレイピングするのは弊社が運営してる[結婚式場の紹介サービスであるハナユメ](https://hana-yume.net/)というサイトです。結婚式場の詳細が書かれたページのなかから赤枠で囲われている情報を取得してきたいと思います。

![スクリーンショット 2023-11-20 11.17.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/7b308ca0-a2ff-1c60-7d98-69b8e15a926e.png)

# 準備

今回は[Cherrio](https://github.com/tani/cheeriogs)というライブラリを利用したスクレイピングの方法をご紹介します。

まず「ライブラリ」をクリックしたあと、スクリプトIDに

`1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0`

を入力し、最新のバージョンのCheerioを追加します。

![スクリーンショット 2023-11-20 9.38.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/2db385dd-30b5-3065-c1ca-2984e3ee362d.png)

Cherrioの使い方は[こちら](https://github.com/tani/cheeriogs#usage)に詳しく書いてありますがjQueryライクに要素を取ってくることが出来ます。こんな感じで簡単にコンテンツを取ってこれます。

```javascript
function getContent_(url) {
    return UrlFetchApp.fetch(url).getContentText()
}
```
wikipediaのメインページを取得する方法。
```javascript
  const content = getContent_('https://en.wikipedia.org');
  const $ = Cheerio.load(content);
  Logger.log($('#mp-right').text());
```

これでCherrioライブラリが使用できるようになったので、ここからは実際どんなコードを書いて実装していくのか順を追って説明していきます。

# GASを書いていく
## URLが入ってるA列のセルの中身を取得

```javascript
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet  = spreadsheet.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var range  = sheet.getRange(1,1, lastRow);
  var values = range.getValues().flat().filter(String).map(String);
```

ここは主に[昨日の記事](https://qiita.com/kakakaori830/private/388fbadbd80f14a2637a)でやった内容です。`sheet.getLastRow()`の部分が新しく出てきていますが、この関数はそのシートでデータの入ってる最終行数を返す関数です。サンプルだとURLが５つはいってるので`sheet.getRange(1,1,5)`としても動作しますが、この書き方だとURLをどんどん追加していったときに都度プログラム側を変更する必要があるのであまり美しくないですよね。なので最終行を取ってこれる関数を使って指定をするようにしています。

`range.getValues().flat().filter(String).map(String)`の部分は二次元配列をその後の処理で扱いやすいように一次元配列にしています。今回の場合入力されてるのは文字列型なので`String`を指定していますが、数値型であれば`Number`を指定しましょう。

## 取れてきたURLを１つずつ順番にスクレイピングする

```javascript
  for(let i = 0; i < values.length; i++) {
    var url = values[i];
    if (url) {
      var content = UrlFetchApp.fetch(url).getContentText();
      var $ = Cheerio.load(content);
      var targetText = $('.hallStatus').text();
      setInfo[i] = [];
      setInfo[i].unshift(targetText.replace(/[\r\n]+/g,""));
      Utilities.sleep(3000);      
    }
  } 
```
A列から取ってきたURLのリストは先程の手順で一次元の配列になっているので今回はforで１つずつ見ていきます。URLが空の場合にスクレイピングの処理を通そうとするとエラーが出てしまうので空欄があったときのことを考慮して`if`でURLがセルに記載されている場合のみスクレイピング処理を実行するようにします。実際のスクレイピングは上述のサンプルの通りです。なんともめちゃくちゃ簡単・・・感動・・・！
![スクリーンショット 2023-11-20 22.01.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/9813ec81-dfe8-9d2c-a1ec-3758eec0f52f.png)
今回取ってきたかったところには`hallStatus`というclassが振られていたので`.hallStatus`という指定をしています。もしこれが`id="hallStatus"`であれば`$('#hallStatus').text()`と指定すればOK。もっと細かな指定もできますが、ここでは割愛。jQueryのDOMの指定方法と同じなので[jQuery DOM操作あれこれ](https://qiita.com/nishiurahiroki/items/5fe52bbcbb91d3181bbd)がとても参考になると思います。

そのまま`targetText`を入れていたのでが改行コードが含まれていたため`.replace(/[\r\n]+/g,"")`を追加して改行コードを空文字で置換しています。

そしてスクレイピングはお行儀よくしないとなので`Utilities.sleep(3000);`で３秒間処理を止めています。大量アタックはサイト運営者にご迷惑おかけしちゃうからね。`Utilities.sleep(ミリ秒)`という指定の仕方をしてください。

## スクレイピングして得られた結果を書き込む

```javascript
  var outputRange = sheet.getRange(1, 2, lastRow);
  outputRange.setValues(setInfo);
```

ここでは取得してきたデータを二次元配列で保持しておいてそれを`setValues`をつかって書き込んでいます。これだけのデータ量であればfor回している中で１セルずつに対して`setValue`しても、最後にまとめて`setValues`しても速度はそこまで変わらないのですが、データ行数が数百レベルになってくると実行結果に明らかな差が出てきます。
GASには１度の処理での実行時間に上限があるのでなるべくまとめてデータをとりだす、まとめてデータを書き込むを身に着けておいたほうがいいので今回はまとめて書き込む方法でサンプルを作成しました。

それではここまでをまとめたものと実際の実行結果を見てみましょう！

# 完成品がこちら

## GASのコード
```javascript
function myFunction() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet  = spreadsheet.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var range  = sheet.getRange(1,1, lastRow);
  var values = range.getValues().flat().filter(String).map(String);
  var setInfo = [];

  for(let i = 0; i < values.length; i++) {
    var url = values[i];
    if (url) {
      var content = UrlFetchApp.fetch(url).getContentText();
      var $ = Cheerio.load(content);
      var targetText = $('.hallStatus').text();
      setInfo[i] = [];
      setInfo[i].unshift(targetText.replace(/[\r\n]+/g,""));
      Utilities.sleep(3000);      
    }
  }  

  var outputRange = sheet.getRange(1, 2, lastRow);
  outputRange.setValues(setInfo);
}
```

## 実行結果
無事に赤枠で囲っていた部分を取得してB列に書き込めていますね。

![スクリーンショット 2023-11-20 21.45.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/82fe0a84-3fe5-c7a0-718d-5057f5c4f0fd.png)


# 最後に
今回はCheerioライブラリを使ってGASでスクレイピングするやりかったについてまとめました。思ったより簡単にできましたね！明日は弊社でもよく使われそうなSlack連携について書いていこうと思います。
くれぐれもスクレイピングは迷惑のない範囲で使いましょうね！