【コードつき】GASでよくある利用例5選（外部リクエスト編） 

Google Apps Script（以下GAS）は、Googleの各種サービスを自動化するための非常に便利なツールです。特に、外部リクエストができるので、API等を通じて、データの取得や送信を行う場面は多々あります。本記事では、外部リクエストのよくある利用例を5つ紹介し、それぞれのコードを具体的に解説します。実際のプロジェクトでの利用をイメージしやすいように、各コードにはコメントも付けてありますので、ぜひ参考にしてください。

## 利用例1　外部APIを使う
外部API等にリクエストしてデータを取得・送信するケースがよくあるかと思います。  
世の中APIは、REST API であることが多いので、ここでは [JSONPlaceholder](https://jsonplaceholder.typicode.com/)というテスト用のREST APIサービスを使って、以下のリクエストするコードをご紹介します。

- 投稿一覧の取得
- 投稿の作成
- 投稿の更新
- 投稿の取得
- 投稿の削除

#### 投稿一覧の取得
```
function listPosts() {
  // 投稿一覧を取得するURL
  const url = 'https://jsonplaceholder.typicode.com/posts';
  const res = UrlFetchApp.fetch(url, {
    method: "get", // methodは get = 取得
    muteHttpExceptions: true // false（デフォルト）の場合、レスポンスのステータスコードが200番代以外だと例外が投げられてしまう
  });
  
  // 投稿一覧が取得できた場合（ステータスが200 OK）
  if (res.getResponseCode() === 200) {
    const posts = JSON.parse(res.getContentText());
    console.log(posts);
    return;
  }
  // 投稿一覧の取得が失敗したときの処理をする
};
```

#### 投稿の作成
```
function createPost() {
  const url = `https://jsonplaceholder.typicode.com/posts`;
  const res = UrlFetchApp.fetch(url, {
    method: "post", // methodは post = 作成
    muteHttpExceptions: true, // false（デフォルト）の場合、レスポンスのステータスコードが200番代以外だと例外が投げられてしまう
    contentType: "application/json", // APIがJSON形式の場合は必ずいれる
    payload: JSON.stringify({
      title: 'foo',
      body: 'bar',
      userId: 1,
    }), // APIがJSON形式なのでオブジェクトをJSON化する
  });

  // 投稿が作成できた場合（ステータスが201 Created）
  if (res.getResponseCode() === 201) {
    const newPost = JSON.parse(res.getContentText());
    console.log(newPost);
    return;
  }
  // 投稿の作成が失敗したときの処理をいれる
};
```

#### 投稿の更新
```
function updatePostById(id) {
  const url = `https://jsonplaceholder.typicode.com/posts/${id}`;
  const res = UrlFetchApp.fetch(url, {
    method: "put", // methodは put = 更新
    muteHttpExceptions: true, // false（デフォルト）の場合、レスポンスのステータスコードが200番代以外だと例外が投げられてしまう
    contentType: "application/json", // APIがJSON形式の場合は必須
    payload: JSON.stringify({
      title: 'foo',
      body: 'bar',
      userId: 1,
    }),
  });
  if (res.getResponseCode() === 200) {
    return;
  }
  // 投稿の更新が失敗したときの処理をいれる
};
```

#### 投稿の取得
```
function getPostById(id) {
  const url = `https://jsonplaceholder.typicode.com/posts/${id}`;
  const res = UrlFetchApp.fetch(url, {
    method: "get", // methodは get = 取得
    muteHttpExceptions: true, // false（デフォルト）の場合、レスポンスのステータスコードが200番代以外だと例外が投げられてしまう
  });
  
  // 投稿一覧が取得できた場合（ステータスが200 OK）
  if (res.getResponseCode() === 200) {
    const post = JSON.parse(res.getContentText());
    console.log(post);
    return;
  }
  // 投稿の取得が失敗したときの処理をいれる
};
```

#### 投稿の削除
```
function deletePostById(id) {
  const url = `https://jsonplaceholder.typicode.com/posts/${id}`;
  const res = UrlFetchApp.fetch(url, {
    method: "delete", // methodは delete = 削除
    muteHttpExceptions: true, // false（デフォルト）の場合、レスポンスのステータスコードが200番代以外だと例外が投げられてしまう
  });
  // 投稿が削除できた場合（ステータスが200 OK）
  if (res.getResponseCode() === 200) {
    return;
  }
  // 投稿の削除が失敗したときの処理をいれる
};
```

外部リクエストする際に知っておくといいのが、 `UrlFetchApp.fetch` のオプションである `muteHttpExceptions` についてです。
デフォルトの値は `false` なのですが、`false` の場合、レスポンスのステータスコードが200番代以外のときに、例外がスローされ、API側からのレスポンスがわからない状態になります。（以下は存在しないURLにリクエストしたときの例外のメッセージ）

`Exception: Request failed for https://jsonplaceholder.typicode.com returned code 404. Truncated server response: {} (use muteHttpExceptions option to examine full response)`

そのため、`muteHttpExceptions` は `true` にしておくことで、レスポンスに応じて細かいエラーハンドリングができるようになります。（今回のコードサンプルでは省略しています）

## 利用例2　外部に公開されている画像をGoogleドライブに保存する
SNSの投稿の画像等、外部に公開されている画像をGoogleドライブに保存したいケースがよくあるかと思います。
ここでは [placehold.jp](https://placehold.jp/)というプレースホルダー作成サービスの [https://placehold.jp/150x150.png](https://placehold.jp/150x150.png) にある画像を取得するコードをご紹介します。

```
function saveImage() {
  // 保存したい画像のURL
  const url = "https://placehold.jp/150x150.png";
  const res = UrlFetchApp.fetch(url, {
    method: "get",
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() === 200) {
    const blob = res.getBlob() // getBlobメソッドを使うことで、レスポンスをファイルとして扱うことができる
    const folder = DriveApp.getRootFolder() // 画像を保存するフォルダ（この場合はルートフォルダ
    const file = folder.createFile(blob);
    file.setName("150x150");
    console.log(file.getUrl());
    return;
  }
  // 画像の取得が失敗したときの処理をいれる
}
```

外部APIを使う場合と書き方はほとんど変わりませんが、レスポンスをファイルとして扱うために、 `getContentText()` ではなく `getBlob()` を使っています。

## 利用例3　スクレイピングでデータを取得する
いくつかやり方はあるのですが、[Cheerio](https://github.com/tani/cheeriogs)というライブラリを使うのがおすすめです。導入の仕方は以下です。

1. GASのエディタのサイドメニューの「ライブラリ」をクリック
![Frame 53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794884/8e3a0dee-9d56-7420-98d3-6d890ebd0ed6.png)
2. スクリプトIDに `1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0` を入力し、「追加 」をクリック
![Frame 54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794884/0bba56a2-f5be-ebfa-5b7b-268092fb019e.png)

Cheerioを使って、wikipediaの[このページ](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8)
の「ウィキペディアへようこそ」というタイトルを取得してみます。
```
function scraping() {
  const url = "https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8";
  const res = UrlFetchApp.fetch(url, {
    method: "get",
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() === 200) {
    const $ = Cheerio.load(res.getContentText()); // 返ってきたレスポンスを読み込む
    const el = $("#welcome"); // cssセレクタを使って取得したいデータを含む要素を取得する
    const pageTitle = el.text().trim();
    console.log(pageTitle); // 「ウィキペディアへようこそ」と出力される
  }
}
```
なお、Chromeブラウザを使ったcssセレクタの取得方法としては以下です。
1. 取得したい要素にカーソルをあて右クリックし、「検証」をクリック
![スクリーンショット 2024-05-19 21.43.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794884/7a5098e5-de33-1916-3400-f080742b6d9d.png)
2. （Chromeの開発者ツールを開くので）、要素の左の「･･･」をクリックし、Copy > Copy Selector をクリック
![スクリーンショット 2024-05-19 21.44.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794884/202d13ae-3101-e8c3-7c1a-b5fac73f3e98.png)
3. クリップボードにセレクタがコピーされているので、コード上に貼り付ける

ちなみに、スクレイピングのマナーとしてアクセス頻度は1秒間に1回というものがありますが、GASでスリープをいれるには以下のようにします。
```
Utilities.sleep(1000); // ミリ秒指定
```

## 利用例4　最大実行時間の回避する
GASには最大の実行時間が6分間という制約があり、6分以上の処理だとタイムアウトエラーになってしまい、処理が途中で強制終了してしまいます。
外部APIを使ってページネーションがとても多いとき等にこの6分の壁を超えないといけない場面があります。
いくつかやり方はあるかと思いますが、1つの例を紹介します。

まず以下のような関数を作ります。第一引数: `timeoutInMs` がタイムアウトするまでの時間（ミリ秒）、第二引数: `fn` が6分以上かかる可能性のある処理が入った関数です。
```
const timeoutError = new Error("time out error")
const runWithTimeout = (timeoutInMs, fn) => {
  const since = new Date()
  const isTimeout = () => {
    if (new Date().getTime() - since.getTime() >= timeoutInMs) {
      throw timeoutError
    }
    return false
  }
  fn(isTimeout)
}
```
この関数は以下のように使います。
```
function main() {
  try {
    // サンプルコードなので timeoutInMs には 1秒（ミリ秒）を指定している
    // 本番で使う場合は、1000 * 60 * 5.5 で5分半とかを設定するとよい
    // 6分よりも短い時間を設定し、既存の処理の保存、次の処理のトリガー設定を行う
    runWithTimeout(1000, (isTimeout) => {
      // 6分以上かかると想定される処理（APIのページネーション等）

      // サンプルコードなので適当なループ処理
      let done = false;
      let count = 0;
      while (!done) {
        count++;
        console.log(count);
        if (count > 100000) {
          done = true;
        }
        
        // 繰り返し処理が終わるごとにタイムアウトしていないかチェックする
        // timeoutInMs で渡している（ここでは1秒）を過ぎてたら、例外 timeoutError がスローされる。
        isTimeout();
      }
    });
  } catch (e) {
    // timeoutError のときだけ次の実行のトリガーをセットする
    if (e === timeoutError) {
      const oneMinuteLater = new Date(new Date().getTime() + 60000);
      const trigger = ScriptApp.newTrigger("main").timeBased().at(oneMinuteLater).create();
      // ここで設定したトリガーのIDはスクリプトプロパティ等に保存しておき、のちほど実行される処理で消すとよい（不要なトリガーが溜まっていくのを防げる）
      console.log(trigger.getUniqueId());
      console.log("処理を中断し、1分後に再度実行します");
      return;
    }
    // timeoutError以外のときはエラーハンドリングをする
  }
}

```
ここではサンプルのため、ただの `while` ループ処理ですが、実際は外部APIのページネーションなど、繰り返し処理が発生する場合に、その繰り返しごとに `isTimeout` 関数を実行します。
第一引数である `timeoutInMs` として渡している時間（ここでは1秒）を過ぎてたら、例外 `timeoutError` がスローされるので、catch() 内で `timeoutError` のときは次回実行用のトリガーを作成する。
なお、外部APIを叩く場合は、ページネーションの情報もどこかに保存し、次の処理で引き継ぐ必要があります。

## 利用例5　定期的にスクリプトを実行させる
[ドキュメント](https://developers.google.com/apps-script/reference/script/clock-trigger-builder?hl=ja)をみればわかる話かもしれませんが、コピペ用にサンプルコードを載せておきます。
```
  // 毎日9時に実行
  ScriptApp.newTrigger("main").timeBased().atHour(9).everyDays(1).create();

  // 1時間おきに実行
  ScriptApp.newTrigger("main").timeBased().everyHours(1).create();

  // 指定時刻に実行（2024年7月1日）
  ScriptApp.newTrigger("main").timeBased().at(new Date(2024, 6, 1)).create();
```
作成したトリガーのIDはどこかに保存しておくと、のちにプログラムからそのトリガーを消すこともできて便利です。
```
const trigger = ScriptApp.newTrigger("main").timeBased().atHour(9).everyDays(1).create();
const id = trigger.getUniqueId();

// トリガーのIDをもとにトリガーを割り出し、削除することもできる
const existingTrigger = ScriptApp.getProjectTriggers().find((trigger) => trigger.getUniqueId() === id);
if (existingTrigger) {
  ScriptApp.deleteTrigger(trigger);
}
```
## まとめ
以上、GASでよくある利用例5選とその具体的なコードを紹介しました。GASを活用することで、様々なタスクを自動化し、作業の効率を大幅に向上させることができます。今回紹介した5つの利用例は、GASで外部リクエストをする上でよくあるシチュエーションです。ぜひ、これらのコードを参考にして、自分自身のプロジェクトに応用してみてください。GASの可能性は無限大ですので、新たなアイデアをどんどん試してみてください。

## 宣伝
GASラボでは、便利な自動化ツール（GAS製・100種類以上）、新規サービス、テンプレートを開発・公開しております。
最新情報については、Twitter(X）でも配信しておりますので、是非チェックしていただけると幸いです。

[【GASラボ】Twitter（X）アカウント](https://x.com/gas93740656?ref_src=twsrc%5Etfw%7Ctwcamp%5Eembeddedtimeline%7Ctwterm%5Escreen-name%3Agas93740656%7Ctwcon%5Es1_c1)