2019年1月13日日曜日

Google Apps Script小ネタ集(1)


Google Apps Scriptでたまに欲しくなる小ネタをまとめた。

  • フォルダの作成・取得
  • 多階層フォルダの作成・取得
  • テキストファイル保存
  • ファイルへのデータ保存
  • 指定期間のメールを処理する


フォルダの作成・取得

始めにフォルダ作成のダメな例ですが、普通にcreateFolderを呼び出すと、既に同じ名前のフォルダが存在していても構わず同じ名前のフォルダを生成する。そのためトリガを設定して何度も実行されるスクリプトでこれを行うと同じ名前のフォルダがどんどん増えていき使い物にならない。
// ダメな例
// すでに存在していてもフォルダを作成する。
// 何度も実行すると、毎回同じ名前のフォルダが生成される。
DriveApp.getRootFolder().createFolder( 'HogeHoge' );
対策としては既にフォルダが存在していたらそのフォルダを使用し、存在していない場合には新規に作成することにする。ちなみにファイルやフォルダが存在しているかどうかはgetFoldersByName( name ) 関数を使用する。Foldersと複数形となっていることからもわかるように、同じ名前のフォルダが複数あった場合には複数のフォルダが見つかる。その場合どのフォルダを採用するか悩んでも仕方がないので、最初に見つかったものを採用する。
var folderName = 'HogeHoge';
var root = DriveApp.getRootFolder();    
var folders = folder.getFoldersByName( folderName );
if( folders.hasNext() ) {
  // フォルダが見つかった場合は最初のフォルダを採用
  folder = childs.next();
} else {
  // フォルダが見つからなかった場合はフォルダを作成
  folder = folder.createFolder( folderName );
}
これを関数化しておく。
/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string} name 取得・生成するフォルダ名
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
 */
function getFolder( parent, name, noCreate ) {
  if ( parent == null ){
    parent = DriveApp.getRootFolder();
  }
  if ( noCreate !== true ){
    noCreate = false;
  }
  var folders = parent.getFoldersByName(name);
  if( folders.hasNext() ) {
    // フォルダが見つかった場合は最初のフォルダを採用
    var folder = folders.next();
  } else {
    if( noCreate ){
      // noCreate指定の場合はundefinedを返す。
      var folder = undefined;
    }else{
      // フォルダが見つからなかった場合はフォルダを作成
      var folder = parent.createFolder(name);
    }
  }
  return folder;
}


多階層フォルダの作成・取得

一階層のフォルダを生成するだけであれば先ほどの処理でよいが、写真を整理する場合などは年フォルダの下に月フォルダを生成したい場合がある。単純に処理を繰り返すだけだが、毎回コードを書くのが面倒なので関数化しておく。
/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}
使い方は以下。
function myFunction() {
  // ルートフォルダにHoge1フォルダを生成
  var f1 = getFolder( null,'Hoge1' );  

  // ルートフォルダにあるHoge2フォルダを取得
  var f2 = getFolder( null,'Hoge2', true );
  if( f2 == undefined ){
    Logger.log('Did Not create folder.' )
  }

  // Hoge1フォルダの下に多階層フォルダを生成
  var v3 = getFolder( f1,['Hoge3','Hoge3-1','Hoge3-2'] );
  // 以下のフォルダ構成の'Hoge3-2'フォルダを取得
  // \\Hoge1\Hoge3\Hoge3-1\Hoge3-2
}


テキストファイルを保存

テキストファイルへの保存と読み込みはDrive Serviceのリファレンスに従いうだけではあるが、毎回悩むので記録しておく。
// 新規ファイルの生成と追記
var folder = DriveApp.getRootFolder();
var file = folder.createFile( 'filename.txt', 'Hello, world!!' );
var text = file.getBlob().getDataAsString();
file.setContent(text + '\nNew line!!' );
// 既存ファイルの読み込みと追記
var files = folder.getFilesByName( 'filename.txt' );
if( files.hasNext() ){
  var file = next();
  var text = file.getBlob().getDataAsString();
  file.setContent(text + '\nNew line!!' );
}


ファイルへのデータ保存

テキストの読み書きができているので、クラスを保存したい場合などはJSON化すればよい。
// 保存するオブジェクトの配列を定義
var Users = [new User(123,'Taro'),new User(124,'Jiro')];

// オブジェクトをJSONに変換してファイルに保存
var json = JSON.stringify(Users);
var file = folder.createFile( 'filename.txt', json );

// 保存したJSONを読み込んでオブジェクトを復元
var Users2 = JSON.parse(file.getBlob().getDataAsString());
保存されたファイル(filename.txt)は以下。
[{"id":123,"name":"Taro"},{"id":124,"name":"Jiro"}]


指定期間のメールを処理する

GMAILの検索はGmailApp.search(query,start,max)を使用する。しかしこの関数の戻り値はGmailThread[]となっており非常に厄介。なぜかというと、期間を指定検索をすると期間内のメールも含むスレッドが検索されるのだが、そのスレッドには期間外のメールも含まれていることがある。
今回はその期間外のメールを除外したメールのID一覧を取得する関数の紹介。といっても、期間指定して検索した結果をループで回して期間内のメールだけを抽出している。特に工夫していないが、いざ欲しいときにコーディングしようと思うと時間を取られるので残しておく。
/**
 *  メールの検索結果から指定した期間のメールのIDリストを取得する。
 *
 *  @param {string} searchString 期間指定以外の検索条件
 *  @param {Date} start 検索期間の開始
 *  @param {Date} end 検索期間の終了
 *  @return {string[]} メールIDリスト
 */
function getMailIdList( searchString, start, end )
{
  var after = 'after:' + Math.floor( start.getTime()/1000 ).toString();
  var before = 'before:' + Math.floor( end.getTime()/1000 ).toString();
  const search = after + ' ' + before + ' ' + searchString;

  var mailIdList = [];
  var indexStart = 0;
  while( true )
  {
    var myThreads = GmailApp.search(search, indexStart, 500);
    if( myThreads.length==0)
    {
      return mailIdList;
    }
    indexStart+=myThreads.length;
    
    var myMsgs = GmailApp.getMessagesForThreads(myThreads);
    for(var thread = 0;thread < myMsgs.length;thread++){
      for(var msg = 0;msg < myMsgs[thread].length;msg++){
        var reciveDate = myMsgs[thread][msg].getDate();
        if( start < reciveDate && end > reciveDate )
        {
          mailIdList.unshift(myMsgs[thread][msg].getId());
        }
      }
    } 
  }
}
ちなみに使い方は以下を想定している。
MailList = getMailIdList('SearchString',new Date('2018/10/01 0:00:00'),new Date('2018/10/01 13:00:00'));
while( MailList.length > 0 )
{
  var mail = GmailApp.getMessageById(JobList.shift());
  mail.xxxXxxxx(); // メールに対する処理を実施
}

0 件のコメント:

コメントを投稿

質問、要望、指摘など書いていただいてもよいですが、対応できるとは限りませんのでご了承ください。私に対するものも含め他の人を嫌な気分になるようなコメントは避けてください。