GASでプログラミング入門 ～応用編 Vol.3 リンクチェッカーを作ってみる～

:::note
この記事は Google Apps Script Advent Calendar 2021 15日目の記事です。
:::

https://qiita.com/advent-calendar/2021/google-apps-script

---

こんにちは！
LIFULLエンジニアの吉永です。

社内サークルにてエンジニアから非エンジニアの方向けにプログラミングを教えるという活動を行っています。
※私の自己紹介は下記のnoteをご参照ください。
https://note.com/lifull_yoshinaga/n/n1f70208fd237?magazine_key=mf24604fe44d6

今回はその教材応用編の第3弾です。
過去の記事一覧は下記です。

| 記事 | 内容 |
| ---- | ---- |
| [GASでプログラミング入門 Vol.1](https://qiita.com/YoshinagaYuta/items/7dfef1ef9fdf0e3d2f28) | 変数 |
| [GASでプログラミング入門 Vol.2](https://qiita.com/YoshinagaYuta/items/83f1709bee0a5ef10626) | 逐次・反復・分岐 |
| [GASでプログラミング入門 Vol.3](https://qiita.com/YoshinagaYuta/items/ef2779594c5a101e5dea) | 各種演算子 |
| [GASでプログラミング入門 Vol.4](https://qiita.com/YoshinagaYuta/items/05ed55663786218ad72a) | 関数 |
| [GASでプログラミング入門 Vol.5](https://qiita.com/YoshinagaYuta/items/f2601cffc1aa1498557e) | 関数の戻り値・コメント |
| [GASでプログラミング入門 Vol.6](https://qiita.com/YoshinagaYuta/items/c3cf64078edcd0df5fd7) | let・var・const |
| [GASでプログラミング入門 Vol.7](https://qiita.com/YoshinagaYuta/items/269d051f28d42bd7d979) | 配列 |
| [GASでプログラミング入門 Vol.8](https://qiita.com/YoshinagaYuta/items/c3a547ce6b49a8ae826f) | 連想配列 |
| [GASでプログラミング入門 Vol.9](https://qiita.com/YoshinagaYuta/items/6a2001cd33d46c323021) | null・break・論理演算子 |
| [GASでプログラミング入門 Vol.10](https://qiita.com/YoshinagaYuta/items/fd1d3892c49cfb520bd3) | switch・代入演算子 |
| [GASでプログラミング入門 ～応用編 Vol.1～](https://qiita.com/YoshinagaYuta/items/3b90449fdc7ecf8c0d5f) | スプレッドシートセル読みこみ |
| [GASでプログラミング入門 ～応用編 Vol.2～](https://qiita.com/YoshinagaYuta/items/e24cbae09aeaf05c1bd8) | スプレッドシートセル書きこみ |

## リンクチェッカーを作ってみる

今回は[応用編Vol.1](https://qiita.com/YoshinagaYuta/items/3b90449fdc7ecf8c0d5f)と[Vol.2](https://qiita.com/YoshinagaYuta/items/e24cbae09aeaf05c1bd8)の内容を組み合わせて、リンクチェッカーを作ってみたいと思います。
このリンクチェッカーなのですが、企画職の同僚がちょうどこのようなツールを欲しており、社内サークル活動の時間で勉強しながら作成できるならやってみたいということで作ることにしました。

### リンクチェッカー仕様

- リンクチェック対象となるURLにGASでアクセスして、HTTPの戻りステータスが2xx/3xx系ならOK、それ以外はNGという判定ができる
- チェック対象のURLはスプレッドシートで管理し、エンジニアでなくても簡単に追加や削除ができる
- リンクチェック結果はURLと一緒の行の別列で管理し、チェック日時とチェック結果を保存できる
- リンクチェックプログラムは定期的に実行できる


### スプレッドシートの列仕様

スプレッドシートは下記表の書式で用意します。

| 列 | 列名 |
| ---- | ---- |
| A1 | 確認URL |
| B1 | 遷移結果 |
| C1 | 最終チェック日 |

実際に作成してみたスプレッドシートのキャプチャは以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/3390d2f6-93f2-9656-1d9a-ae0c500c951a.png)

今回は動作確認用に下記のテスト用URLを登録しました。

```
https://example.com/
https://example.net/
https://example.org/
https://example.jp/
```

それでは上記スプレッドシートにGASのコードを追加していきましょう。

### まずはURL一覧をGASで取得してみる

Apps Scriptの画面を開いて下記コードを実行してみましょう。
※内容的には[応用編Vol.1](https://qiita.com/YoshinagaYuta/items/3b90449fdc7ecf8c0d5f)の復習です。

```js
function linkChecker() {
    // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート)
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート)
    const sheet = spreadsheet.getActiveSheet();
    // 指定するセルの範囲（A2～An）を取得
    const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1);
    // 値を取得
    const values = range.getValues();
    for (let i = 0; i < values.length; i++) {
        for (let j = 0; j < values[i].length; j++) {
            // リンクチェック対象のURL一覧を表示する
            console.log(values[i][j]);
        }
    }
}
```

※コードの内容を補足すると、`sheet.getLastRow()`ですが、これがスプレッドシート上でデータが入力されている行数を取得できるので、1行目の見出し行の分だけ行数を-1して有効な行の最終行までをまとめてA2～An行まで取得しています。
今回の例だと`sheet.getLastRow()`は5が返却されるので、2行目から4行取得するっていうのを`sheet.getRange(2, 1, sheet.getLastRow() - 1, 1)`で行っています。

実行すると下記のような表示が実行ログ欄に表示されると思います。

```
17:23:44	情報	https://example.com/
17:23:44	情報	https://example.net/
17:23:44	情報	https://example.org/
17:23:44	情報	https://example.jp/
```

表示が確認出来たらファーストステップは完了です。

### URLチェック日時とチェック結果をセルに書きこむ

続いて、リンクチェックした日時とチェック結果をセルに書き込みます
※内容的には[応用編Vol.2](https://qiita.com/YoshinagaYuta/items/e24cbae09aeaf05c1bd8)の復習です。

先ほどのコードにいくつか処理や関数を追加して、下記のコードの状態にします。

```js
function linkChecker() {
    // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート)
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート)
    const sheet = spreadsheet.getActiveSheet();
    // 指定するセルの範囲（A2～An）を取得
    const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1);
    // 値を取得
    const values = range.getValues();
    for (let i = 0; i < values.length; i++) {
        for (let j = 0; j < values[i].length; j++) {
            // リンクチェックを実行する
            let result = 'NG';
            if (linkCheckExec(values[i][j])) {
                result = 'OK';
            }

            // 現在の行番号を設定。
            // 見出し行分+1するのと、GASにおける行番号は1から始まる為、
            // ループカウンターに+2する必要がある
            let curRowNum = i + 2;

            // チェック結果をセルに書きこむ為、Bxセルを取得
            let writeRange = sheet.getRange(curRowNum, 2, 1, 1);
            writeRange.setValue(result);

            // 現在日時をセルに書きこむ為、Cxセルを取得
            writeRange = sheet.getRange(curRowNum, 3, 1, 1);
            writeRange.setValue(new Date());
        }
    }
}

// リンクチェック結果がOKならtrue、NGならfalseを返す
function linkCheckExec(url) {
    // ここで、リンクチェック実行する予定。
    // ひとまずモックで常にfalseを返すようにする
    return false;
}
```

`linkCheckExec`ですが、この後のステップで実際の処理を実装するので、今回はひとまずモックとして動作するように常にfalseを返してリンクチェック結果が失敗するようにしてあります。

```js
// リンクチェックを実行する
let result = 'NG';
if (linkCheckExec(values[i][j])) {
    result = 'OK';
}
```

ややこしいのは`curRowNum`ですね。
コメントにも書きましたが、見出し行を加味して+1行する必要があるのと、GASにおけるセルアクセス時の始点が0ではなく1から始まるのでループカウンターと、実際にアクセスしたい行番号には2のギャップがあるので、+2しています。

```js
// 現在の行番号を設定。
// 見出し行分+1するのと、GASにおける行番号は1から始まる為、
// ループカウンターに+2する必要がある
let curRowNum = i + 2;
```

実行するとスプレッドシートは下記のような内容に書き換わっていると思います。

| 確認URL | 遷移結果 | 最終チェック日 |
| ---- | ---- | ---- |
| https://example.com/ | NG | {GAS実行日時} |
| https://example.net/ | NG | {GAS実行日時} |
| https://example.org/ | NG | {GAS実行日時} |
| https://example.jp/ | NG | {GAS実行日時} |

表示が確認出来たら次のステップへ進みましょう。

### 実際にURLにアクセスして結果を得る

コードはこれで完成系になります。
`linkCheckExec`に具体的なアクセス処理と、アクセス結果の判定処理を組み込みました。

```js
function linkChecker() {
    // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート)
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート)
    const sheet = spreadsheet.getActiveSheet();
    // 指定するセルの範囲（A2～An）を取得
    const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1);
    // 値を取得
    const values = range.getValues();
    for (let i = 0; i < values.length; i++) {
        for (let j = 0; j < values[i].length; j++) {
            // リンクチェックを実行する
            let result = 'NG';
            if (linkCheckExec(values[i][j])) {
                result = 'OK';
            }

            // 現在の行番号を設定。
            // 見出し行分+1するのと、GASにおける行番号は1から始まる為、
            // ループカウンターに+2する必要がある
            let curRowNum = i + 2;

            // チェック結果をセルに書きこむ為、Bxセルを取得
            let writeRange = sheet.getRange(curRowNum, 2, 1, 1);
            writeRange.setValue(result);

            // 現在日時をセルに書きこむ為、Cxセルを取得
            writeRange = sheet.getRange(curRowNum, 3, 1, 1);
            writeRange.setValue(new Date());
        }
    }
}

// リンクチェック結果がOKならtrue、NGならfalseを返す
function linkCheckExec(url) {
    let res = false;
    if (url) {
        try {
            // URLが空文字などでなければアクセス実行
            let response = UrlFetchApp.fetch(url);
            let code = response.getResponseCode() / 100;
            if (code === 2 || code === 3) {
                res = true;
            }
        } catch (e) {
            // 例外エラー処理
            Logger.log(e)
        }
    }
    return res;
}
```

追記したコードを補足すると、まず引数で受取るurlという変数はスプレッドシートのセルの内容を飛び飛びで入力した場合などには空白文字になったりすることも運用上発生しうるので、有効な変数かのチェックを簡易的に行っています。

```js
// URLが空文字などでなければアクセス実行
let response = UrlFetchApp.fetch(url);
let code = response.getResponseCode() / 100;
if (code === 2 || code === 3) {
    res = true;
}
```

そして、問題なければ、上記の`UrlFetchApp.fetch`を使ってアクセスを実行し、`response.getResponseCode()`にHTTPの戻りステータスが格納されているので、100で割って百の位の数値が2か3の場合は成功扱いとしています。
※100で割る理由ですが、HTTPのステータスコードは2xxや3xxと表記されることもあるように、100の位で大枠のステータスを示しており、1や10の位が変わっても百の位さえ期待した数値であればアクセス結果の判断としては十分だからです。詳細はこちらの[RFC文書](https://datatracker.ietf.org/doc/html/rfc7231#section-6.1)を確認してください。

実行するとスプレッドシートは下記のような内容に書き換わっていると思います。

| 確認URL | 遷移結果 | 最終チェック日 |
| ---- | ---- | ---- |
| https://example.com/ | OK | {GAS実行日時} |
| https://example.net/ | OK | {GAS実行日時} |
| https://example.org/ | OK | {GAS実行日時} |
| https://example.jp/ | NG | {GAS実行日時} |

`https://example.jp/`だけが無効なURLなので遷移結果がNGとなっています。
これで最低限のリンクチェッカー機能の開発が完了しました。

### 定期実行設定

次にこのGASプログラムを定期実行する設定を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/e88cd8d7-4891-2542-390a-28501cbad6a0.png)
Apps Script画面の左メニューの時計アイコンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/638ec502-563a-3e86-a240-79cb79daf45a.png)
画面が切り替わったら、右下の青いボタン、トリガーを追加を押します。

表示される画面の各設定を下記表のようにします。

| 項目 | 設定 |
| ---- | ---- |
| 実行する関数を選択 | linkChecker |
| 実行するデプロイを選択 | Head |
| イベントのソースを選択 | 時間主導型 |
| 時間ベースのトリガーのタイプを選択 | 日付ベースのタイマー |
| 時刻を選択 | 午前9時～10時 |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/1d711799-c225-75b7-f00d-e7aec5a260a3.png)
最後に保存ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/3cfbb954-cf77-ef43-51bd-453140ef446b.png)
キャプチャのような表示になれば定期実行トリガーの作成完了です。

これで毎日午前9時～10時の間のどこかでリンクチェッカープログラムが定期実行されるようになります。

### 作成したトリガーの削除方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/3a94c6ea-cf6e-c499-fda1-6dc17c930458.png)

一応トリガーの削除方法も記しておきます。
先ほどのトリガー一覧画面から、任意のトリガー行の右端の3点ボタンを押して表示されるメニューからトリガーを削除を選択すれば削除できます。
一時停止とかはできないので、作成したスクリプトが動いてしまってはまずいなどの場合はトリガーごと削除するしかありませんが、トリガーの追加は先述した手順のように、非常に簡単なステップで追加できますので問題はないかと思います。

### まとめ

いかがでしたでしょうか？
今回はGASで業務を楽にする一例としてリンクチェッカーの作成手順を解説しました。
もう少し踏み込んで、エラー検知時にチャットツールへ通知するなんていう機能も盛り込んだら更に業務を楽にできそうですね。

それではまた次の記事でお会いしましょう。

## おまけ

いつもは冒頭に掲載している前記事の演習問題の解答です。

### 前回の演習問題の解答例

(1). for文を使用して下記のようにA1～A5セルに書き込まれるプログラムを記述してください。

```
ループ処理1回目
ループ処理2回目
ループ処理3回目
ループ処理4回目
ループ処理5回目
```

解答例コード

```js
function myFunctionVol2_1() {
    // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート)
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート)
    const sheet = spreadsheet.getActiveSheet();
    for(let i=1;i<=5;i++){
      // 行番号をループするごとに増やしていく
      let range = sheet.getRange(i, 1, 1, 1);
      range.setValue('ループ処理' + i + '回目');
    }
}
```

(2). 変数strを作成し、"条件式1"という文字列で初期化し、下記のようにB1セルに書き込まれるプログラムを作成してください。

```
条件式1の処理を実行
```

また、変数strの値を"条件式2"に変更した場合は下記のようになるようにしてください。

```
条件式2の処理を実行
```

解答例コード

```js
function myFunctionVol2_2() {
    // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート)
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート)
    const sheet = spreadsheet.getActiveSheet();
    // 指定するセルの範囲（B1）を取得
    const range = sheet.getRange(1, 2, 1, 1);
    let str = "条件式2";
    if (str == "条件式1") {
        range.setValue("条件式1の処理を実行");
    } else if(str == "条件式2") {
        range.setValue("条件式2の処理を実行");
    }
}
```