ラベル Google Apps Script の投稿を表示しています。 すべての投稿を表示
ラベル Google Apps Script の投稿を表示しています。 すべての投稿を表示

2022年11月29日火曜日

Google Formsでドロップダウン形式の日付を毎日変更する

背景

グーグルフォームのGASで自動で毎日ドロップダウン形式の日付が変わる仕組みの質問&質問があり、リファレンスマニュアルをみてなんとなくできそうなことはわかっていたが、実際に作ってみた。

ソースコート

function updateForm() {
  // 毎日実行するトリガを設定
  setTrigger( arguments.callee.name );

  var forms = FormApp.getActiveForm();
  var items = forms.getItems();

  var table = [
    {'title':'発症日','start':-14 ,'end':0 }, // 二週間前から今日まで
    {'title':'診察希望日1','start':7 ,'end':1 }, // 7日後から明日まで(逆順)
    {'title':'診察希望日2','start':1 ,'end':7 } // 明日から7日後まで
  ];
  
  // 途中に日付が変わると面倒なのでここで取得
  var today = new Date();
  items.forEach( (item) =>{
    table.forEach( (range) =>{
      // リスト形式で質問がテーブルのtitleと一致したときに処理
      if( item.getType() == FormApp.ItemType.LIST && item.getTitle() == range.title )
      {
        item = item.asListItem();
        var choices = [];

        // テーブルの内容に従い選択肢を追加
        var dir = (range.start<range.end)?1:-1;
        for( var i = range.start; (range.start<=i&&i<=range.end)||(range.end<=i&&i<=range.start); i += dir )
        {
         var day = new Date( today );
          day.setDate(today.getDate() + i);
          choices.push( item.createChoice(formatDate( day, "yyyy/MM/dd" )));
        }
        item.setChoices(choices);
      }
    });

  });
}

function formatDate (date, format) {
  format = format.replace(/yyyy/g, date.getFullYear());
  format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/dd/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/HH/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  format = format.replace(/SSS/g, ('00' + date.getMilliseconds()).slice(-3));
  return format;
}

function setTrigger(name) {
  // 念のため既存のトリガはすべて削除してから次のトリガーを設定する
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( (trigger) =>{
    if( trigger.getHandlerFunction() == name )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  var setTime = new Date();
  setTime.setDate(setTime.getDate() + 1)
  setTime.setHours(12);
  setTime.setMinutes(00); 
  ScriptApp.newTrigger(name).timeBased().at(setTime).create();  
}

フォームに追加したプルダウン



2022年5月28日土曜日

GASでドライブの画像を表示する

知恵袋への回答は質問に直接答えたが、画面ロード時に時間がかかりそうなので処理を全体的に変えてみた。


コード.gs
function doGet(e) {
  return HtmlService.createTemplateFromFile("index.html").evaluate().setTitle("タイトル");
}

function GetBase64Image(fileId,param)
{
  var file = DriveApp.getFileById(fileId);
  var blob = file.getBlob();
  var contentType = blob.getContentType();
  var base64 = Utilities.base64Encode(blob.getBytes());
  var imageSrc = "data:" + contentType + ";base64, " + base64;
  return {"name":file.getName(),"id":fileId,"base64":imageSrc,"param":param};
}
index.html
<html>
<head>
  <script>
    <?
      var folder = DriveApp.getFolderById("1MdeTdaYDv9BK9QTaZes5_bTDRpKiLRnA");
      var files = folder.getFiles();
      var list = [];

      while (files.hasNext()) {
        var file = files.next();
        list.push({"id":file.getId(),"name":file.getName(),"url":file.getUrl()});
      }
    ?>
    var list = <?!= JSON.stringify(list) ?>;
  
    function onSelected(event)
    {
      var img = document.getElementById("image")
      img.alt = "Loading....";
      img.src = null;
      var fileId = event.currentTarget.value;
      google.script.run
        .withSuccessHandler( SuccessGetBase64Image )
        .withFailureHandler( Failure )
        .GetBase64Image( fileId, img.id );
    }
    function Failure(){
      alert("失敗");
    }
    function SuccessGetBase64Image(entry)
    {
      document.getElementById(entry.param).alt = entry.name;
      document.getElementById(entry.param).src = entry.base64;
    }

    function onLoad()
    {
      var select = document.getElementById("select");
      select.addEventListener('change', onSelected);
      list.forEach( file => {
        var option = document.createElement("option");
        option.value = file.id;
        option.innerText = file.name;
        select.appendChild(option);
      });
    }
  </script>
</head>
<body onload="onLoad();">

<label for="select">Choose a menu:</label>
<select name="select" id="select">
<option value="">--Please choose an option--</option>
</select>

<p><img src="" height="562" width="750" alt="選択されてません" align="top" id="image">ここに画像表示</p>
</script>

</body>
</html>

2022年2月26日土曜日

GoogleDriveで共有されたデータをマイドライブにコピーする

G Suite無料のアカウントを無料で使い続ける方法を知らない時にドライブのデータを頑張って移動するために作ったスクリプトを紹介します。
オーナー権限を変更するスクリプトもあったのですが、G Suite(Google Workspace)の制約で組織外のユーザーに権限移行ができないため、今回はフォルダ階層を維持したままコピーするスクリプトを作成しましたり

アカウントを廃止するなどの目的でGoogleDriveで共有されたフォルダを自分のアカウントにコピーするスクリプトです。ご利用は自己責任で、、、
var me;
  function CopyFolders() {
  var url = "https://drive.google.com/drive/folders/6dpqdZ1ESK<< ここはコピー元のフォルダURL >>yPoQBdhwEOP";

  var idAndResoucekey = url.replace(/.*\//g,"").split("?");
  var id = idAndResoucekey[0];
  var resoucekey = null;
  me = Session.getActiveUser();
  if( idAndResoucekey.length == 2 )
  {
    resoucekey = idAndResoucekey[1].replace(/.*=/,"");
  }
  var srcFolder = DriveApp.getFolderByIdAndResourceKey(id,resoucekey);
  var dstFolder = getFolder( null, '共有フォルダからのコピー', false );
  CopyFolder( srcFolder, dstFolder );
  
}

function CopyFolder(src,dst){
  var folders = src.getFolders();
  var files = src.getFiles();
  while( folders.hasNext() )
  {
    var folder = folders.next();
    CopyFolder( folder, getFolder( dst, folder.getName() ) );
    if( src.getFiles().hasNext() )
    {
      Logger.log("削除できないファイルが残ってる?");
    }else{
      folder.removeEditor(me);
    }
  }
  while( files.hasNext() )
  {
    var file = files.next();
    try
    {
      file.makeCopy(file.getName(),dst);
      file.removeEditor(me);
    }
    catch(e)
    {
      Logger.log(file.getName()+" 削除失敗");
    }
  }
}


/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @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;
}

2022年2月22日火曜日

Googleスプレッドシートに書かれたデータに関してWEBからデータを取得する

サンプルとしてGoogle Apps Scriptでスプレッドシートに書かれた郵便番号から住所を調べるスクリプトを作成した。郵便番号を取得するなら別の方法をお勧めする、あくまでGASの使い方説明の素材にしただけです。
エラー処理などを皆無だし、HTMLから必要なデータを抽出する部分も適当です。
スプレッドシートなどから呼び出した際にすぐに処理を抜けたい場合はstart関数を呼び出す、いきなり処理したい場合やデバッグ時はzipSearch関数を呼び出す。5分経過したらいったん中断し、1分後に処理を再開しまう。

A:B列を取得してB列が空の行のみを処理します。すべて処理が終わるか5分経過するかで結果をスプレッドシートに書き込みます。都度書き込むとAPI呼び出し回数が増え、処理も遅くなります。


function start(){
  // 1分後にスタート
  setTrigger("zipSearch");
}
function zipSearch() {
  // 一旦トリガを削除
  clearTrigger( arguments.callee.name );
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // シートから入出力部分を取得
  // A列に郵便番号(”222-0001”)が入力されており、対応する住所をB列に入力する
  let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  let range = ss.getRange(1,1,ss.getLastRow(),2);
  let values = range.getValues();

  // 一行ずつ処理を実施
  values.forEach( row =>{
    if( row[0] != "" && row[1] == "" )
    {
      let url = "https://www.post.japanpost.jp/cgi-zip/zipcode.php?zip="+row[0];

      // 今回は<td class="data"><small>.*?</small></td>を抽出して処理
      let table = UrlFetchApp.fetch(url)
                  .getContentText()
                  .match(/<td class="data"><small>.*?<\/small><\/td>/gis)
     if( table!=null )
     {
       // 結合しタグを削除
       row[1] = table.join(" ").replace(/<.*?>/gis,"");
     }
     else
     {
       row[1] = "error";
     }
    }
    // スクリプト開始から5分経過したら一旦終了
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      // 結果をシートに書き戻す
      range.setValues(values);
      //トリガをセットして終了
      setTrigger(arguments.callee.name);
      return;
    }
  });
  // 結果をシートに書き戻す
  range.setValues(values);
}

function setTrigger(func)
{
  trigger = ScriptApp.newTrigger(func)
            .timeBased()
            .everyMinutes(1)
            .create();
}

function clearTrigger(func)
{
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( trigger=>{
    if( trigger.getHandlerFunction() == func )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

2022年2月20日日曜日

Googleドライブのフォルダに保存している画像をOCRしてドキュメントを生成する

画像をOCRして、画像とOCR結果が貼り込まれたドキュメントを生成するスクリプトです。

function main() {
  // マイドライブ\'GoogleAppsScript\dev\test-data\変換元データ
  var srcFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換前データ'], false );
  var dstFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換後データ'], false );


  let option = {
    "ocr": true,// OCRを行うかの設定です
    "ocrLanguage": "ja",// OCRを行う言語の設定です
  }
  var files = srcFolder.getFiles();
  while( files.hasNext() )
  {
    var file = files.next();
    var resource = {
      title: file.getName()
    };
    let doc = DriveApp.getFileById( Drive.Files.copy(resource, file.getId(), option).id);
    doc.moveTo( dstFolder );
  }

}

/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @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;
}

2022年2月12日土曜日

AdSenseのデータをスプレッドシートに書き込むスクリプト

以下のスクリプトを実行するとAdSenseのデータをスプレッドシートに取り込みます。 未取得分だけを取得して追加するので、時間駆動のトリガで1時間おきにでも実施するのが良いです。 取り込んだデータはスプレッドシート側でデータ集計したり、グラフを作成してください。
function myFunction() { // 1時間おきのトリガを設定したい場合はこちらを実行
  trigger = ScriptApp.newTrigger("report")
  .timeBased()
  .everyHours(1)
//  .everyMinutes(5)
  .create();
  report();
}

function report() {
  var adsenseClientID = 'ca-pub-NNNNNNNNNNNNNNNN';
  var adsenseAccounts = 'pub-NNNNNNNNNNNNNNNN';
  var metrics = ['ESTIMATED_EARNINGS', 'PAGE_VIEWS', 'CLICKS', 'PAGE_VIEWS_CTR', 'COST_PER_CLICK', 'PAGE_VIEWS_RPM'];
  var sheetName = 'report';
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(sheetName);
  if (sheet == null) {
    sheet = ss.insertSheet(sheetName);
  }

  const today = new Date();
  var lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    var last = new Date('1900/1/1');
    sheet.getRange(1, 1, 1, metrics.length+1).setValues([['Date'].concat(metrics)]);
    lastRow = 2;
  }
  else {
    var last = new Date(sheet.getRange(sheet.getLastRow(), 1).getDisplayValue());
  }

  var report = AdSense.Accounts.Reports.generate(
    'accounts/'+adsenseAccounts,
    {
      // Specify the desired ad client using a filter.
      filters: ['AD_CLIENT_ID=='+adsenseClientID],
      metrics: metrics,
      dimensions: ['DATE'],
      dateRange: 'CUSTOM',
      'startDate.year': last.getFullYear(),
      'startDate.month':last.getMonth() + 1,
      'startDate.day':last.getDate(),
      'endDate.year': today.getFullYear(),
      'endDate.month':today.getMonth() + 1,
      'endDate.day':today.getDate(),
      // Sort by ascending date.
      reportingTimeZone: 'ACCOUNT_TIME_ZONE',
      orderBy: ['+DATE']
    } );

  sheet.getRange(lastRow, 1, report.rows.length, report.rows[0].cells.length)
    .setValues(report.rows.map(row => row.cells.map(cell => cell.value)));
}

Reports.generate関数はヘルプ(Method: accounts.reports.generate)を、metricsやdimensionsに指定する値はヘルプ(Metrics and Dimensions)を参照してください。

2022年2月7日月曜日

厚生労働省のオープンデータから新規陽性者数の推移(日別)を取得してスプレッドシートに書き込むスクリプト

厚生労働省のオープンデータから新規陽性者数の推移(日別)を取得してスプレッドシートに書き込む処理を毎日行うGoogle Apps Script

function myFunction() {
  trigger = ScriptApp.newTrigger("scraping")
  .timeBased()
  .everyDays(1)
  .create();
  scraping();
}
  
function scraping() {
  var content = UrlFetchApp.fetch("https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv").getContentText();
  var values = Utilities.parseCsv(content);
  var sheet = SpreadsheetApp
            .getActiveSpreadsheet()
            .getSheetByName('シート1');
  sheet.getRange(1, 1, values.length, values[0].length).setValues(values);  
}

2022年2月6日日曜日

GoogleAppsScriptを使用しHTML内のテーブルをスプレッドシートに書き込む

Googleドライブの特定のフォルダに格納されているHTMLファイルの中に記載されているテーブルをスプレッドシートに書き込むスクリプト

どんな利用シーンがあるか分かりませんが、知恵袋で質問があったので作成してみた。なにかのツールが吐き出す結果のHTMLファイルにテーブルが含まれていて、その結果をシートにまとめたいという要望だと思う。

まったく同じようなシーンはあまり考えにくいが、何かの役に立つかもしれないので自分用のメモ代わりに残しておく。

6分越え対策やテーブルタグ(<tr>や<td>)内のデータ抽出などもふくめてエラー処理などかなり雑です、コメントで指摘してくれたら修正するかもしれません。


ソースコード

function myFunction() {
  // HTMLファイルが格納されているフォルダ階層を配列形式で指定する。
  // マイドライブ/AAA/BBB/ フォルダを指定するときは["AAA","BBB"]と指定する。
  var srcFolder = getFolder( null,["GoogleAppsScript","dev","HTML内の表を抜き出す","未処理"]);
  // HTMLファイルが格納されているフォルダ階層を配列形式で指定する。
  var dstFolder = getFolder( null,["GoogleAppsScript","dev","HTML内の表を抜き出す","処理済み"]);

  // スプレッドシートの指定は環境に合わせてください。
  var sheet = SpreadsheetApp
            .getActiveSpreadsheet()
            .getSheetByName('シート1');

  // スプレッドシートに書き込む二次元配列
  var table = [];

  // 一気に処理すると6分で終わらないので適当な回数で区切る。
  // 数が多いときは時間主導のトリガで繰り返し実行するとよい。
  var numberOfFiles = 50;

  var htmlFiles = srcFolder.getFiles();
  while (htmlFiles.hasNext()) {
    // 指定したファイル数の処理が完了したら抜ける。
    numberOfFiles--;
    if( numberOfFiles < 0 ){
      break;
    }

    var file = htmlFiles.next();
    var text =  file.getBlob().getDataAsString();
    
    // ここからが必要なデータの取り出しとシートへの貼り込み
    // <tr></tr>の抜き出して1行分を取り出す。
    var rows = text.match(/<tr(?:\s.+?)?>.*?<\/tr\s*?>/gis);
    for( var r=0; r<rows.length; r++ )
    {
      // 1行分の配列
      var arrayCols = [];
      // <th></th>もしくは<td></td>を抜き出す。
      var cols = rows[r].match(/(<th(?:\s.+?)?>.*?<\/th\s*?>)|(<td(?:\s.+?)?>.*?<\/td\s*?>)/gis);
      for( var c=0; c<cols.length; c++ )
      {
        // <th>,</th>,<td>,</td>を削除して配列に格納する
        arrayCols.push( cols[c].replace(/^(<(td|th)(?:\s.+?)?>)|(<\/(td|th)\s*?>$)/ig,""));
      }
      // 1行分の配列を追加
      table.push(arrayCols);
    }
    // 処理が完了したファイルは処理済みフォルダに移動
    file.moveTo( dstFolder );
  }
  // ファイル移動と書き込みは同じ場所が理想
  // 処理高速化を考えてファイル移動はループ内
  // スプレッドシートへの書き込みはループの外に移動
  sheet.getRange(sheet.getLastRow()+1,1,table.length,table[0].length).setValues(table);
}


/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @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;
}

1つ目のHTMLファイル

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ひこざるさん</title>
</head>
<body>
    <a href="https://blog.hikozaru.com">ひこざるさんのブログ</a>
    <table border="1">
      <tr>
        <th>XXX</th>
        <th>YYY</th>
        <th>zzz</th>
      </tr>
      <tr>
        <td>111X</td>
        <td>111Y</td>
        <td>111Z</td>
      </tr>
    </table>
</body>
</html>

2つ目のHTMLファイル

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ひこざるさん</title>
</head>
<body>
    <a href="https://blog.hikozaru.com">ひこざるさんのブログ</a>
    <table border="1">
      <tr>
        <th>XXX</th>
        <th>YYY</th>
        <th>zzz</th>
      </tr>
      <tr>
        <td>222X</td>
        <td>222Y</td>
        <td>222Z</td>
      </tr>
    </table>
</body>
</html>

実行結果

2021年3月21日日曜日

Google PhotosとGoogle Driveのデータを他のアカウントに移行する方法

大学からもらったアカウントでGoogleフォトやGoogleドライブを運用していて、そのまま卒業して大事な青春時代のデータを失くす人がちらほらいるので、そうならないようにデータを移行する方法をまとめた。


Googleデータのエクスポート機能でパソコンのHDDにデータを保存するのが良い。その方法についてはhttps://support.google.com/accounts/answer/3024190?hl=jaに記載されているので参照するとよい。この際にデータをDriveに保存したりメールで受信する方法もあるが、アカウントが消えるとDriveにアクセスできなくなるし、メールも閲覧できなくなるので確実にパソコンにダウンロードしておくことをお勧めする。


Googleフォトのデータ移行

これについてはGoogleフォトのデータを別のアカウントに移行するを参照してください。


Google Apps ScriptによるGoogleドライブのデータ移行

Googleドライブにはデータ共有機能があるが共有しただけではオーナーアカウントが消えたときにデータがなくなってしまう。共有設定だけでなくオーナー変更も行う必要がある。

ファイル数が少ない場合は手動で行えばよいが、データが多いときに手動で実行するのは大変です。そこで登場するのがその手順を自動で実行してくれるGoogle Apps Script(GAS)です。
Google Apps Scriptを使ったことがない人はGoogle Apps Scriptの始め方はみてほしい。

当然すべてのケースを検証しているわけではないので保障はできないので、この方法でデータが紛失しても責任は負えません。事前にのGoogleデータのエクスポート機能でパソコンにダウンロードしておくことをお勧めします。

手順

  1. 移行元アカウントでGoogleドライブにログインしGoogle Apps Scriptを作成する。
    この時スクリプト名としてわかりやすい名前を付ける。
    後ほど紹介するスクリプトを張り付け、移行先のアカウントの指定部分だけ編集する。
  2. 移行元アカウントでGoogle Apps Scriptを実行する。
  3. 移行先アカウントに上記スクリプトも含めて移行されるが、マイドライブの下に紐づけられていないので、紐づけを行うために移行先アカウントでもう一度スクリプトを実行する。
    手順1で作成したスクリプトも移行されているはずなので、再度同じスクリプトをそのまま実行する。スクリプトが見つからない場合は手順1でつけたわかりやすい名前で検索する。

スクリプトの紹介

  • 下で紹介しているスクリプトの"移行先アカウント@gmail.com"と"移行先アカウント確認用@gmail.com"を編集してください。
    • 移行先のアカウントを間違えるとほかの人にデータが移動してしまうので注意が必要です。
    • "移行先アカウント@gmail.com"と"移行先アカウント確認用@gmail.com"とで別の値を設定しています。
    • 実際に動かすときは同じ値にしてください。
  • 以降元アカウントで実行するときと、移行先アカウントで実行するときで処理を分けています。(日本語の気持ち悪い関数名にしています。)
    • 移行元アカウントで実行するときは、すべてのフォルダおよびファイルのオーナーを変更します。
    • 移行先アカウントで実行すると、移行されたデータをマイドライブに紐づけます。

function myFunction() {
  ////////////////////////////////////////////////
  // ここだけカスタマイズが必要(同じ値を設定する)
  var 移動先アカウント       = "移行先アカウント@gmail.com";
  var 移動先アカウント確認用 = "移行先アカウント確認用@gmail.com";
  ////////////////////////////////////////////////
  if( 移動先アカウント != 移動先アカウント確認用 )
  {
    return;
  }

  var スクリプト実行アカウント = Session.getActiveUser().getUserLoginId();

  if( 移動先アカウント == スクリプト実行アカウント ) {
    移行先アカウントで実行する処理();
  }
  else
  {
    移行元アカウントで実行する処理(スクリプト実行アカウント,移動先アカウント);
  }
}

function 移行元アカウントで実行する処理(predecessorId,successorId) {
  var folders = DriveApp.getFolders();
  while (folders.hasNext()) {
    var folder = folders.next();
    try {
      folder.addEditor( successorId );
      folder.setOwner( successorId );
//      folder.removeEditor( predecessorId );
    }
    catch(e) {
      Logger.log(e)
    }
  }
  
  var files = DriveApp.getFiles();
//  var scriptId = ScriptApp.getScriptId();
  while (files.hasNext()) {
    var file = files.next(); 
    try {
      file.addViewer( successorId );
      file.setOwner( successorId );
//      if( scriptId != file.getId())
//      {
//        file.removeEditor( predecessorId );
//      }
    }
    catch(e) {
      Logger.log(e)
    }
  }
}

function 移行先アカウントで実行する処理() {
  var root = DriveApp.getRootFolder();

  var folders = DriveApp.getFolders();
  while( folders.hasNext() )
  {
    var folder = folders.next();
    var parents = folder.getParents();
    if( parents.hasNext() == false){
      root.addFolder(folder);
    }
  }

  var files = DriveApp.getFiles();
  while( files.hasNext() )
  {
    var file = files.next();
    var parents = file.getParents();
    if( parents.hasNext() == false){
      root.addFile(file);
    }
  }

}

注意事項

  • ここで記載した方法で何かトラブルが発生しても責任は負えません。解決に向けて多少の協力は行えるかもしれません。
  • 2021/3/21時点で動作確認しています、Google側の仕様変更などで動作が変更される場合がありますのでご注意ください。
  • 本格的に検証していません、スクリプトの意味が分かる人が内容を理解したうえで実行してください。Drive ServiceのAPIリファレンスを参照にしてください。
  • 移行先アカウントの容量が足りないときにどのようになるか検証できていません。
  • Google Apps Scriptは一回に実行できる時間に制限があります。1回実行し途中までしか移行できていない場合、コメントアウトしている3箇所(合計6行)のコメントアウトを外したうえですべてのデータが移行できるまで繰り返し実行してください。
    • 移行完了したファイルやフォルダが移行元アカウントから見えなくなるのでスクリプトを実行するたびに移行未完了データを処理できるようになります。
    • スクリプトファイルが見えなくなると繰り返し実行できなくなるので除外するようにしています。
  • Googleデータのエクスポート機能で別の場所に保存してから実行することをお勧めします。

2020年7月26日日曜日

Google Driveで共有しているファイルやフォルダの一覧を表示する。

Googleドライブで共有しているものを探す方法を知りたい人がいたので、簡単にコードだけを張っておきます。(見てもらえそうなヒントだけ張っておきましたが、見てもらえるか、、、、)
といっても15,000程度のファイルを処理できるか検証途中です。

①GoogleのSpreadsheetでスクリプトエディタを開く
②以下のコードを張り付けて保存する
③メニューの「実行」>「関数を実行」>「start」を行う
④「ContinuationToken」というシートが追加され途中経過が保存される。
 6分間で処理しきれないので、処理を中断するためのトークンが保存されている
 A1セルおよびA2セルがFinishになったら処理終了
⑤結果は「SharedFolders」シートと「sharedFiles」シートに出力される。

  // メールに対する処理
function start()
{
  // ファイルとフォルダを並行して処理する
  // フォルダの処理は1分後にトリガ指定
  ScriptApp.newTrigger("CreateFolderList")
  .timeBased()
  .everyMinutes(5)
  .create();
  
  CreateFilesList();
  return;
}

function CreateFilesList() {
  
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // トリガを削除する
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if( triggers[i].getHandlerFunction() == arguments.callee.name )
    {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }

  // 出力シート
  var resultSheet = getSheet('SharedFiles');

  // ContinuationTokenを保存するセルを取得
  var ContinuationTokenRange = getSheet('ContinuationToken').getRange("A1");

  // ContinuationTokenを取得
  var ContinuationToken = ContinuationTokenRange.getValue();
  if( ContinuationToken == "" ){
    // ContinuationTokenが空の時は全ファイルを取得
    var targets = DriveApp.getFiles();
  }else if( ContinuationToken == "Finish" ){
    // 処理が終了している場合は実行しない。
    return;
  }else{
    // ContinuationTokenを取得できた場合はイテレータにセット
    var targets = DriveApp.continueFileIterator(ContinuationToken);
  }

  while( targets.hasNext() )
  {
    // 5分間実行したら処理を止めて、次のトリガーを設定する。
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      var trigger = ScriptApp.newTrigger(arguments.callee.name)
      .timeBased()
      .everyMinutes(5)
      .create();
      ContinuationTokenRange.setValue(targets.getContinuationToken());
      return;
    }

    // 処理対象を取得
    var target = targets.next();
    
    // 共有モードを取得
    var access = target.getSharingAccess();
    
    if( DriveApp.Access.PRIVATE != access )
    {
      // 共有している場合は出力用のデータを取得
      var path = createPathString(target);
      var owner = target.getOwner().getEmail();
      var id = target.getId();
      resultSheet.getRange(resultSheet.getLastRow()+1,1,1,4).setValues([[path,owner,access,id]]);
    }
  }
  ContinuationTokenRange.setValue("Finish");
  return;
}

function CreateFolderList() {
  
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // トリガを削除する
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if( triggers[i].getHandlerFunction() == arguments.callee.name )
    {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }

  // 出力シート
  var resultSheet = getSheet('SharedFolders');

  // ContinuationTokenを保存するセルを取得
  var ContinuationTokenRange = getSheet('ContinuationToken').getRange("A2");

  // ContinuationTokenを取得
  var ContinuationToken = ContinuationTokenRange.getValue();
  if( ContinuationToken == "" ){
    // ContinuationTokenが空の時は全フォルダを取得
    var targets = DriveApp.getFolders();
  }else if( ContinuationToken == "Finish" ){
    // 処理が終了している場合は実行しない。
    return;
  }else{
    // ContinuationTokenを取得できた場合はイテレータにセット
    var targets = DriveApp.continueFolderIterator(ContinuationToken);
  }

  while( targets.hasNext() )
  {
    // 5分間実行したら処理を止めて、次のトリガーを設定する。
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      var trigger = ScriptApp.newTrigger(arguments.callee.name)
      .timeBased()
      .everyMinutes(5)
      .create();
      ContinuationTokenRange.setValue(targets.getContinuationToken());
      return;
    }

    // 処理対象を取得
    var target = targets.next();
    
    // 共有モードを取得
    var access = target.getSharingAccess();
    
    if( DriveApp.Access.PRIVATE != access )
    {
      // 共有している場合は出力用のデータを取得
      var path = createPathString(target) + "/";
      var owner = target.getOwner().getEmail();
      var id = target.getId();
      resultSheet.getRange(resultSheet.getLastRow()+1,1,1,4).setValues([[path,owner,access,id]]);
    }
  }
  ContinuationTokenRange.setValue("Finish");
  return;
}

// targetファイル/フォルダの親フォルダをループしてパスを生成する
function createPathString(target)
{
  var path = target.getName();
  var cur = target;
  while( cur.getParents().hasNext() ){
    cur = cur.getParents().next();
    path = cur.getName() + "/" + path;
  }
  return path;
}

// 名前を指定してシートを取得、シートがないときは新規に作成する
function getSheet(sheetName)
{
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(sheetName);
  if( sheet === null )
  {
    sheet = ss.insertSheet(sheetName)
  }
  return sheet;
}

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();
}

GMAILに受信したメールをGoogle Apps Scriptで処理する

GMailに受信したメールを処理するスクリプトを作成した。
検索を行いその結果に対して処理を行うが、一度処理したメールは再度処理しないようにしなければならない。処理をしたメールにたいし、ラベルをつけたり、既読化したり、スターをつけたりすることで対処しようとしているページが多い。ラベルや既読化はスレッド単位で検索されてしまうので問題が発生するし、既読化やスターをつける方法は複数のスクリプトで処理したいメールがある場合に対応できない。

今回はメール受信日時で処理済みかどうかを判断する方法を採用した。時間駆動トリガで定期的に実行することを想定しているが、トリガを仕掛ける処理は組み込んでいないので、手動でトリガ指定する必要がある。処理量が多いと一回のスクリプト実行時間(6分)では処理しきれないこともあるため中断することも考慮に入れている。
目的に合わせて青字の部分をカスタマイズして使用する。


/// トリガ設定は行っていないので、手動で指定する必要があります。

function Main() {
  var ScriptStartTime = new Date();
//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズはここから
////////////////////////////////////////
  // メールを検索する条件(beforとafterは指定しない)
  var SearchString = 'in:all -in:spam -in:trash has:attachment';
    
  // 処理対象期間
  var SearchStartDate = new Date('2018/10/00 0:00:00');
  var SearchEndDate = new Date(ScriptStartTime);

  // 一回の検索で検索する期間:5分で処理できる量を指定するのが一番良い。
  var IncrementHours = 24;

  // メールに対する処理(例:メール受信日時フォルダを生成し、その中に添付ファイルを保存する)
  var Action = function(mail)
  {
    var DriveFolderName = "GmailAttachments";  
    var reciveDate = mail.getDate();
    var folderID = getDriveFolder([DriveFolderName,reciveDate.getFullYear(),reciveDate.getMonth()+1,reciveDate.getDate()]);
    var attachments = mail.getAttachments();
    for (var n = 0; n < attachments.length; n++)
    {
      folderID.createFile(attachments[n]);
    }
  }  
////////////////////////////////////////
// カスタマイズはここまで
//////////////////////////////////////////////////////////////////////////////////////

  var properties = PropertiesService.getScriptProperties().getProperties();
  
  // 検索開始日時をプロパティから取得する
  if( 'SearchStartDate' in properties ){
    SearchStartDate = new Date(properties['SearchStartDate']);
  }

  // 実施すべきジョブ(MailIDの配列)をプロパティから取得する。
  var JobList = [];
  if( 'JobList' in properties ){
    JobList = JSON.parse(properties['JobList']);
  }

  for( var start = SearchStartDate; start < SearchEndDate; )
  {
    // メール検索期間の設定
    var end = new Date(start);
    end.setHours(end.getHours() + IncrementHours);
    if( end > SearchEndDate ){
      end = new Date(SearchEndDate);
    }
    
    // JobListの生成(処理すべきメールのIDリスト)
    if( Object.keys(JobList).length == 0 )
    {
      JobList = getMailIdList(SearchString,start,end);
    }
    
    // JobListに従い処理実行
    while( JobList.length > 0 )
    {
      // スクリプト開始から5分経過している場合は実行経過をプロパティに保存して処理を中断する。
      var CurrentTime = new Date();
      if( (CurrentTime - ScriptStartTime) > 5*60*1000 )
      {
        properties['SearchStartDate'] = start;
        properties['JobList'] = JSON.stringify(JobList);
        PropertiesService.getScriptProperties().setProperties( properties );
        return;
      }
      
      Action(GmailApp.getMessageById(JobList.shift()));   // 処理実行関数呼び出し
    }
    
    // 検索期間を更新
    start = end;
  }
  properties['SearchStartDate'] = start;
  properties['JobList'] = JSON.stringify(JobList);
  PropertiesService.getScriptProperties().setProperties( properties );
}

// 指定期間で検索を行い、指定期間に受信したメールの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());
        }
      }
    } 
  }
}

//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここから
////////////////////////////////////////
// 名前を指定したフォルダ生成する、すでに存在する場合はそのフォルダを取得する
// 階層構造を一気に作成できます。
// 作成したいフォルダ:(RootFolder)/folderA/folderB/folderC
// 呼び出し方:getDriveFolder(["folerA","folderB","folderC"]);
function getDriveFolder( folderHierarchy, folder ) {
  if ( folder === undefined ) folder = DriveApp.getRootFolder();    
  for(var h = 0;h < folderHierarchy.length; h++ ){
    var childs = folder.getFoldersByName(folderHierarchy[h]);
    if( childs.hasNext() ) {
      // 指定された名前のフォルダがあるときはそのフォルダを使用
      folder = childs.next();
    } else {
      // 指定された名前のフォルダが見つからなかったときはフォルダを生成
      folder = folder.createFolder( folderHierarchy[h] );
    }
  }
  return folder;
}
////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここまで
//////////////////////////////////////////////////////////////////////////////////////

メールから必要な部分を抜き出してスプレッドシートに張り付ける場合のAction関数は以下のような感じで実現した。

  // メールに対する処理
  var Action = function(mail)
  {
    // 処理サンプル:以下のメールから必要な部分をシートに張り付ける。
      //    氏 名 様
      //    
      //    いつも ANAプリペシルバーマイルコース をご利用いただき、
      //    ありがとうございます。
      //    氏 名 様のカードのご利用がありましたのでご連絡します。
      //    
      //     【ご利用明細の承認番号】
      //      1234567
      //    
      //     【ご利用カード番号】
      //      ****-****-****-0000
      //      ※セキュリティ保護のため、下4桁のみ表示しています。
      //    
      //     【ご利用日時(日本時間)】
      //      2018/12/29 17:20
      //    
      //     【ご利用先】
      //      セブン-イレブン
      //    
      //     【ご利用金額】
      //      1,234 円
      //    
      //     【バリュー残高】
      //      123,456 円
      //    
      //    ご利用先は、加盟店からの売上情報到着前の情報となり、実際のご利用先と名称が異なる場合があります。
      //    (売上情報到着後の名称については、マイページにログインのうえ「ご利用明細の確認」メニューよりご確認ください。)
      //    なお、携帯電話料金などの月額料金のお支払いに登録され、
      //    9:00PM以降にバリュー残高からの加算・減算が発生した場合は、翌8:00AM以降のメール送信となります。
      //    
      //    ご不明点などありましたら、JCBプリペイドカードデスクにお問い合わせください。    
    
    
    var sheet = SpreadsheetApp.getActiveSheet();
    var valTitle = [["【ご利用日時(日本時間)】","【ご利用先】","【ご利用金額】","【バリュー残高】","【ご利用明細の承認番号】","【ご利用カード番号】"]];
    var i = sheet.getLastRow();
  
    if(i==0){
      sheet.getRange(1, 1, 1, valTitle[0].length).setValues(valTitle); //シートに貼り付け
      i=1;
    }
    var bodys = mail.getPlainBody().replace(/[\t \r]/g,'').split("\n");
    var valMsgs=[];
    valMsgs[0]=[];
    for(var l = 0;l < valTitle[0].length; l++ ){
      valMsgs[0][l] = bodys[bodys.indexOf(valTitle[0][l])+1];
    }
    sheet.getRange(i+1, 1, 1,valTitle[0].length).setValues(valMsgs); //シートに貼り付け
  }

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月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文字以上を指定すると失敗する模様。

関連情報

2018年3月31日土曜日

Google Apps Scriptで設定ファイルの保存・読込

内容
スクリプトの設定情報やスクリプトの実行状況などを保存する方法としてClass PropertiesServiceが提供されている。しかし、これには少し欠点があるので別の方法を考えてみたのでそれを紹介する。紹介といっても自分用の備忘程度なので細かい説明は書きません。

Class PropertiesServiceについて
データの共有範囲毎にメソッドが用意されている。非常に簡単にデータを保存できるので何かと便利です。後に述べる問題点に該当しなければジャンジャン使いましょう。

メソッド 共有範囲/編集閲覧
getDocumentProperties() すべてのユーザーで共有されるドキュメント(document,spreadsheet,form)のプロパティオブジェクトを取得する。
[T.B.D.]閲覧編集方法不明(調査未実施)
getScriptProperties() すべてのユーザーで共有されるスクリプトのプロパティオブジェクトを取得する。
スクリプトエディタの 「ファイル」>「プロジェクトのプロパティ」>「スクリプトプロパティ」 で閲覧編集可能。
getUserProperties() ユーザー毎でスクリプト毎のプロパティオブジェクトを取得する。
スクリプトエディタの 「ファイル」>「プロジェクトのプロパティ」>「ユーザープロパティ」 で閲覧編集可能。

詳細はClass PropertiesServiceを参照ください。

プロパティの保存方法
var properties = PropertiesService.getUserProperties();
  properties.setProperty("Target File ID","205Ps_mAhEFs.......Ugm1m");

プロパティの参照方法
var properties = PropertiesService.getUserProperties();
  var TargetFileId = properties.getProperty("Target File ID");

問題点
  • 複数のスクリプトで共有することができない。
  • 保存できるサイズに制限があり、実験的に9217文字以上を指定すると関数が失敗する事がわかっている。
    • 9216文字までと確定している場合には問題にならないが、不定長の場合には注意が必要。

解決策
ファイルにプロパティを保存できるようにします。用意している関数は以下の4つ。
  • getProperty(key)
  • setProperty(key,value)
  • deleteProperties()
  • deletePropertu(key)
使い方の例は以下の通り。
function myFunction() {
  // スクリプトを起動した回数をプロパティから取得
  var count = getProperty("count");
  if( count != null )
  {
    count++;
  }
  else
  {
    count=0;
  }
  // プロパティに値を保存
  setProperty("count",count);
  setProperty("hoge","123");
  setProperty("hogehoge","456");
  if( count > 1 )
  {
    // 指定したプロパティを削除
    deleteProperty("hogehoge");
  }
  if( count > 4 )
  {
    // すべてのプロパティを削除
    deleteProperties();
  }
}

上記を実行するとマイドライブの下に、[スクリプト名]_[スクリプト実行ユーザーメールアドレス]という名前のファイルが生成され、そこにプロパティが保存されている。 いかが一回スクリプトを実行したときに生成されるファイルの内容。
{"count":0,"hoge":"123","hogehoge":"456"}
実行するたびにスクリプトの内容が変化するので内容を確認しながら実行すると動作がわかると思う。

最後にコード
// 使い方
//  var properties = getProperty("count");
//  setProperty("count",12345);
//  deleteProperties();
//  deleteProperty("count");

// プロパティのファイル
var PropertiesFile=null;

// プロパティを取得する。
function getProperty(key)
{
  try{
    return JSON.parse(getPropertiesFile().getBlob().getDataAsString())[key];
  }
  catch(e)
  {
    return null;
  }
}

// プロパティをセットする。
function setProperty(key,obj)
{
  try{
    // エラーに備えてからのオブジェクトを生成
    var property = {};
    // ファイルからテキスト読み込み
    var text =  getPropertiesFile().getBlob().getDataAsString();
    // オブジェクト生成
    property =  JSON.parse(text);
  }
  finally
  {
    // 指定されたプロパティを追加
    property[key] = obj;
    // 最終的にオブジェクトをファイルに保存
    getPropertiesFile().setContent(JSON.stringify(property));
  }
}

// すべてのプロパティを削除する。
function deleteProperties()
{
  // PropertiesServiceに保存してあるファイルIDを取得
  var key = "PropertiesFileID";
  var id = PropertiesService.getUserProperties().getProperty(key);
  // PropertiesServiceを削除
  PropertiesService.getUserProperties().deleteProperty(key);

  try{
    if( PropertiesFile==null )
    {
      if( id != null ){
        DriveApp.getFileById(id).setTrashed(true);
      }
    }
    else
    {
      PropertiesFile.setTrashed(true);
    }
  }
  finally{}
}

// プロパティを削除する。
function deleteProperty(key)
{
  // エラーに備えてからのオブジェクト生成
  var property = {};
  try{
    // ファイルにアクセス
    var text =  getPropertiesFile().getBlob().getDataAsString();
    // ファイルの内容からオブジェクト生成
    property =  JSON.parse(text);
    // 指定されたプロパティ削除
    delete property[key];
  }
  finally
  {
    // 最終的にオブジェクトをファイルに保存
    getPropertiesFile().setContent(JSON.stringify(property));
  }  
}


// プロパティファイルを取得する。
function getPropertiesFile()
{
  if( PropertiesFile!=null )
  {
    return PropertiesFile;
  }
  else
  {
    var userProperties =  PropertiesService.getUserProperties();
    var id = userProperties.getProperty("PropertiesFileID");
    try{
      if( id!=null )
      {
        PropertiesFile = DriveApp.getFileById(id);
        if( PropertiesFile.isTrashed() )
        {
          PropertiesFile=null;;
        }
      }
    }
    catch(e)
    {
      PropertiesFile=null;
    }
    if( PropertiesFile==null )
    {
      // ロパティを保存するファイル名を決める
      // 必要に応じて変更する。
      // 複数のスクリプトで共有する場合はここを同じ名前にする。
      var name = DriveApp.getFolderById(ScriptApp.getScriptId()).getName()
                   + "_" + Session.getActiveUser().getEmail();
      PropertiesFile =  DriveApp.createFile(name,"{}",MimeType.PLAIN_TEXT);
      userProperties.setProperty("PropertiesFileID",PropertiesFile.getId());
    }
    return PropertiesFile;                             
  }
}


コードの説明は省略。。。
気が向いたら解説を追記します。

2018年3月18日日曜日

Google Driveにあるファイルサイズの合計を求める

Googleドライブのストレージ消費量はここ(新しいウインドウで開きます)で確認できます。しかし、確認できるのはストレージ容量を消費している容量です。
基本的にはそれでも困らないのですが、高画質設定のGoogleフォトにバックアップした画像などの容量を消費しないファイルサイズの合計を知りたいときもある?。

ということで、Googleドライブに保存されているファイルの総容量を数えてみようと思います。ですが今回はチョット応用して画像(静止画と動画)の総容量を求めてみます。

手順
  1. GoogleドライブにGoogle Apps Scriptをインストールし、新規のスクリプトを作成する。Google Apps Scriptを使ったことがない人はここ(新しいウインドウで開きます)を参照。
  2. 下記のスクリプトを張り付ける。
  3. スクリプト内のMain関数を実行する。(メニュー>実行>関数を実行>Main)
  4. 実行結果はメールで受け取ります。しかし、ファイル数が多い場合には複数回に分けで処理を実行します。そのため5分に1回程度途中経過を通知する集計中メールを飛ばすようにしています。完了メールが来るまで待ってください。

スクリプト
// メニュー>実行>関数を実行>Main を選択して実行してください
function Main(){
  PropertiesService.getUserProperties().deleteAllProperties();
  CountFileSize();
}
  
function CountFileSize(){
  var StartTime   = new Date();

  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

  trigger = ScriptApp.newTrigger("CountFileSize")
  .timeBased()
  .everyMinutes(5)
  .create();
  
  var UserProperties = PropertiesService.getUserProperties();
  var ContinuationToken = UserProperties.getProperty("ContinuationToken");

  if(ContinuationToken) {
    var images = DriveApp.continueFileIterator(ContinuationToken);
    var TotalFileSize = parseFloat(UserProperties.getProperty("TotalFileSize"));
  } else {
    // カウントしたい条件に応じてコメントアウトする行を変更してください。

    // ドライブのすべてのファイルをカウントする場合
    // var images = DriveApp.getFiles();

    // 自分がオーナーのファイルだけをカウントする場合
    // var images = DriveApp.searchFiles('"me" in owners');

    // 画像だけをカウントする場合
    var images = DriveApp.searchFiles('mimeType contains "image/" or mimeType contains "video/"');

    // 自分がオーナーの画像だけをカウントする場合
    // var images = DriveApp.searchFiles('(mimeType contains "image/" or mimeType contains "video/") and "me" in owners');

    var TotalFileSize = 0;
  }
  
  while(images.hasNext()) {
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 4*60*1000 ){
      UserProperties.setProperty("ContinuationToken", images.getContinuationToken());
      UserProperties.setProperty("TotalFileSize", TotalFileSize);
      // 集計中メールがうるさい場合は以下の一行をコメントアウトしてください。
      SendResult("集計中\n" + TotalFileSize + " Byte\n" + TotalFileSize/1024 + " KByte\n" + TotalFileSize/1024/1024 + " MByte\n" + TotalFileSize/1024/1024/1024 + " GByte\n" );
      return;
    }

    var file = images.next();
    TotalFileSize += file.getSize();
  }

  SendResult("完了\n" + TotalFileSize + " Byte\n" + TotalFileSize/1024 + " KByte\n" + TotalFileSize/1024/1024 + " MByte\n" + TotalFileSize/1024/1024/1024 + " GByte\n" );
  ScriptApp.deleteTrigger(trigger);
  UserProperties.deleteAllProperties();
}

function SendResult(MailBody){
  var address = Session.getActiveUser().getEmail();
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), "Googleドライブのファイルサイズの合計を求める", MailBody);
}

スクリプトの説明

要点だけ書きます、なんとなく処理の順に記載しますが、厳密には処理順ではありません。難しいコードではないのでコードを見てください。
  1. 起動はMain関数、実処理はCountFileSize関数
  2. Main関数では後述するトリガを削除する。
  3. 6分制限対策としてスクリプト起動に5分のタイマー起動のトリガーを設定、トリガではCountFileSize関数を呼び出す。(処理が完了したときにトリガを削除する。)
  4. カウントするファイルリストを取得する。
    ここでカウントしたいファイルを検索する。
    • ドライブ内のすべてのファイルをカウントしたい場合
      var images = DriveApp.getFiles();
    • 自分がオーナーのファイルだけをカウントする場合
      var images = DriveApp.searchFiles('"me" in owners');
    • 画像だけをカウントする場合
      var images = DriveApp.searchFiles('mimeType contains "image/" or mimeType contains "video/"');
    • 自分がオーナーの画像だけをカウントする場合var images = DriveApp.searchFiles('(mimeType contains "image/" or mimeType contains "video/") and "me" in owners');
    • 特定のオーナーのファイルをカウントする場合
      var images = DriveApp.searchFiles('"XXXX@google.com" in owners');
  5. スクリプト実行開始から4分経ったらユーザプロパティに必要な情報を保存して処理を中断する。すでにトリガ設定が終わっているのでここではトリガ設定の処理は行わなくてもよい。
  6. ユーザープロパティには「どこまで処理が完了したか」と「集計中のトータルファイルサイズ」を保存する。
  7. ファイルサイズをTotalFileSizeにどんどん加算していきます。
  8. 正常に終了したらトリガを削除し、本文に集計結果を含めて完了メールを送信する。
スクリプトの応用
高画質設定のGoogleフォトにアップロードした画像の総容量を調べたい場合
  1. Googleフォトを使用していない別ユーザにパートナー共有する。
  2. 別ユーザーはすべてのファイルをライブラリに保存する設定にする。
  3. 別ユーザーのGoogleドライブ設定で「Google フォト」フォルダを作成するをオンにする
  4. 別ユーザーで本スクリプトを実行する。もしくはGoogleフォトフォルダをオリジナルユーザーに共有し、オリジナルユーザー側でOwner指定で本スクリプトを実行する。

    2018年3月11日日曜日

    Google Apps Scriptの始め方

    手順



    1. ブラウザでGoogle Driveにログインする。
    2. ”新規” > "その他” > ”+アプリを追加” を選択する。

    3. 検索入力ボックスに”Google Apps Script”と入力しEnterキーを押して検索を行い、Google Apps Scriptを”+接続”する。
    4. ”新規” > "その他” > ”Google Apps Script” を選択する。
    5. コードを入力し、実行したい関数を指定して、実行を押す。(いきなり説明がいい加減、、、)

      今回は以下のスクリプトにした。
      function myFunction() {
        Logger.log("こんにちは");
      }
      
    6. すぐに実行できるかと思いきや、いろいろと許可しないといけない。ただし、こう言ったものはちょくちょく変化していくので細かい説明は省略。現時点での画面を貼っておくので、参考にしてほしい。
    参考になるかもしれない情報

    Googleドライブでフォルダに属していないファイルを集める

    Googleドライブを使用していると、どのフォルダにも属していないファイルが見つかることがある。 ドライブ内を検索したり、ストレージ容量消費が多い順にファイルを並べたりしたとき目にすることがある。
    • フォルダに属しているファイルの情報には”パス”という項目がある。


    • フォルダに属していないファイルの情報には本来あるべき”パス”という項目がない。
    見えないファイルがあるのは気持ち悪いので、行方不明ファイルを一か所のフォルダに集めちゃおうというのが今回の内容。

    手順
    1. GoogleドライブにGoogle Apps Scriptをインストールし、新規のスクリプトを作成する。詳細はここを参照。
    2. 下記のスクリプトを張り付ける。
    3. スクリプト内のMain関数を実行する。(メニュー>実行>関数を実行>Main)
    4. ドライブで管理しているファイル数が少ないときは一回の実行で完了するが、ファイル数が多くなると時間がかかり、Google Apps Scriptの制限の一つである6分以内という制限に引っかかってしまうため、繰り返し実行することですべてのファイルをスキャンする。処理が完了したらメールで通知を行う。
    スクリプト

    • 本スクリプトはフォルダに属していないファイルをマイドライブ直下のLostFilesフォルダにまとめる。
    • Main関数を一回実行すると処理が終了するまで定期的にスクリプトが実行される。
    • 処理が完了するとメールが送られてくる。

    // http://blog.hikozaru.com/2018/03/google.html
    // メニュー>実行>関数を実行>Main を選択して実行してください
    function Main(){
      PropertiesService.getUserProperties().deleteAllProperties();
      FindLostFiles();
    }
    
    function FindLostFiles(){
      var StartTime   = new Date();
    
      var triggers = ScriptApp.getProjectTriggers();
      for (var i = 0; i < triggers.length; i++) {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    
      trigger = ScriptApp.newTrigger("FindLostFiles")
      .timeBased()
      .everyMinutes(5)
      .create();
        
      var UserProperties = PropertiesService.getUserProperties();
      var ContinuationToken = UserProperties.getProperty("ContinuationToken");
    
      if(ContinuationToken) {
        var files = DriveApp.continueFileIterator(ContinuationToken);
      } else {
        var files = DriveApp.getFiles();
      }
      
      var Folder = getSubFolder( DriveApp.getRootFolder(),'LostFiles');
      while(files.hasNext()) {
        var CurrentTime = new Date();
        if( (CurrentTime - StartTime) > 4*60*1000 ){
          UserProperties.setProperty("ContinuationToken", files.getContinuationToken());
          return;
        }
        
        var file = files.next();
        if( !file.getParents().hasNext() )
        {
          Folder.addFile(file);
        }
      }
      // トークンを削除
      ScriptApp.deleteTrigger(trigger);
      UserProperties.deleteAllProperties();
      SendResult("処理終了\n" + Folder.getUrl());
    }
    
    function getSubFolder( baseFolder, subFolderName ) {
      var subFolders = baseFolder.getFoldersByName(subFolderName);
      if( subFolders.hasNext() )
      {
        return subFolders.next();
      }
      return baseFolder.createFolder(subFolderName);
    }
    
    function SendResult(MailBody){
      var address = Session.getActiveUser().getEmail();
      GmailApp.sendEmail(Session.getActiveUser().getEmail(), "フォルダに属していないファイルを探す", MailBody);
    }
    

    スクリプトの説明

    • Main関数はスタート用の関数。
    • FindLostFilesが時間主導トリガとして実行される。
    • スクリプト起動から4分程度でスクリプトを中断する。
    • 再開用のトリガはスクリプト起動時に設定するので中断時には設定不要。
    • 終了時にはファイルを集めたフォルダのURIと主にスクリプトの終了をメールで通知する。

    参考にした情報