GASでWeb上のファイルを、ドライブへ保存する

GAS

※本記事はプロモーションを含みます

目次

業務システムやWebサイトから画像やPDFなどの各種アセットをバックアップしたり、外部サービスから提供されるCSVや帳票データを定期的に収集したりする際、Google Apps Script(GAS)を使って直接Googleドライブへ取り込みたい場面は日常的に発生します。GASはローカル環境を経由せずにクラウド上で完結させられるツールなので、定期トリガーと組み合わせれば完全自動化が可能になります。ただし、単純に外部URLを指定してリクエストを投げるだけでは、取得したファイルの拡張子が消えてしまうことがあります。というのは、意図しないHTMLエラーページが保存されたり、ファイル名が意図したものにならずにドライブ内の整理がつかなくなったりするトラブルが頻発するからです。

外部URLのファイルを直接ドライブへ流し込むときに起きる「名前と形式の不整合」

GASで外部URLの画像ファイルをダウンロードしてドライブへ保存する処理を組む際、最初に直面しやすいのは保存されたファイルがローカル環境で開けない問題です。なので、ドライブ上でプレビューが表示されないというトラブルも一緒に発生します。ブラウザのアドレスバーにURLを入力したときは正常に画像やPDFが表示されるので、GASからツールを実行しても同じようにファイルが落ちてくると思い込みがちです。ただし、内部的にはHTTPレスポンスの扱い方で大きな差が生じます。

Webサーバー側がファイルを返す際、URLの末尾に「.png」や「.pdf」といった拡張子が含まれていない動的エンドポイントになっているケースが多々あります。というのは、パラメーター付きのAPIリクエストや一時的な署名付きダウンロードリンクなどがこれに該当するからです。このようなURLに対してリクエストを送った場合、GASのレスポンスオブジェクトからそのままファイルを作成しようとすると、ファイル名が「download」などのデフォルト名になります。なので、拡張子が存在しないデータとしてドライブ上に配置されることになります。GoogleドライブはMIMEタイプ(データ形式の種類)をもとにプレビュー画面をレンダリングします。ただし、拡張子やMIMEタイプが正しく設定されていないBlob(バイナリデータ)を渡してしまうと、一般的なデータとして扱われます。なので、ダウンロードしてから拡張子を手動で付け直さないと開けないファイルが出来上がってしまいます。

また、URL先のサーバーがBasic認証を要求していたり、リファラ制限やユーザーエージェント制限をかけていたりする場合があります。なので、一時的なセッショントークン切れでログイン画面へリダイレクトされている場合もあります。このときGASは、エラー画面のHTMLテキストを正常レスポンス(HTTPステータス200)として受け取ってしまうことがあります。ただし、プログラム側でステータスコードやContent-Typeの検証を行わずにファイルとして保存してしまうと、壊れたファイルがドライブ内に蓄積されていきます。というのは、画像の名前がついているのに中身はHTMLソースコードになってしまうからです。こうしたズレを防ぐためには、HTTPレスポンスからBlobオブジェクトを取り出す段階で、適切なファイル名と形式を明示的に割り当てましょう。なので、通信結果が正常であるかを確認する手順も不可欠になります。

UrlFetchAppで取得したバイナリをBlobとしてドライブに格納する

Web上のファイルデータをGASで取得してドライブへ保存するための最小限の基本構成は、UrlFetchApp.fetch()で対象のURLへアクセスする流れになります。なので、得られたレスポンスからBlobオブジェクトを抽出してファイル化します。fetch結果のBlobを取得し、拡張子とファイル名を指定してDriveへ保存しましょう。ただし、この手順を踏むことで破損のない正常なファイルとしてドライブ上に配置できます。

function saveExternalFileToDrive() {
  const url = "https://example.com/image.png";
  const response = UrlFetchApp.fetch(url);
  const blob = response.getBlob().setName("downloaded_image.png");
  DriveApp.getRootFolder().createFile(blob);
}

上記の処理において中核となるのはresponse.getBlob()というメソッドです。このメソッドはHTTPレスポンスのボディ部をバイナリデータ(Blob)として変換します。なので、その直後にsetName("downloaded_image.png")を呼び出すことで、ドライブ上で認識されるファイル名を明示的に上書きしています。ここでのポイントは、取得した直後のBlobがURL末尾の文字列やサーバーの返したヘッダーに依存した名前を持っていることです。ただし、保存前にプログラム側で拡張子を含めた完全な名称を確定させています。

この基本コードを使うべき場面は、単発のファイルバックアップを行うタイミングです。なので、URLが確実に静的な画像やファイルを指していると分かっている環境で使いましょう。ただし、この処理をそのまま実運用に回すと、通信エラーのハンドリングがありません。というのは、相手先サーバーが500エラーや404エラーを返した瞬間にスクリプト全体がクラッシュして、後続の処理が止まるからです。また、保存先がDriveApp.getRootFolder()となっているので、実行するたびにマイドライブの直下にファイルが散らばります。なので、他のスプレッドシートやドキュメントと混ざって管理不能に陥ります。実務で運用する際は、例外処理を組み込んだ上で、特定の保存先フォルダを指定する形へ拡張していきましょう。

スクレイピングや外部APIから取得したメディアファイルをドライブに蓄積する際の注意点

外部アセット保存の用途として、スクレイピング(Web情報の自動収集ツール)や外部APIから取得したメディアファイルをドライブに蓄積できます。スプレッドシートに記載された大量の画像URLリストを巡回してドライブへ一括保存するケースがこれに当たります。なので、ECサイトの商品情報更新に合わせて商品画像をダウンロードするケースも該当します。ただし、複数ファイルを連続してダウンロードする処理には、GAS特有の制約とWeb通信特有のボトルネックが幾重にも重なります。

まず意識すべきなのは、GASの実行時間制限です。通常のGoogleアカウントでは1回のスクリプト実行上限が6分間に設定されています。Webからのファイルダウンロードは、相手先サーバーの応答速度やネットワーク帯域に大きく依存します。なので、数十メガバイトのファイルや数百件の画像URLを1回のループで処理しようとすると、確実にタイムアウトを迎えます。ただし、途中でスクリプトが強制終了した場合でも、どのファイルまで保存が完了してどこからが未処理なのかを追跡できるようにしましょう。というのは、スプレッドシート側へ処理結果ステータスを書き戻す設計にしておかなければ、次回実行時に重複してダウンロードしてしまうからです。

さらに、短時間に連続して外部URLへアクセスを行うと、相手側のサーバーからDoS攻撃とみなされてアクセス制限を受けたりIPブロックを食らったりする危険があります。GASの実行環境はGoogleの共有IPアドレスから通信が行われます。なので、同一IPからの急激なリクエスト急増は相手サーバーのセキュリティシステムに検知されやすい傾向があります。スクレイピングや一括収集を行う際は、Utilities.sleep()を挟んで適切なインターバルを設けましょう。ただし、一度に処理する件数を数十件程度に制限してトリガーで段階的に実行する工夫も必要です。

加えて、GASの1日あたりのUrlFetch呼び出し容量やDriveへの書き込み制限にも注意が必要です。巨大なZIPファイルや高解像度動画などをGAS経由で無理にドライブへダウンロードしようとすると、メモリ不足エラーで異常終了します。というのは、GASのBlobが扱えるデータサイズには実質的な制限があるからです。なので、巨大なファイルはGASを中継させず、別のアプローチを検討しましょう。

フォルダの階層指定と二重保存を防ぐ実務での分岐処理

マイドライブ直下にファイルを保存し続けると、ドライブの検索性や整理が著しく悪化します。実務では必ず用途に応じた保存先フォルダを作成または特定しましょう。なので、そのフォルダに対してcreateFile(blob)を実行する形に整える必要があります。ただし、定期トリガーで巡回保存する場合、同じURLのファイルが実行のたびに重複して新規作成されてしまいます。というのは、ドライブの容量を無駄に圧迫するトラブルが非常に多く見られるからです。

Googleドライブは一般的なローカルファイルシステムと異なり、同一フォルダ内に全く同じ名前のファイルが何個でも同居できる仕様を持っています。なので、ファイル名の重複チェックを行わずに保存処理を実行すると、同じ名前の画像ファイルが数十個も生成されてしまいます。これを回避するためには、ファイルを生成する前に保存先フォルダ内を検索しましょう。ただし、同名のファイルが既に存在している場合は処理をスキップするか、上書きするロジックを挟む必要があります。

以下に、指定したIDのフォルダへ保存しつつ通信エラーを適切に処理して、同名ファイルの二重作成を防ぐ実装例を記載します。

function downloadAndSaveUniqueFile() {
  const fileUrl = "https://example.com/assets/report.pdf";
  const folderId = "YOUR_FOLDER_ID_HERE";
  const targetFileName = "monthly_report_2024.pdf";

  const folder = DriveApp.getFolderById(folderId);
  
  // 同名ファイルの存在確認
  const existingFiles = folder.getFilesByName(targetFileName);
  if (existingFiles.hasNext()) {
    Logger.log("既に同名のファイルが存在するため保存をスキップしました: " + targetFileName);
    return;
  }

  try {
    const response = UrlFetchApp.fetch(fileUrl, {
      muteHttpExceptions: true,
      headers: {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
      }
    });

    const statusCode = response.getResponseCode();
    if (statusCode !== 200) {
      Logger.log("ダウンロード失敗 HTTPステータス: " + statusCode);
      return;
    }

    const blob = response.getBlob().setName(targetFileName);
    const newFile = folder.createFile(blob);
    Logger.log("ファイルを保存しました: " + newFile.getName() + " (ID: " + newFile.getId() + ")");

  } catch (e) {
    Logger.log("通信エラーまたはファイル保存エラー: " + e.toString());
  }
}

この構成では、muteHttpExceptions: trueを指定しています。なので、接続先サーバーが404や500といったHTTPエラーコードを返してきた場合でも、スクリプトが例外を吐いて停止しません。ただし、後続のステータスコード判定で安全に処理を分岐できるようにしています。また、folder.getFilesByName(targetFileName)を用いて事前に保存先フォルダ内をスキャンしましょう。というのは、既に同名ファイルが存在しているときは保存を行わずに終了させられるからです。これにより、スプレッドシートの行ごとにトリガー実行を繰り返すような運用でも、無駄なファイル増殖を確実に抑え込むことができます。

レスポンスヘッダーのContent-Type判定と拡張子の自動補正

外部URLから画像やファイルを収集する業務では、事前に保存すべきファイル名や拡張子が分かっていないケースがあります。例えば、URLがパラメータ形式になっており、末尾に拡張子が含まれていない場合です。このような動的URLからダウンロードを行う場合、ファイル名を静的に固定してしまうと問題が起こります。というのは、サーバー側がPNGを返してきたのかJPEGを返してきたのか判別できず、誤った拡張子を付けて保存してしまうリスクがあるからです。

同じシート操作の続きは、Googleスプレッドシートの外部APIが落ちたとき、少し待って再実行する に書いてあります。

タイトルとURLをコピーしました