2020年2月22日土曜日

Googleフォトの「保存容量の節約」(旧「高画質」)と「元の画質」の画像比較

Googleフォトの「保存容量の節約」(旧「高画質」)は画像が再圧縮されるので画像が劣化する。では実際にどの程度劣化するのか?この差が気になるのか気にならないのかは人によるので比較結果を参考にしてください。

1.使用画像

元の画質 保存容量の節約(旧「高画質」)

2.拡大比較

元の画質 高画質

3.超拡大比較

元の画質 保存容量の節約(旧「高画質」)
拡大場所

4.検証環境

撮影機材 iPhone 7 Plus iOS 13.3.1
使用PC Windows 10 Pro x64 (10.0.18362.0)
.NET Runtime 4.0.30319.42000
DPI 96 (1.00x scale)
paint.net V4.2.10 (Final 4.210.7348.40816)

2020年1月4日土曜日

Googleフォトの「空き容量を増やす」で特定の画像を端末に残す

概要

スマホでGoogleフォトを使用する目的の一つに、Googleフォトにバックアップした画像を端末から削除する「空き容量を増やす」機能がある。「空き容量を増やす」を実施するとGoogleフォト(ブラウザでhttps://photos.google.comにログインして使用するサービス)にバックアップ画像をスマホから削除できる。スマホの空き容量が足りなくなったときに非常に重宝する。

しかし、この機能はバックアップが完了したすべての画像を削除するので、端末に残しておきたい画像も削除してしまう。端末に画像がないと、Facebook、Twitter、インスタなどに画像を投稿する場合に不便。期間を指定して空き容量を増やすことができるとよいのだが、そういった機能がない。

「空き容量を増やす」を実行する際に、任意の画像を端末に残す方法を説明する。

1.iPhoneの場合
2.Androidの場合

iPhoneに関しては簡単に対応できるが、Androidの場合は少し面倒で機種により操作が違うかもしれない。

1.iPhoneの場合

iPhoneの場合にはiOS標準の写真アプリにある「非表示」機能を使用することで、Googleフォトが端末から写真を削除するのを防ぐ。これで説明としては十分な気がするが、念のため操作画面を張り付けておく。

1-1.写真アプリで該当データを非表示

残したいデータを選択し、共有メニューの中にある非表示を行う。


1-2.Googleフォトアプリで「空き容量を増やす」を実施

Googleフォトアプリの画面右上「アカウントアイコン」をタップし「フォトの設定」>「デバイスの空き容量の管理」と遷移し「空き容量を増やす」を実施する。



1-3.写真アプリで非表示にしたデータを再表示

写真アプリの「非表示」アルバムの中にある写真を「再表示」する。


2.Androidの場合

まだ十分を検証できていないので、今後更新予定。
おおむね以下の手順
2-1.Fileアプリなどのファイルマネージャーアプリを用いて、Google作業用のフォルダに画像を移動する。
2-2.Googleフォトアプリでそのフォルダの同期がオフになっていることを確認する。
2-3.その後「空き容量を増やす」を実施する。
2-4.作業用フォルダから元のフォルダに戻す。(戻さなくてもよい)





2019年11月16日土曜日

Googleフォトの写真をまとめてAndroid端末にダウンロードする方法(すべてをダウンロード)

Googleフォトにバックアップ(アップロード)した画像を一括で端末にダウンロードする方法がわかりにくいのでまとめてみる。機種変更後に新しい端末に写真をダウンロードしたい人が多いようだが、Androidの四色風車アイコンのフォトアプリでは一括でダウンロードできない。

ここではすべての画像をダウンロードする方法を書くので、選択した画像をまとめてダウンロードしたい場合は以下を参照してほしい
Googleフォトの写真をまとめてAndroid端末にダウンロードする方法(選択してダウンロード)

まずは全体の流れ:
1.ブラウザで「自分のデータをダウンロード」にアクセスしてアーカイブ作成行う。
2.データ(.zip)のダウンロード
3.ダウンロードされたzipファイルを開く
4.ダウンロードされたzipファイルから画像を取り出す

それでは詳細を説明する。

ただし、この方法でダウンロードすると表示順が狂うと言う報告もあります。

1.ブラウザで「自分のデータをダウンロード」にアクセスしてアーカイブ作成行う。

ブラウザで 「自分のデータをダウンロード」にアクセスする。
「新しいアーカイブの作成」を押してる「①追加するデータの選択」からGoogleフォトだけを選択する。(「サービス」の部分にある「選択をすべて解除」してから「Googleフォト」にチェックを入れるとよい)
画面を下までスクロールし「次のステップ」を押下する。
「②アーカイブ形式のカスタマイズ」画面の配信方法として「ダウンロード リンクをメールで送信」を指定、でファイル形式に「.zip」を選択し「エクスポートの作成」を行う。

2.データ(.zip)のダウンロード

アーカイブの作成が完了するとメールでダウンロードのリンクが届くので、そこからZIPファイルをダウンロードする。
Googleフォトの容量が大きいと分割されているので、端末の容量が少ない場合には分割されたZIPファイルをひとつづつ操作するとよい。その場合は手順の3,4を繰り返す。

3.ダウンロードされたzipファイルを開く

ここからはGoogleフォトの動作ではないので、Android端末により使用するアプリがことなる。Pixel3しかもっていないので、Pixel3の標準でインストールされている「ファイル」アプリを例に説明する。
「ファイル」アプリを開き、「ダウンロード」フォルダから先ほどダウンロードした「takeout-2019XXXXXXXX.zip」を開く。
 

4.ダウンロードされたzipファイルから画像を取り出す

「⋮」メニューより「すべて選択」を選び、再度「⋮」メニューより「次の場所に解凍...」を選択する。
続いてどこに解凍したいか選ぶが、これは人それぞれなので自分の好きな場所に展開すればよいが、今回はDCIMフォルダを選択する。「≡」メニューから「Pixel3」を選択し、最後に画面右下の「解凍」ボタンを押す。


※ 手順の4番目以降は端末メーカ毎に異なるので、ZIPファイルの展開/解凍方法を別途調べてほしい。

Googleフォトの写真をまとめてAndroid端末にダウンロードする方法(選択してダウンロード)

Googleフォトにバックアップ(アップロード)した画像を一括で端末にダウンロードする方法がわかりにくいのでまとめてみる。機種変更後に新しい端末に写真をダウンロードしたい人が多いようだが、Androidの四色風車アイコンのフォトアプリでは一括でダウンロードできない。

ここでは選択した画像をダウンロードする方法を書くので、すべての画像をまとめてダウンロードしたい場合は以下を参照してほしい
Googleフォトの写真をまとめてAndroid端末にダウンロードする方法(すべてをダウンロード)

まずは全体の流れ:
1.ブラウザでGoogleフォトにアクセス
2.Googleフォトでダウンロードしたい画像を選択する
3.選択した画像をダウンロードする。
4.ダウンロードされたzipファイルを開く
5.ダウンロードされたzipファイルから画像を取り出す

それでは詳細を説明する。

ただし、この方法でダウンロードすると表示順が狂うと言う報告もあります。

1.ブラウザでGoogleフォトにアクセス

ブラウザで Googleフォト( https://photos.google.com/login )にログインし、画像を表示する。

2.Googleフォトでダウンロードしたい画像を選択する

「≡」メニューの並びにある「⋮」メニューより「写真を選択してください」を選択する。写真選択モードになるので写真左上にチェックを入れる。

3.選択した画像をダウンロードする。

ゴミ箱アイコンの横にある「⋮」メニューより「ダウンロード」を選択する。しばらくすると「Photos.zip」がダウンロードされる。

4.ダウンロードされたzipファイルを開く

ここからはGoogleフォトの動作ではないので、Android端末により使用するアプリがことなる。Pixel3しかもっていないので、Pixel3の標準でインストールされている「ファイル」アプリを例に説明する。
「ファイル」アプリを開き、「ダウンロード」フォルダから「Photos.zip」を開く。

5.ダウンロードされたzipファイルから画像を取り出す

「⋮」メニューより「すべて選択」を選び、再度「⋮」メニューより「次の場所に解凍...」を選択する。
続いてどこに解凍したいか選ぶが、これは人それぞれなので自分の好きな場所に展開すればよいが、今回はDCIMフォルダを選択する。「≡」メニューから「Pixel3」を選択し、最後に画面右下の「解凍」ボタンを押す。


※ 手順の4番目以降は端末メーカ毎に異なるので、ZIPファイルの展開/解凍方法を別途調べてほしい。

2019年11月6日水曜日

Googleフォトで画像を消す方法

このページには最新の情報が反映できていないので新しい「Googleフォトで画像を消す方法」ページを参照ください。


スマホ(iPhoneやAndroid)向けのGoogleフォトアプリから画像を消す方法を説明します。といってもGoogleフォトアプリが写真を保持しているわけではないので、削除する対象はGoogleフォト(https://photos.google.com)の写真および端末の写真です。
以下の3パターンを順番に説明します。

 1.Googleフォトに写真を残し、スマホ(iPhoneやAndroid)から写真を削除する
 2.スマホ(iPhoneやAndroid)に写真を残し、Googleフォトから写真を削除する
 3.Googleフォトからも、スマホ(iPhoneやAndroid)からも写真を削除する

なお、いきなり大切な写真で実施せずにテスト用の画像で動きを確認してから大切な写真で作業してください。

なお、Xiaomi、Oppo、OnePlus、Realmeを使用している場合には
この記事を参考に「デバイスのデフォルト ギャラリーによるアクセスを無効にする」を実行してください。私自身がXiaomi、Oppo、OnePlus、Realmeを使用したことがないということと、使用しているユーザーが混乱しているようなので無効にすることをお勧めします。

1. Googleフォトに写真を残し、スマホ(iPhoneやAndroid)から写真を削除する
1-1.選択した写真だけをスマホから削除する方法
絶対やってはいけない方法: Googleフォトアプリのゴミ箱アイコンで削除

やってはいけない方法: Googleフォトアプリを使用せず、写真アプリなどで削除

おススメの方法: Googleフォトアプリで削除したい画像を選択して、「・・・」や「⋮」メニューから「デバイスから削除」や「元のファイルn個をデバイスから削除」を行う。もしくは下から表示されたメニューを横スクロースして「元のファイルをデバイスから削除」を実施する。※デバイスやアプリのバージョンによって操作性が変わるようです。



おススメする理由はバックアップされていないファイルを端末から削除しようとすると以下のような警告メッセージが表示される。
【iPhone】
【Android】

これらのメッセージが出たときは操作をキャンセルしてバックアップを行う。 

1-2.バックアップが完了している写真すべてをスマホから削除する方法
Googleフォトアプリの画面右上「アカウントアイコン」メニューから「このデバイスから XX個のファイルを削除できます」を実施してください。

1-3.バックアップが完了している写真を削除する際に一部の画像を残す方法(iPhone)
スマホ本体から画像を削除するとFacebookやInstagramなど他のアプリで画像を使いたいときに不便になる。そのため最近撮影した写真やよく使用する写真は本体に残しておくとよい。そういったときは"Googleフォトの「空き容量を増やす」で特定の画像を端末に残す"を参考にしてください。

2. スマホ(iPhoneやAndroid)に写真を残し、Googleフォトから写真を削除する

2-1.iPhoneに写真を残してGoogleフォトから写真を削除したい場合
・SafariブラウザのプライベートタブでGoogleフォトWEB版( https://photos.google.com/login )にアクセスして「ログイン」してください。
左上に「≡」アイコンがあればGoogleフォトWEB版にアクセスできています、左上が「≡」アイコンではない場合はアプリに飛ばされている可能性があるので、ブラウザでアクセスできているか再確認してください。
・削除したい画像を表示してゴミ箱アイコンを使用して写真を削除してください。

・Googleフォトでブラウザアクセスした際に複数画像を一気に選択したい場合は「…」メニューの「写真を選択してください」選ぶと複数画像を選択できるようになります。

2-2.Androidに写真を残してGoogleフォトから写真を削除したい場合
・フォト アプリの右上の「アカウントアイコン」>「フォトの設定」>「バックアップ」で「バックアップ」をオフにします。※「バックアップと同期」が「バックアップ」に名変更されましたがま機能は変わってません。
・そのうえでChromeブラウザのシークレットモードでGoogleフォトWEB版( https://photos.google.com/login )にアクセスして「ログイン」してください。
左上に「≡」アイコンがあればGoogleフォトWEB版にアクセスできています、左上が「≡」アイコンではない場合はアプリに飛ばされている可能性があるので、ブラウザでアクセスできているか再確認してください。 
・削除したい画像を表示してゴミ箱アイコンを使用して写真を削除してください。
・ゴミ箱に入れた写真がAndroidの端末に残っていることを確認してください。(Googleフォト以外のアプリで確認してください。)ゴミ箱からも削除しなくても容量は増えてますので焦ってゴミ箱を空にしなくて良いです。
・今後撮影した画像をGoogleフォトに保存したい場合には、作業が完了後に「バックアップ」の設定を元に戻してください。

・Googleフォトでブラウザアクセスした際に複数画像を一気に選択したい場合は「•••」メニューの「写真を選択してください」選ぶと複数画像を選択できるようになる。


3. Googleフォトからも、スマホ(iPhoneやAndroid)からも写真を削除する
Googleフォトアプリで写真を選択してゴミ箱アイコンで削除してください。
Googleフォトからも端末からも削除されるので、本当に要らない写真だけこの方法で削除してください。


繰り返しになりますが、いきなり大切な写真で操作せず、テスト用画像で操作や動作を確認してください。

2019年9月8日日曜日

Googleのバックアップと同期の設定画面

Googleのバックアップと同期の設定画面は文章だけで説明するのが面倒なので設定画面のキャプチャ画像を張ってみた。

1.タスクトレイの通知領域に表示されるGoogleのバックアップと同期アイコン



1-1.待機状態のGoogleのバックアップと同期アイコン

1-2.同期実施状態のGoogleのバックアップと同期アイコン

2.Googleのバックアップと同期アイコンクリック時のポップアップメニュー


2-1.Googleドライブアイコンのポップアップメニュー

3.Googleのバックアップと同期設定画面

3-1.マイ パソコン


3-1-1.設定画面:マイ パソコン
①バックアップしたいフォルダにチェックを入れます
②バックアップしたいフォルダを①のリストに追加します
③バックアップ対象のファイルを選択します。
「すべてのファイル」か「写真と動画だけ」か選びます。
④写真と動画のアップロードサイズを選択します。
「高画質」か「元の画質」か選べます。
「高画質」は画質の変化を伴います。
⑤ファイル削除時の動作を選択します。Googleフォトには影響を与えません。
常に両方のコピーを削除する: パソコンまたは drive.google.com からアイテムを削除すると、他の場所からも削除されます。
両方のコピーを削除しない: パソコンからアイテムを削除しても、drive.google.com からは削除されません。
両方のコピーを削除する前に確認メッセージを表示する: パソコンからアイテムを削除するときに、他の場所からも削除するかどうかを確認するメッセージが表示されます。
⑥写真をGoogleフォトにアップロードするときにチェックを入れます
⑦カメラなどのデータを直接アップロードしたいときに設定します。

3-2.Googleドライブ


3-2.設定画面:Googleドライブ

3-3.設定


3-3.設定画面:設定

























2019年8月15日木曜日

デジカメの画像をGoogleフォトにアップロードする方法(iPhone/iPad)

デジカメで撮影した写真をGoogleフォトにアップロードする方法としては一度パソコンに取り込んでから「バックアップと同期」を用いてバックアップする方法がある。
しかし、「バックアップと同期」を用いてGoogleフォトにアップロードするとドライブ側にもアップロードされてしまいます。また、写真をバックアップするためにパソコンを起動する必要もあります、めったにパソコンを使用しない私としては非常に煩わしい。

そこで、おススメの方法はLightning SDカードカメラリーダーを使用して、デジカメの写真をiPhoneやiPadのカメラロールに取り込み、「Googleフォト」アプリでGoogleフォトにバックアップする方法です。カメラロールへの取り込みは迷うこともなく、簡単に行えるので特に方法は説明しません。旅行中にデジカメの写真を手軽にバックアップできることも魅力の一つです。
名前の通り、Lightningコネクタに接続してSDカードに保存されているデジカメのデータをカメラロールに読み込むためのものだが、アップル純正という安心感と写真の管理が一気に楽になることを考えると¥2,800 (税別)は非常にお買い得だとおもう。(¥3,200で購入したのにいつの間にか値下げされている、、、が満足している。)
カメラデータ以外読み込めないし、リーダーというだけあって書き込むこともできない、サードパーティ製のSDカードアダプタはもう少し高機能かもしれないので、そういったものにも興味がある人は調べてから購入するとよい。

我が家の目的としてはSDカードリーダーが良かったのですが、Lightning USBカメラアダプタやLightning USB 3カメラアダプタやUSB-C SDカードリーダーなどもあるので用途に合わせて選んでください。

「互換性」の項目にご自身の端末が登録されていることも忘れずにチェックしてください。

2019年1月19日土曜日

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

Google Driveで指定したフォルダ以下に配置されているすべてのファイルに対して処理を行うGoogle Apps Script(GAS)を考えてみた。普通に考えるとフォルダ内のファイルとフォルダを処理する関数を再帰的に呼び出すのだが、、、GASには6分以上経過すると強制的にスクリプトが終了してしまう制限がある。

この6分の壁対策として時間トリガを設定して中断した処理を再開する必要がある。しかしその再開のためにはどこまで処理したかを保存しなけれがならない。一般的には中断するときにFileIteratorクラスのgetContinuationToken()を使用してトークンを取得しそれをスクリプトプロパティなどに保存して、時間トリガで起動した際にはDriveAppクラスのcontinueFileIterator(continuationToken)でFileIteratorを復元する。

しかし、再帰的に関数を呼び出している処理を中断するのは少しコツがいる。

今回は単純な方法を紹介する。
  1. ドライブで管理しているすべてのFileIteratorを所得する
  2. FileIteratorですべてのファイルをスキャンする
  3. ファイルが指定したフォルダ配下かどうかをチェックする
  4. 5分経過した場合にはFileIteratorをプロパティに保存する
ただし、この方法には欠点がある。すべてのファイルで、親フォルダをスキャンするので処理が重く、FileIteratorは1週間という有効期限があるため、ドライブ内のすべての処理が1Week以内で終わるようにトリガタイミングと処理を調整しなければならない。

そのうち6分の壁対策版の再帰処理コードも載せると思う。

function start()
{
  // プロパティとトリガをすべて削除
  PropertiesService.getScriptProperties().deleteAllProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

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

function triggerFunction() {
  var StartTime   = new Date();
  var TargetFolder = "1zXo...処理対象のフォルダID....xEfEI";

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

  while(files.hasNext()) {
    var CurrentTime = new Date();
    // トリガを設定する。(5分間実行していたら止める。)
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      PropertiesService.getScriptProperties().setProperty("ContinuationToken",files.getContinuationToken());
      return;
    }
    var file = files.next();
    if( checkSubordinate( file, TargetFolder ) )
    {
      ////////////////////////////////////
      // ファイルごとの処理をここから記載する。

      // ★★ ここにfileに対する処理を記載する ★★

      // ファイルごとの処理をここまでに記載する。
      ////////////////////////////////////
    }
  }
  
  PropertiesService.getScriptProperties().deleteAllProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

// targetが指定フォルダ(folder ID)の配下のフォルダかチェックする。
function checkSubordinate(target,folderID)
{
  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;
    }
  }
  return false;
}

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(); // メールに対する処理を実施
}

2019年1月2日水曜日

Google Apps Script でプロパティをファイルに保存する

GoogleAppsScriptは便利だがいろいろと困ることがある、その代表格は6分制限。スクリプトの実行時間が6分を超えると途中で処理が終了してしまいます。 その対応として実行経過を保存し時間駆動型のトリガーを設定して、細切れに処理をする必要がある。これについてはGoogleで検索すれば山のように情報が出てくるので割愛する。
今回その途中経過を保存する方法について書いてみる。 一般的にはProperties ServiceのsetProperty(key, value)などを用いると非常に楽である。しかし、9217文字以上の文字列を扱おうと思うとエラーが発生してしまう。

var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty(
  'key_string',  // keyにもサイズ制限があるかもしれないが未検証
  'ここに文字数が9217以上の文字列を指定、XXXXXXXXXX');
}

上の例では以下のようにエラーではじかれる。
引数が大きすぎます:value(行 nn,ファイル「コード」)
そこまで大きい値を扱わない場合には問題ないが、DriveAPIで複雑な検索式でsearchFilesを行った場合のContinuationTokenがこの上限を大幅に超えることがある。9216文字以下になるように分割して複数のプロパティに保存することで対処可能ではあるが、今回はプロパティをファイルに保存する方法を考えてみたので忘れないようにメモ。使い方はそのうち別記事で記載します。

初回起動時にプロパティファイルを生成し、そのファイルIDをProperties Serviceで保存しておく。ファイルIDが変わらない限りはプロパティファイルの名前が変更されても参照が継続する。使い勝手が良いのか、悪いのかはよくわからない。
// 使い方はfunction examplePersistentProperty()を参照

// PersistentProperty()
// 概要:クラスのコンストラクタ
// 引数:filename  プロパティ保存する初期ファイル名
// 戻り値:インスタンス
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
// 備考:一度生成したファイルを参照し続ける。ファイル名を変えても参照し続ける。
PersistentProperty = function(filename){
  this.file = null;
  var id =  PropertiesService.getUserProperties().getProperty("PropertiesFileID");
  try{
    if( id!=null )
    {
      // ファイル取得を試みる
      this.file = DriveApp.getFileById(id);
      if( this.file.isTrashed() )
      {
      // ユーザープロパティから取得したファイルがゴミ箱にある場合には廃棄する
        this.file=null;;
      }
    }
  }
  catch(e)
  {
    // ファイル取得に失敗した場合は
    this.file=null;
  }
  // プロパティファイルがない場合には新規に作成する。
  if( this.file==null )
  {
    var name = filename;
    this.file =  DriveApp.createFile(name,"{}",MimeType.PLAIN_TEXT);
    var fileId = this.file.getId();
    PropertiesService.getUserProperties().setProperty("PropertiesFileID",fileId);
  }

  // ファイルからテキスト読み込み
  var text =  this.file.getBlob().getDataAsString();
};


// deleteAllProperties()
// 概要:保存されているプロパティをすべて消去
// 引数:なし
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteAllProperties();
// 備考:
PersistentProperty.prototype.deleteAllProperties = function(){
  this.file.setContent(JSON.stringify({}));
};

// deleteProperty(key)
// 概要:指定したプロパティを消去
// 引数:key  削除するプロパティを指定するキー
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.deleteProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  try{
    // 指定されたプロパティ削除
    delete Properties[key];
  }
  finally
  {
    // 最終的にオブジェクトをファイルに保存
    this.file.setContent(JSON.stringify(Properties));
  }  
};

// getKeys()
// 概要:保存しているプロパティのキーの一覧を取得
// 引数:なし
// 戻り値:キーの一覧(string[])
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var keys = pp.getKeys()
// 備考:
PersistentProperty.prototype.getKeys = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  var keys = [];
  for (key in Properties) {
    keys.push(key);
  }
  return keys;
};

// getProperties()
// 概要:保存しているプロパティリストを取得
// 引数:なし
// 戻り値:保存しているプロパティリスト(連想配列)
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var properties = pp.getProperties();
//   var value = properties['PropaertyKeyString'];
// 備考:
PersistentProperty.prototype.getProperties = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);
  return Properties;
};

// getProperty(key)
// 概要:指定したプロパティを取得
// 引数:key  取得するプロパティを指定するキー
// 戻り値:プロパティの値
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var value = pp.getProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.getProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( key in Properties ){
    return Properties[key];
  }else{
    return null;
  }
};

// setProperties(properties,deleteAllOthers)
// 概要:プロパティリストを設定
// 引数:properties  設定するプロパティリスト(連想配列)
//   :deleteAllOthers  trueを指定すると他のプロパティをクリア、書略時はfalseとして動作する
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperties({ "key1":"value1","key2":"value2"},true); // 他のプロパティは削除
//   pp.setProperties({ "key3":"value3","key4":"value4"}); // プロパティを追加
// 備考:deleteAllOthersがfalseの場合はプロパティリストの追加
PersistentProperty.prototype.setProperties = function(properties,deleteAllOthers){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( deleteAllOthers ){
    Properties = properties;
  }else{
    for( var key in properties ){
      Properties[key] = properties[key];
    }
  }
  var json = JSON.stringify(Properties);
  this.file.setContent(json);
};

// setProperty(key,value)
// 概要:プロパティを設定
// 引数:key  設定するプロパティのキー
//   :value  設定するプロパティの値
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperty( "key1", "value1" );
// 備考:
PersistentProperty.prototype.setProperty = function(key,value){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  Properties[key] = value;
  this.file.setContent(JSON.stringify(Properties));
};

//////////////////////////////////////////////////////////////////////////////
// これ以降は使い方サンプル及びDEBUG用
//////////////////////////////////////////////////////////////////////////////
function examplePersistentProperty()
{
  // ファイル名を決める
  var pp = new PersistentProperty('PersistentPropertyTest');
  
  // プロパティのセット
  pp.setProperty( "test1","test1 value" );

  // プロパティを複数追加
  pp.setProperties(
    { "test2":"test2 value","test3":"test3 value"});

  // プロパティを複数追加
  pp.setProperties(
    { "test4":"test4 value","test5":"test5 value"},false );

  // 一つずつ取得してもよい
  var test1 = pp.getProperty("test1");
  
  // 一気に取得してもよい
  var properties = pp.getProperties();
  var test2 = properties["test2"];
 
  // プロパティを一部削除
  pp.deleteProperty("test4");
  
  // プロパティを一部削除や追加し一気に更新
  delete properties["test5"];
  properties["test6"] = "New Property";
  pp.setProperties(properties,true);
  
  // 全プロパティを列挙
  var keys = pp.getKeys();
  for( var i=0; i<keys.length; i++ ){
    var key = keys[i]
    var value = pp.getProperty(key);
  }

  // 全プロパティを削除
  pp.deleteAllProperties();
}