MENU

【検証】GASでローカルファイルを取り込む|Drive経由方式でアップロード→自動変換→転記する

GASでローカルファイルをDrive経由で取り込む検証|GAS
目次

はじめに

VBAではApplication.GetOpenFilenameFileDialogを使えば、ローカルPCや社内サーバー上のファイルを直接指定して開けます(詳しくは【Excel VBA】ファイルを開いてデータを取得する(その1)(その2)を参照)。ところがGASにはこれに相当する仕組みがありません。GASはブラウザ上で動くサンドボックス環境のため、スクリプト側からローカルPCのファイルシステムに直接アクセスすることができないためです。

そこで今回は、GASでローカルファイルを取り込むための代替アプローチとして、「HTMLフォームでファイルを選択→Googleドライブに一時アップロード→スプレッドシート形式に自動変換→中身をコピー→一時ファイルを削除」というDrive経由方式を実装・検証します。

ダイアログ方式の記事で使用した HtmlService と、新たに Drive API(Advanced Google Services)を組み合わせて利用します。

Drive経由方式とは?全体像

今回実装する流れは次のとおりです。

① ファイル選択:HTMLフォームの <input type=”file”> でローカルファイルを選択
② アップロード:選択したファイルをBase64文字列に変換し、google.script.run でサーバー側に送信
③ Driveへのアップロード&自動変換:
受け取ったBase64データからBlobを作成し、Drive API(Advanced Google Services)で
「Googleスプレッドシート形式」を変換先に指定してアップロード
→ アップロードと同時にGoogleスプレッドシートへ自動変換される
④ 中身をコピー:変換されたスプレッドシートを開き、getDataRange().getValues() でデータを取得
→ 取り込み先シートに setValues() で書き込み
⑤ 後片付け:Drive上に残った変換済みの一時ファイルを削除

ローカルファイルを直接読み書きできない代わりに、「いったんGoogleドライブを経由させる」という発想がポイントです。次回の記事では、Driveを経由せずブラウザ側のJavaScriptライブラリでその場でファイルをパースするクライアント側パース方式を検証する予定です。その後、両方式を比較する記事も作成しようと思っています。

準備①:Advanced Drive Serviceを有効にする

DriveApp(GASの標準サービス)でもアップロード自体は可能ですが、アップロードと同時にGoogleスプレッドシート形式へ変換する処理はできません。「Advanced Google Services」の一つ、Drive APIを有効にする必要があります。

  1. Apps Scriptエディタ左側の「サービス」の横にある「+」をクリック
  2. 一覧から「Drive API」を選択し、バージョンは「v3」を選んで「追加」をクリック
  3. 左側のファイル一覧にDriveが追加されれば完了
Apps ScriptエディタでDrive APIサービスを追加する画面

注意:v2で追加するとDrive.Files.createが存在しません。 v2ではファイル作成のメソッド名がDrive.Files.insertに変わるためです(本記事のコードはcreateを使うv3前提)。

さらに注意:バージョン選択プルダウンに「v2」しか表示されない場合があります。 その場合はマニフェストファイル(appsscript.json)を直接書き換えて強制的にv3へ切り替えられます。⚙️(プロジェクトの設定)→「”appsscript.json” マニフェスト ファイルをエディタで表示する」にチェック→ファイル一覧のappsscript.jsonを開き、enabledAdvancedServices内の"version": "v2""version": "v3"に書き換えて💾保存します。

appsscript.jsonでDrive APIのバージョンをv3に書き換える

実機で効果を確認済みです。 この「v3が選択肢に出てこない」現象はプロジェクトによって発生したりしなかったりするようで、同じGoogleアカウント・同じ手順でも再現性が一定しません(最初のプロジェクトでは問題なくv3を選べましたが、検証用に別プロジェクトで追加し直すとv2しか出ませんでした)。GAS側の表示不具合の可能性があります。

VBAでいうと、「ツール→参照設定」で外部ライブラリを追加する操作に近い感覚です。コード上ではDrive.Files.create(...)のようにDriveオブジェクト経由でAPIを呼び出せます。

初回実行時の権限承認: あらかじめDrive APIを追加した状態で初めて実行した場合は、通常の権限承認フロー(Googleアカウント選択→アクセス許可の確認)だけで進めました。GAS最初の記事のときと同様、特別な操作は不要でした。

なお検証中、いったんスクリプトを承認済みの状態からDrive APIを後付けで追加したケースでは、「認証が必要です」(「このドキュメントに添付されたスクリプトを実行するには、あなたの許可が必要です。」)という再承認用のダイアログが別途表示されました。これは新しく追加したDrive APIのアクセス権限を承認し直すためのもので、「わかりました」を押すだけで完了しました(アカウント選択や許可確認の画面は改めて表示されませんでした。おそらく元のスクリプト自体はすでに承認済みだったためと考えられます)。先にDrive APIを追加してから初回実行する場合は、この再承認ダイアログ自体が出ずに済みます。

初回実行時の「認証が必要です」ダイアログ

準備②:Apps ScriptエディタにHTMLファイルを追加する

アップロード用のフォームをダイアログとして表示するため、【検証】GASでHtmlServiceダイアログの進捗表示を実装するの記事と同じ手順でHTMLファイルを追加します。

  1. スプレッドシートのメニュー「拡張機能」→「Apps Script」でスクリプトエディタを開く
  2. 左側ファイル一覧の「+」アイコン→「HTML」を選択
  3. ファイル名はUploadDialogと入力(拡張子は不要、自動的にUploadDialog.htmlになる)

準備③:「取込先」シートを用意する

下記のコードは、データの書き込み先として取込先という名前のシートをgetSheetByName('取込先')で取得する前提になっています。このシートが存在しないとnullが返り、Cannot read properties of null (reading 'clearContents')というエラーになります。あらかじめシート下部の「+」から「取込先」という名前のシートを追加しておいてください。

Step1:基本実装(アップロード〜変換〜転記)

サーバー側(Code.gs)

// ダイアログを開く
function showUploadDialog() {
  const html = HtmlService.createHtmlOutputFromFile('UploadDialog')
    .setWidth(450)
    .setHeight(340);
  SpreadsheetApp.getUi().showModalDialog(html, 'ファイルを取り込む');
}

// ダイアログ側から呼ばれる:アップロード→変換→転記→後片付け
function importFileViaDrive(base64Data, fileName, mimeType, sheetName) {
  const decoded = Utilities.base64Decode(base64Data);
  const blob = Utilities.newBlob(decoded, mimeType, fileName);

  // ①Driveへアップロードすると同時にGoogleスプレッドシート形式へ変換
  const resource = {
    name: fileName,
    mimeType: MimeType.GOOGLE_SHEETS
  };
  const convertedFile = Drive.Files.create(resource, blob);

  try {
    // ②変換されたスプレッドシートを開いてデータを取得
    const tempSs = SpreadsheetApp.openById(convertedFile.id);

    // シート名が指定されていればそのシート、未指定なら先頭シート(Sheet1)を使う
    let tempSheet;
    if (sheetName) {
      tempSheet = tempSs.getSheetByName(sheetName);
      if (!tempSheet) {
        throw new Error(`シート「${sheetName}」が見つかりませんでした。`);
      }
    } else {
      tempSheet = tempSs.getSheets()[0];
    }

    const data = tempSheet.getDataRange().getValues();

    if (data.length === 0) {
      throw new Error('取り込んだファイルにデータがありませんでした。');
    }

    // ③取り込み先シートに書き込み(例:「取込先」シートのA1から)
    const targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('取込先');
    targetSheet.clearContents();
    targetSheet.getRange(1, 1, data.length, data[0].length).setValues(data);

    return `${data.length}行のデータを取り込みました。`;
  } finally {
    // ④Drive上の変換済み一時ファイルを完全に削除(ゴミ箱を経由しない)
    Drive.Files.remove(convertedFile.id);
  }
}

try...finallyで囲んでいるのは、途中でエラーが発生した場合でも一時ファイルの削除だけは確実に実行するためです(VBAのOn Error+後始末処理に近い考え方です)。

注意1:sheetName未指定時は「先頭シート」であって「アクティブシート」ではありません。 実際に検証したところ、Excelファイルを開いたときに表示されるシート(アクティブシート)ではなく、常にシート順で1番目(一番左)のシートが取り込まれることを確認しました。VBAのActiveSheetとは挙動が異なる点に注意してください。特定のシートを取り込みたい場合は、sheetNameにシート名を指定してください(次の「クライアント側」のフォームに入力欄を用意しています)。

注意2:sheetNameには「シートのオブジェクト名(コード名)」ではなく「シート見出しに表示されている名前」を指定してください。 VBAのプロジェクトエクスプローラーで見えるSheet1のようなオブジェクト名(コード名)を指定したところ、getSheetByName()では一致せず「シート「◯◯」が見つかりませんでした。」というエラーになることを確認しました。getSheetByName()が参照するのは、あくまでシートタブに表示されている名前(VBAでいう.Nameプロパティ、.CodeNameではない方)です。

注意3:showUploadDialog()はApps Scriptエディタの「実行」ボタンから直接実行できないことがあります。
最初に検証していたプロジェクトでは、エディタの「実行」ボタンから直接実行するとException: Cannot call SpreadsheetApp.getUi() from this context.というエラーになりました(GAS最初の記事で判明した制約と同じで、getUi()はスプレッドシートのUIに紐づいた実行でないと呼び出せない、という理解でした)。ところが、後から検証用に作った別のプロジェクトでは、同じようにエディタの「実行」ボタンから直接実行してもエラーにならず成功しました。発生条件はまだ特定できていませんが、プロジェクトによって挙動が異なることが分かっています。** エラーになる/ならないに関わらず、シート上のボタンからの実行であれば確実に動作するため、動作確認は次のStep2でシート上にボタンを設置してから行うことをおすすめします。

クライアント側(UploadDialog.html)

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
</head>
<body>
  <p>取り込むファイルを選択してください(.xlsx / .xls / .xlsm / .csv)</p>
  <input type="file" id="fileInput" accept=".xlsx,.xls,.xlsm,.csv">
  <br><br>
  <label>取り込むシート名(Excelファイルのみ・省略時は先頭シート)</label><br>
  <input type="text" id="sheetNameInput" placeholder="例:取込対象">
  <br><br>
  <button onclick="upload()">取り込む</button>
  <p id="status"></p>

  <script>
    function upload() {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];
      if (!file) {
        document.getElementById('status').textContent = 'ファイルを選択してください。';
        return;
      }

      document.getElementById('status').textContent = 'アップロード中…';

      const sheetName = document.getElementById('sheetNameInput').value;

      const reader = new FileReader();
      reader.onload = function(e) {
        // data:application/...;base64,xxxxx の形式なので、base64部分だけ取り出す
        const base64 = e.target.result.split(',')[1];

        google.script.run
          .withSuccessHandler(function(msg) {
            document.getElementById('status').textContent = msg;
          })
          .withFailureHandler(function(err) {
            document.getElementById('status').textContent = 'エラー:' + err.message;
          })
          .importFileViaDrive(base64, file.name, file.type, sheetName);
      };
      reader.readAsDataURL(file);
    }
  </script>
</body>
</html>

FileReaderでファイルをBase64文字列に変換し、google.script.runでサーバー側のimportFileViaDrive関数に渡しています。このFileReaderによるBase64変換は、次回検証するクライアント側パース方式でも土台として使う処理です。

GASでファイルを取り込むダイアログの実行結果(シート名指定・101行取り込み成功)

Step2:ボタンから実行できるようにする

GAS最初の記事の GASでボタンから実行する方法(VBAのシートボタンに相当)と同じ「図形にスクリプトを割り当てる」方法で、シート上にボタンを設置します。

  1. メニュー「挿入」→「図形描画」で図形を作成
  2. 図形を選択した状態で右上の「︙」→「スクリプトを割り当て」
  3. 関数名にshowUploadDialogと入力

これでボタンを押すとアップロードダイアログが開きます。

Step3:注意点|Excelのスピル関数(動的配列)は正しく変換されない

スピル関数(動的配列を返す新しいExcel関数)を含むファイルを取り込んだところ、変換結果に問題が見つかりました。=MAXIFS(D2:D101,E2:E101,"*"&G4:G14&"*")のように条件範囲(G4:G14)でスピルさせる数式や、明示的にスピルさせるEXPAND関数を含むファイルを取り込むと、スピル元のセルには計算済みの値が残るものの、スピルして表示されていたはずの周囲のセルはすべて空白になりました。 Drive経由の自動変換では実質「1つの数式・1つの値」としてしか扱われないためと考えられます。

下の画像では、SORT関数が変換でていない状況がわかります。

Excelのスピル関数がDrive変換後にスピル先が空白になる様子

読み取りタイミングの問題ではないことを確認済み: 変換直後の再計算待ちが原因という可能性も考え、変換された一時ファイルをApps Script経由ではなくGoogleドライブ上で直接開いて確認しましたが、その時点ですでにスピル先は空白でした。変換処理そのものでスピル先の値が失われていることが確定しています。

実務への影響: 元のExcelファイルにスピル関数(FILTERUNIQUESORTSEQUENCE・条件範囲を複数指定したXLOOKUP/MAXIFSなど)が含まれていると、取り込み時にスピル表示の大部分が失われます。取り込み前に「値貼り付け」で確定させる、またはスピル関数を使わない集計方法に置き換えておくといった対策が必要です。

できること・できないことの整理

項目結果
ローカルのExcelファイル(.xlsx)を取り込む✅ アップロード→変換→転記まで実機で成功
エディタの「実行」ボタンから直接実行⚠️ プロジェクトによってgetUi()エラーになる場合とならない場合がある(原因未特定)。シート上のボタンからなら確実に動作する
対応ファイル形式(.xls/.xlsm/.csv)✅ すべて取り込み成功を確認
大きめのファイルでのアップロード時間✅ CSV(995KB・32,486行×2列)で約14秒
取り込まれるシート⚠️ シート名未指定時は「先頭シート」(Sheet1等)。アクティブシートではない点に注意(sheetNameを指定すれば任意のシートを取り込み可能)
通常の数式・値の変換結果✅ 問題なく変換される
スピル関数(動的配列)の変換結果❌ スピル元セルの値のみ残り、スピル先のセルは空白になる(詳しくはStep3)SORT関数など関数によっては NAME? のように全く変換できません
Drive上に一時ファイルが残らないかDrive.Files.remove()で完全削除を確認(ゴミ箱にも残らない)
初回実行時の権限承認の見え方✅ 標準の「認証が必要です」ダイアログが表示される

注意: 本記事は実機検証を進めながら更新しています。ここまでの実機検証で判明した結果(動く/動かない・想定外の挙動など)をそのまま反映しています。

まとめ

  • GASはローカルPCのファイルシステムに直接アクセスできないが、HTMLフォームでアップロード→Googleドライブ上でスプレッドシート形式に自動変換→中身をコピー→一時ファイル削除という「Drive経由方式」で、VBAのGetOpenFilenameに近いことが実現できる
  • Drive.Files.createを使うにはAdvanced Google Servicesの「Drive API」をv3で追加する必要がある。v3が選択肢に出てこない場合はマニフェストファイル(appsscript.json)を直接書き換えることで対処できる
  • SpreadsheetApp.getUi()はエディタの「実行」ボタンから直接実行するとエラーになる場合がある(発生条件は未特定でプロジェクトによって異なる)。シート上のボタンからの実行であれば確実に動作する
  • .xlsx/.xls/.xlsm/.csvいずれも取り込みに成功した。数万行規模のCSVでも実用的な速度で処理できた
  • シート名を指定しない場合は常に先頭(一番左)のシートが取り込まれる。特定のシートを取り込みたい場合はsheetNameにシートタブの表示名(VBAの.CodeNameではない)を指定する
  • Excelのスピル関数(動的配列)を含むファイルは、変換時にスピル先の値が失われる。取り込み前に値貼り付けで確定させておくなどの対策が必要
  • Drive上の変換用一時ファイルはDrive.Files.remove()で確実に削除され、ゴミ箱にも残らない

次回予告

次回GAS第7回の記事では、Driveを経由せず、ブラウザ側のJavaScriptライブラリ(SheetJS等)でアップロードされたファイルをその場でパースし、パース済みのデータを直接google.script.runでサーバー側に渡すクライアント側パース方式を実装・検証します。Drive経由方式(今回)との違い——実装コスト、対応ファイル形式、権限、処理速度など——は、GAS第8回の比較記事で整理する予定です。

サンプルファイルについて

この記事で使用したテスト用スプレッドシートはこちらからダウンロードできます。GASのスクリプトはスプレッドシートに紐づいた状態(コンテナバインド型)で保存されているため、このシートをコピーすればスクリプトごとそのまま複製されます。Office Scriptsのようにファイルとスクリプトを別々に用意する必要はありません。

サンプルスプレッドシートを開く(コピーして使用)

↑ リンク先ファイルのURL末尾を/copyにした「コピー用リンク」にしています。クリックするだけでご自分のGoogleドライブにスプレッドシートのコピーが複製(GASスクリプトごと)されるようになっています。末尾/copyを外してアクセスした場合は、「閲覧のみ」となりますのでコピーを作成してご利用ください。

関連記事

当サイトの記事で使用したVBAなどのサンプルをDLできます

この記事のサンプルをコピーする!

ダウンロードページへは下のカードをクリックすればジャンプできます。
よろしければご利用ください!

よかったらシェアしてね!
  • URLをコピーしました!
目次