2018年4月7日土曜日

Googleスプレッドシートの内容をGoogle Apps Scriptを使用してメール送信する

内容
Google DriveのSpreadSheetに作成されたテーブルの内容に従いメールを送信する。
  • テーブルの内容に従いメールを送信する。
  • テーブルの位置もシート内で指定する。
  • チェックのついている行のみを送信する。
  • 送信完了したら送信完了日時を記録する。
  • GASの6分制限を回避するため5分で安全に終了する。
  • 説明が面倒なので変数名を日本語にしている。
シートの例

スクリプト
function myFunction() {
  var スクリプト開始時間 = new Date();
  var アクティブシート = SpreadsheetApp.getActiveSheet();
  var テーブル範囲 = アクティブシート.getRange("シート1!A3").getValues()[0][0];
  var テーブルデータ = アクティブシート.getRange(テーブル範囲).getValues();
  var テーブル定義 = [];
  for( var i=0; i<テーブルデータ[0].length; i++ )
  {
    テーブル定義[テーブルデータ[0][i]]=i;
  }
  var len = テーブルデータ.length;
  for( var i=1; i<テーブルデータ.length; i++ )
  {
    var 今の時間 = new Date();
    // スクリプト実行開始から5分間実行していたらスクリプトを止める。
    if( (今の時間 - スクリプト開始時間) > 5*60*1000 ){
      // ★で書き込んでいるが念のため再度書き込み
      アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
      return;
    }

    if( テーブルデータ[i][テーブル定義["送信指定"]] == "✔" && テーブルデータ[i][テーブル定義["送信完了日"]] == "" )
    {
      try{
        var 文頭         = テーブルデータ[i][テーブル定義["文頭"]];
        var 氏名         = テーブルデータ[i][テーブル定義["氏名"]];
        var メールアドレス = テーブルデータ[i][テーブル定義["メールアドレス"]];
        var ご質問       = テーブルデータ[i][テーブル定義["ご質問"]];
        var 回答         = テーブルデータ[i][テーブル定義["回答"]];

        GmailApp.sendEmail(
          メールアドレス,
          "Googleスプレッドシートの内容をメール送信するGASからのメール",
          Utilities.formatString("%s 様\n\n%s\n\n<ご質問>\n%s\n\n<ご回答>\n\n%s",氏名,文頭,ご質問,回答 ) );
        テーブルデータ[i][テーブル定義["送信完了日"]] = Utilities.formatDate(new Date(), "Asia/Tokyo","yyyyMMddHHmmss");

        // (★)スクリプトエラーが発生したときにどこまで送信したか分からなくなると困るのでこまめに書き込む
        アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
        
        Utilities.sleep(1000);
      }catch(e)
      {
        // エラーが発生したときはセルを更新しない。
        Logger.log(e);
      }
    }
  }
  // ★で書き込んでいるが念のため再度書き込み
  アクティブシート.getRange(テーブル範囲).setValues(テーブルデータ);
}


実行結果(送信メールの例)
鈴木C助 様

お世話になります。

<ご質問>
質問です。c~

<ご回答>

回答です。c~

2018年4月6日金曜日

iPhoneの機種変更時に写真をGoogleフォトにBackupする方法

概要
iPhoneの機種変更時によくあるトラブルの一つに写真のバックアップミスがある。
Googleフォトにバックアップしたつもりだったのに実はバックアップできていなくて大切な写真を失ってしまう人がいる。特に最近は下取りに出す人が多く本当に戻ってこない。
そういう失敗をする人を少しでも救いと主に、機種変更前に安全にバックアップする方法をまとめてみた。
ただし、一番安全なのは機種変更時に下取りに出さないことです。

よくある失敗
  • Googleフォトアプリに画像が表示されているので、バックアップされていると思い機種変更したが、実はバックアップされておらず写真を消失。 
  • バックアップしているアカウントを忘れて、機種変更後にバックアップ先アカウントにログインできずに写真を紛失。

機種変更の際に確実にバックアップする方法
  1. 機種変更前のiPhoneにGoogleフォトアプリをインストールする
  2. Googleフォトアプリを起動してログインする。
  3. Googleフォトアプリ左上の「≡」>「歯車」(設定)>「バックアップと同期」画面に遷移する。
  4. 「バックアップと同期」をオンにする。その際に設定したバックアップアカウントを記録しておく。
  5. バックアップのタイミングを設定する。(WiFi環境で作業を行うのであれば、両方オフでもよい。)
  6. バックアップが完了するまでしばらく待つ。
  7. バックアップされているかどうかは https://photos.google.com にログインして確認する。(ここではアカウント確認の意味もあるので省略せずに必ず実施しましょう。)
  8. Googleフォトアプリ左上の「≡」>「空き容量を増やす」を実行する。
  9. 「空き容量を増やすため、端末からXXX個のファイルを削除しますか」というメッセージが表示されるので、「削除(XXX個)」を押下する。
  10. 今度は「"GooglePhotos"にXXX個の項目の削除を許可しますか?」と表示されるので「削除」をタップする。
    • 写真が大量の場合はこのメッセージが表示されるまで5分とか10分かかることもあるので気長に待つ
  11. iPhone標準の写真アプリでカメラロールの中をチェックする。残っている写真はバックアップできていない可能性が高い。
    • もし大量に残っている場合はまだバックアップ途中の可能性が高いのでもう少し時間をおいてから、8番目の手順から繰り返す。
    • 何度行っても画像が残る場合がある。その場合はGoogleフォトにバックアップできていない可能性もあるので手動でアップロードして、8番目の手順から繰り返す。手動のアップロードは https://photos.google.com にアクセスして、雲上矢印を押して行う。
    • いろいろ試してみるとポートレートモードで撮影した写真がカメラロールに残っているようだ。今のところポートレードモード撮影した写真は「iPhoneのGoogleフォトアプリ利用時にカメラロールから安全に写真を削除する方法」で削除するしかなさそうだ。

以上でカメラロールの中から写真がなくなればすべての写真がアップロードされていることになる。 安心してiPhoneを下取りに出せる。

上記の方法に限らず、Googleフォトは写真アプリで作成していたアルバム構成などはバックアップできないので注意が必要。今後も機種変更などを行うことを考えると、iPhoneの写真アプリでアルバムを作るのではなく、Googleフォトでアルバムを作ったほうが良い。
また、Googleフォトはバックアップは自動でできても、Googleフォトから端末に自動で戻すことはできないので、必要であれば頑張ってダウンロードするしかない。

補足
同じ写真を撮影することはほぼ不可能なので、紛失しないようにバックアップは重要である。Googleフォトにバックアップしていれば安全かもしれないが、やはり操作ミスやアカウント情報忘れなどでバックアップを削除してしまうことがある。特に機種変更後は端末には写真がなくGoogleフォトにしか写真がないので、バックアップが無い状態と同じだ。
パソコンを持っている場合は「Googleフォトを用いた家族の写真データ管理方法」なども参考にしてほしい、別に一人の人でも参考になると思う。

2018年4月1日日曜日

Google Apps Script で指定フォルダ以下のファイルを処理する方法(2)

概要
Google Apps Script で指定フォルダ以下のファイルを処理する方法(1)とは別の方法も記録として残しておく。どちらが良いかは検索範囲や階層の深さなどによるので一概にどちらが良いとは言えないが。フォルダ数が以上に多くファイル数がそれほど多くない場合は今回の方法が有利だが、あまりそういった環境が想像できないので基本的には前回の方法が良いと思う。
今回の方針はドライブ内のすべてのファイルを対象に処理を行い、対象のファイルの親フォルダをたどっていき、指定フォルダにたどり着くかをチェックする。もし指定フォルダにたどり着いたらそのフォルダの配下と判断し処理を行う。


今回はいきなり使い方も含めたサンプル
function sample()
{
  var files = DriveApp.getFiles();
  var TargetFolderID = "205Ps_mAhEFs.......Ugm1m";
  while( files.hasNext() )
  {
    var file = files.next();
    if( checkSubordinate(file,TargetFolderID) )
    {
      ////////////////////////////////////
      // ファイルごとの処理をここから記載する。
      var logString = file.getName()+":"+file.getId();
      Logger.log(logString);
      // ファイルごとの処理をここまでに記載する。
      ////////////////////////////////////
    }
  }
}

// targetが指定フォルダ(folder ID)の配下のフォルダかチェックする。
function checkSubordinate(target,folderID)
{
  var log = target.getName();
  var parents = target.getParents();
  while( parents.hasNext() )
  {
    var parent = parents.next();
    var id = parent.getId();
    if( id == folderID ){
      return true;
    }
    if( checkSubordinate(parent,folderID) )
    {
      return true;
    }
    Logger.log( log );
  }
  return false;
}

Google Apps Script で指定フォルダ以下のファイルを処理する方法(1)

概要
Google Apps ScriptでGoogleドライブの特定のフォルダ以下に含まれるファイルに対して処理を行いたい場合がある。そういった場合、サブフォルダも含め再帰関数を用いたり、配列などを用いて頑張ってループする方法もあるが、6分制限対策で処理を中断した際の再開方法が面倒です。
ということで指定したフォルダ以下のファイルを一つのFileIteratorオブジェクトとして取得する関数を作成した。その関数と使い方を簡単に説明する。

まずはいきなりソースコード
// 指定したフォルダ以下に含まれるすべてのファイルを列挙
function getFiles(folderID)
{
  // 「指定したフォルダ内に配置されている」という検索式を作る。
  var searchFileParams = Utilities.formatString("'%s' in parents", folderID);
  var searchFolderParams = searchFileParams;
  var folders;
  // フォルダ検索結果が空になるまで検索を続ける。

  while( (folders = DriveApp.searchFolders(searchFolderParams)).hasNext() )
  {
    // 最後に先頭の" or "を削除したいので先頭を示す"@"を入れておく
    searchFolderParams = "@";
    while( folders.hasNext() )
    {
      // このループでは検索式に" or 'XXX' in parents"を追加していく 
      var folder = folders.next();
      searchFolderParams += Utilities.formatString(" or '%s' in parents", folder.getId() );
    }
    // でき上がった検索式の先頭から不要な"@ or "を削除する。
    // searchFolderParamsは次の階層のフォルダ検索式
    searchFolderParams = searchFolderParams.replace( "@ or ","" );
    // searchFileParamsはファイルを一気に検索する検索式
    searchFileParams += " or " + searchFolderParams;
  }
  // 指定フォルダ以下全階層のファイルを一気に検索する。
  return DriveApp.searchFiles(searchFileParams);
}

簡単に説明すると、指定されたフォルダを親フォルダに含むファイルを検索するサーチ文を作成し、searchFilesで一気に検索を行う。
デバッグでsearchFileParamsを観察すると動きがわかる。

基本的な使い方
var files = getFiles("205Ps_mAhEFs.......Ugm1m");
while(files.hasNext())
{
  var file = files.next();
  ////////////////////////////////////
  // ファイルごとの処理をここから記載する。
  var logString = file.getName()+":"+file.getId();
  Logger.log(logString);
  // ファイルごとの処理をここまでに記載する。
  ////////////////////////////////////
}


本格的に使用する場合の使い方
フォルダ以下のファイルすべてに処理したいという要望の時は大体ファイル数が膨大なので、実際にはタイムアウトを考慮する必要がありますので、その場合の実装例。
// メニュー>実行>関数を実行>startupFunction を選択して実行してください
function startupFunction(){
  // まずはトリガをすべて削除する。
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

  // プロパティすべて消す。
  deleteProperties();
  
  // ここにフォルダIDを書く
  var TargetFolderID = ,"205Ps_mAhEFs.......Ugm1m";
  setProperty("TargetFolderID",TargetFolderID);

  // トリガを設定する。(10分間隔)
  trigger = ScriptApp.newTrigger("triggerFunction")
  .timeBased()
  .everyMinutes(10)
  .create();

  triggerFunction();
}

function triggerFunction() {
  var StartTime   = new Date();

  var ContinuationToken = getProperty("ContinuationToken");
  if( ContinuationToken != null )
  {
    var files = DriveApp.continueFileIterator(ContinuationToken);
  }
  else
  {
    var TargetFolderID = getProperty("TargetFolderID");
    var files = getFiles(TargetFolderID);
  }

  while(files.hasNext()) {
    var CurrentTime = new Date();
    // トリガを設定する。(5分間実行していたら止める。)
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      setProperty("ContinuationToken",files.getContinuationToken());
      return;
    }
    var file = files.next();
    ////////////////////////////////////
    // ファイルごとの処理をここから記載する。
    var logString = file.getName()+":"+file.getId();
    Logger.log(logString);
    // ファイルごとの処理をここまでに記載する。
    ////////////////////////////////////
  }
  
  deleteProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

なおここで使用しているProperty関連の関数については「Google Apps Scriptで設定ファイルの保存・読込」を参照してほしい。ContinuationTokenが異常に長くなることがあり、PropertiesServiceクラスを用いて保存しようとすると失敗することがある。PropertiesServiceクラスは9217文字以上を指定すると失敗する模様。

関連情報

Googleフォトを用いた家族の写真データ管理方法

(2019/7に実施されたGoogleフォト/Googleドライブの仕様変更により、この記事の内容は使用できません。)

家族全員が各自のiPhoneで写真を撮影する。最初のうちはみんなのiPhoneをパソコンにつないで、個人毎、年月毎のフォルダに振り分けて、ファイル名も撮影日時に変更してからGoogleフォトにバックアップすることを行っていた。
そんなにたくさん撮るわけではないが、やはり旅行に行ったりすると一日で相当な枚数になるので面倒になった。いろいろと試行錯誤した結果、以下の形に落ち着いた。
  1. 各人のiPhoneで撮影した画像は、それぞれのアカウントGoogleフォトにバックアップされる。
  2. 各人のGoogleドライブGoogleフォトフォルダがありそちらに表示される。
  3. そのGoogleフォトフォルダ家族用のアカウントに共有する
  4. パソコンは家族用のアカウントに共有された全員のGoogleフォトフォルダをパソコンにダウンロードする。(これは専用の自作ツールを使用している。)
  5. そのダウンロードされた画像を「バックアップと同期(Backup and Sync from Google)」を用いてGoogleフォトにアップロードしなおす。
2018/3/31に作成
写真は①各自のiPhone②各自のアカウントのGoogleフォト③パソコン④家族用アカウントのGoogleフォトに存在することになる。①はiPhoneの容量を確保するためにちょくちょく削除するので、②③④の3か所にデータが存在することになる。
②は各自が操作ミスをすると消える可能性があるが、③はほとんど操作しないのでパソコンが壊れない限りなくなる可能性は低い、④も操作することはないので安心だ。

ちなみにこの図を作成していたら、以前にも同じような図を作成していたことを思い出した。アルバムアーカイブを探していたやはり以前も作成してた。今回の図と比較すると、デジカメの写真の管理方法が変わっている。当時はデジカメの写真をどのようにどのように管理するか悩んでいたようだ。
2016/8/8に作成
なにが変わったかというと、Lightning SDカードカメラリーダーを購入したことでデジカメの写真の運用がすっきりしていることです。以前はその運用方法を悩んでいる様子がうかがえる。
デジカメはあまり使用しないが、家族のだれが使用するか分からない。デジカメを使い終わったら、自分のiPhoneにSDカードカメラリーダーを接続して写真を取り込む、取り込むと自動で各自のGoogleフォトにアップロードされる。各自の責任で作業を行うので、私の管理ミスで画像をバックアップし忘れて気まずい雰囲気になることがない。
名前の通り、Lightningコネクタに接続してSDカードに保存されているデジカメのデータをカメラロールに読み込むためのものだが、アップル純正という安心感と写真の管理が一気に楽になることを考えると¥3,200 (税別)は非常にお買い得だった。
カメラデータ以外読み込めないし、リーダーというだけあって書き込むこともできない、サードパーティ製のSDカードアダプタはもう少し高機能かもしれないので、そういったものにも興味がある人は調べてから購入するとよい。