2024年6月13日

GASを使ってテキストファイルの複数アップロードとイメージファイルアップロードの実証


四国今治城
四国今治城  


愛媛県の今治城に行ってきました。この城は戦国時代に藤堂高虎が作った城で,海水を堀に引き込んでいるのには驚きました。

さて,前回はGASを使って一つのテキストファイルに限定したアップロード方法を記述しました。

GASを使ってローカルPCの日本語テキストファイルをアップロードし英語に変換してみた。

GASを使ってローカルPCの日本語テキストファイルをアップロードし英語に変換してみた。

Google Apps Script(GAS)を使ってPCのローカルテキストファイルをアップロードし,加工・蓄積するGASスクリプトを作成する


今回はこれを複数ファイルでも可能にします。ついでに,PDFやイメージなどのマルチファイルアップロードについても検証してみました。

方法としては,PC側で伝送ファイルをBase64ファイル形式に変換して伝送し,GAS側でデコードする方式を使います。

Base64とは、64進数を意味する言葉で、すべてのデータをアルファベット(a~z, A~z)と数字(0~9)など64種類の文字コードで表すエンコード方式です。

昔は文字コードしかファイル伝送出来なかったので,イメージファイルなどを文字コードに変換してファイル伝送する方法を考えたということです。

全てのファイルは,バイナリーデータの集合体として考えることができますので,データを文字列にして送信するのは応用範囲が広いと考えます。

それでは,複数ファイルの入力方法とドラッグ&ドロップの改良をやった後,伝送方式をBase64形式に改良することにします。

では学習を始めましょう。





  GAS側の「main」コードと今回の変更点について


「main」コードについては,特に変更ありません。


main.gs

function main() {
  var html = HtmlService.createHtmlOutputFromFile("index");
  SpreadsheetApp.getUi().showModalDialog(html, 'ローカルファイル読込');
}


前回は一つのテキストファイルのみ対象にしましたが,今回は以下のように対応します。

(1)複数のファイルを読み込めるようにする。
(2)ファイルのドラッグ&ドロップを可能にする。
(3)読み込めるファイルの種別をTEXT,PDF,CSV,PNG,JPEGに拡大する。

変更したコードは,HTML側とJavaScript側を合わせると長いので,HTML側(index.html)とJavaScript側(js.html)に分けて作成しました。



  HTML側(index.html)コードの内容

 
具体的なコード内容は,以下のとおりです。

index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      #droparea {
        width: 80%;
        height: 20%;
        margin: auto;
        padding: 20px 0 20px;
        border: 2px dashed #494949;
      }
    </style>
  </head>
  <body>
    <form action="" method="post" enctype="multipart/form-data" >
      <div id="drop" align="center">
        <div id="chart" style="cursor: auto;" >
          <p id="droparea">ここにアップロードファイルをドロップして<br />
            読込ボタンを押して下さい<br />または</p>
        </div>
        <br />
        <input id="File_in" name="myFile" type="file" multiple />
        <button id="Click" type="file">読込</button>
        <hr />
        <pre id="result" style="font-size:1.0rem; font-weight: bold;";></pre>
      </div>
    </form>
    <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
  </body>
</html>


このコードブロックへの記載にはHTMLタグ文字(<,>,&,”)をHTMLエンティティ文字に変換する必要があり,以下のツールを使って変換しました。詳しくは以下のサイトをご覧ください。


コードブロック表示のためのHTMLタグ文字変換ツール

コードブロック表示のためのHTMLタグ文字変換ツール

コードブロック表示のためのHTMLタグ文字変換ツール



このHTML文ですが,ファイルのドラッグ&ドロップのためのタグを加えたのが変更点です。

「id="droparea"」の処にドラッグ&ドロップを促す文言を書いています。また,ドロップするエリアを確保するため,CSSスタイルに最低限の装飾を施しています。

このドラッグ&ドロップの処理時に,ドロップされたファイルをInputタグの入力ファイルに渡すようにしており,読込ボタンを押して初めて,ファイル送信がかかるように設計しました。

このHTML構文を動かすと以下のようなモーダルダイアログが出力されます。


モーダルダイアログ図1
モーダルダイアログ図


 なお,<?!= ~  ?>で囲んでいる所がありますが,これはjavascript文を「js.html」という別ファイルにしましたので,それを呼びだすスクリプトレット構文になります。 



  HTML側(js.html)コードの内容


コードは以下のようになります。これは,javascript構文になりますが,画面のイベントに対して動作するイベントドリブン型式になっています。


js.html

<script>
    //初期値設定
    const reader = new FileReader();  //FileAPIインスタンスの生成
    let fileCounter = 0;  //アップロードファイルオブジェkトの配列の添字
    var files = [];  //アップロードファイルオブジェkトの配列   
    var fileObject;  //アップロードファイルのオブジェクト
    var msg = "";    //出力メーッセージの文字列

    // フォームの読込クリックで呼ばれる処理
    window.addEventListener('DOMContentLoaded', function() {
      // ドラッグ&ドロップエリアのオブジェクト取得
      const filearea = document.getElementById('droparea');
      // input[type=file]のオブジェクト取得
      const file_input = document.getElementById('File_in');
      // メッセージの出力オブジェクト取得
      const msg_out = document.getElementById('result');

      //アップロードロードファイルをDropした時の処理
      filearea.addEventListener('drop', function(e){ 
        e.preventDefault(); //submit等のイベントの本来の動作禁止
        for (let i = 0, l = e.dataTransfer.files.length; i < l; i++) {
          if(file_check(e.dataTransfer.files[i]) < 0){
            msg = "画像ファイルとテキストファイル以外は転送できません。";
            msg_out.textContent = msg; //メッセージ出力
            files = []; 
            fileCounter = 0;
            return;
          }
        }
        //DropファイルからInputファイルへオブジェクトをコピー
        file_input.files = structuredClone(e.dataTransfer.files);
        //コピーの成否を確認
        if (typeof file_input.files[0] == "undefined"){
          alert("DropファイルがInputファイルへコピー出来ません(強制終了)\n\n" + e.dataTransfer.files[0].name);
          spread_syuryo(); //終了関数を呼ぶ
        }
      },true);

      // ドラッグオーバー時の処理
      filearea.addEventListener('dragover', function(e){
        e.preventDefault();  //submit等のイベントの本来の動作禁止
      },true);

      // ドラッグアウト時の処理
      filearea.addEventListener('dragleave', function(e){
        e.preventDefault();  //submit等のイベントの本来の動作禁止
      },true);

      //読込ボタンがクリックされた時の処理
      document.getElementById("Click").addEventListener('click', function(e){
        //ファイルが選択されているかチェックする
        if ((typeof file_input.files[0]) === 'undefined'){
          msg = "ファイルが選択されていません";
          msg_out.textContent = msg; //メッセージ出力
          return;
        }
        for (let i = 0, l = file_input.files.length; i < l; i++) {
          if(file_check(file_input.files[i]) != 0){
            msg = "画像・テキスト(CSV含)・PDFファイル以外は転送できません。";
            msg_out.textContent = msg; //メッセージ出力
            files = []; 
            fileCounter = 0;
            break;
          }else{
            files.push(file_input.files[i]);
          }
        }
        nextfile_read();
      },true); 

      //ファイル読込成功時の処理
      reader.addEventListener('load', function(){
        //GASで定義した関数を呼び出す
        google.script.run
        .withFailureHandler(function(err) {
         msg = "GAS側の処理エラー\n\n" + err.message + "\n\nファイル名:" + fileObject.name;
         msg_out.textContent = msg; //メッセージ出力
         fileCounter++;
         nextfile_read();
        }).withSuccessHandler(function() {
        //処理が完了したので次のファイルがあるか読み込む
         fileCounter++;
         nextfile_read();
        }).writeSheet(reader.result, fileObject.name, fileObject.type, fileCounter);        
      },true);
        
      //ファイル読込失敗時の処理
      reader.addEventListener('error', function(){
        alert("FILEAPI読込エラー(強制終了) = " + reader.error.message);
        spread_syuryo(); //終了関数を呼ぶ
      },true);

      //次のアップロードファイルの有無を確認し読み込む
      function nextfile_read(){
        if(files.length == 0){ //アップロードするファイルが無い場合
          fileCounter = 0;
          return;
        }
        if (fileCounter < files.length){
          msg = "ファイルのアップロード中です\n\nファイル名:" + files[fileCounter].name;
          msg_out.textContent = msg; //メッセージ出力
          //fileオブジェクトを取得する
          fileObject = files[fileCounter];
          //fileオブジェクトを読み込む
          reader.readAsDataURL(fileObject);
        } else {
          // 終了メッセージ出力
          msg = 'アップロードが完了しました';
          msg_out.textContent = msg; //メッセージ出力
          files = [];
          fileCounter = 0;
          //alert("アップロードが完了しました。");
          //spread_syuryo(); //終了関数を呼ぶ
        }
      }
      //アップロードファイル種別をチェックする
      function file_check(file){
        // 画像ファイルまたはテキストファイルかを判定
        if (!file.type.match('image.*') &&
            !file.type.match('text.*') &&
            !file.type.match('application/pdf')) {
          return(-1);  //画像ファイルとテキストファイル以外
        }else{
          return(0);
        }
      }
      //終了処理
      function spread_syuryo(){
        // スプレッドシートで、アップロードUIを自動的に閉じる
        google.script.host.close();
      }

    });
</script>


この構文では,「addEventListener()」メソッドを利用しています。インターネット上には,「onXXXXX」プロパティによるイベントハンドラーを用いたコード例をよく見かけますが,同一要素/同一のイベントに対して複数のイベントハンドラーが使えない場合もあり,こちらの方式を採用しました。

また,構文内で共通に使うオブジェクト類は,冒頭で纏めて定義しています。

加えて,複数ファイルを扱うため,ファイルを一つずつFILEAPIに読み込むたびにGAS側の「writeSheet」関数を「google.script.run」クラスで呼び出して,送信ファイルが無くなるまで繰り返しています。

ファイルの読込では,FileAPIを利用してバイナリーファイルを扱うため「readAsDataURL(ファイルオブジェクト)」メソッドを利用しています。

このメソッドは,バイナリーファイルをDataURL形式で加工することが出来,以下のようなフォーマットで出力されます。

フォーマット:「data:MimeType(例:text/plain);base64,データ本体(64進数)・・・」 

このデータをGAS側に送って,データ本体だけを抽出しデコードすることで元のデータに復元することが可能です。但し,セキュリティ配慮は別途必要になります。



  GAS側スクリプトコードの内容


GAS側のコードは,以下の通りです。HTML側からファイルを引数で受け取った後,ユーティリティを使ってBase64をデコードしてBlobファイルを作成しています。


main.gs(writeSheet関数)

function writeSheet(data, fileName, mimeType, fileCounter) {
  const blob = Utilities.newBlob(Utilities.base64Decode(data.substr(data.indexOf('base64,')+7)), mimeType).setName(fileName);

  console.log(blob.getName());
  console.log(blob.getContentType());

  // このblobを名前とファイルタイプをスプレッドシートに出力します。

  // 書き込むシートクラスを取得
  var sheet = SpreadsheetApp.getActiveSheet();

  // 書き込むシートの出力セルをクリアする。
  if (fileCounter == 0){
    let row_count = sheet.getLastRow() -4;    //HEADER4行分を除く
    let col_count = sheet.getLastColumn();    //利用カラムの最大数
    if(row_count > 0){
      sheet.getRange(5, 1, row_count, col_count).clearContent(); //5行目から最終行までを数値クリア
    }
  }
  
  //書き出す行番号を引数から計算する
  let row = 5 + fileCounter;

  // テキスト配列をシートに展開する
  sheet.getRange(row, 1).setValue(blob.getName());
  sheet.getRange(row, 3).setValue(blob.getContentType());

  // 任意のDriveへ出力する場合は下記を記述して下さい。
  //const folderId = "YourFolderID";
  //const folder = DriveApp.getFolderById(folderId);
  //folder.createFile(blob);

  // 処理終了のメッセージボックスを出力(チェックする場合はコメントを外す)
  //Browser.msgBox("ローカルファイル" + blob.getName() + "を読み込みました");

}


特に説明は要らないと思いますが,GASユーティリティを使って,Base64URLメッセージをデコードしているのが特徴です。

substr()メソッドとindexOf()メソッドを使って,受け取ったDataの頭から「base64,」文字までを削除しています。

受け取った引数ファイルの内容を確認するため,ファイル名とファイルタイプをスプレッドシートに出力させました。

以下が,スプレッドシートの出力サンプルです。

スプレッドシート出力サンプル
スプレッドシート出力サンプル

スクリプトの実行ボタンを付けています。作成方法は,前々回記載したとおりです。



  最後に

ローカルPCからGAS側へ,複数ファイルのアップロード方法について学習しました。javascriptのイベントドリブンによる作成は興味深いところです。エクセルのVBAと同じように作ることができます。

また,GASのユーテリティは,インターネットWebAPI利用時に応用できる感じがします。

イメージファイルやPDFファイルもアップロードしてみました。特に問題なく復元出来ましたので,このやり方は応用範囲が広いと感じます。

次回は,GASを使ったダウンロード手法について考察したいと思います。

少し,長くなりましたが,以上です。

それでは,楽しいITリテラシーライフをお過ごしください。


(ご注意)情報の正確性を期していますが,実施される場合には自己責任でお願いします。

0 件のコメント: