自習用」カテゴリーアーカイブ

[GAS]DriveAppでファイルを移動する

stackoverflowの説明が端的でわかりやすい。Googleドライブ上のファイルがどのフォルダに入っているかは、いわゆるフォルダ構成とはイメージが異なる。Googleドライブであるファイルがどのフォルダに入っているのかは、Gmailのラベルに相当するものと考えればよい。同時に複数のフォルダに同一のファイルが所属できるのも、Gmailのラベルを思い出せばおかしなことではないだろう。

新しいファイルの生成は、ルートフォルダである「マイドライブ」で行われる。これは新しいファイルに「マイドライブ」のラベルが付いた状態だと思えばいい。Googleドライブ上でどこかのフォルダにファイルを移動させたければ、移動先のfolderに対してfolder.addFile(ファイル)を行ったうえで、「マイドライブ」フォルダからremoveFile()すればよい。addFileする前にファイルのgetParents()をしておけば、移動元のフォルダ(「マイドライブ」だろう)を取得できる

日本語だとGASで業務効率化 ~スプレッドシートからGoogleドキュメントを作成する~の説明通りにやればファイルを移動できる。ほかのサイトだと、copyして元のファイルを削除しているケースが見られるが、これだとcopyした意味がない。

[GAS]どこで勤務しているか所在情報を共有する仕組み

小さな組織から大きな組織まで、出欠や勤怠をまとめた一覧表を用意している場合が多い。よくある方式が、大きなホワイトボードに氏名が並んでいて、マグネットで自分がどこにいるのかを示す形である。少人数のうちはホワイトボードでもいいが、100人を超えるような組織で、かつ居場所が散在している場合には、自分の所在を変更するためホワイトボードまで行くだけで相当な手間がある。ホワイトボードから離れた場所にいるものには他人の情報を確認できないし、忘れたまま出かけてしまった場合は残っている人に連絡して情報を書き換えてもらう必要がある。このような不便を解消するため、Google Apps Scriptを使って所在情報を共有する仕組みを開発した。

所在情報をまとめるスプレッドシートを作成し、さらに各ユーザーの利便性のためWEBアプリケーションを経由してスプレッドシートに記録されている自信の情報を読み書きできるようにした。さらに、 外部から所定のメールアドレスへメールを送り、GmailをGASから操作して所所在情報を書き換えられるようにした。 所在情報を社外から書き換えたい場合でも G Suiteに社内ネットワークからしかアクセスできないという制限を踏まえたためである。

スプレッドシートの構成は以下の通り。
所在の情報の確認でも最も短時間で反応する必要がある場面を総務部門の担当者が代表電話で連絡を受けた場合と判断し、氏名をあいうえお順で並べたシートを本体とした。
A列 ID 検索に使う
B列 氏名
C列 読み仮名
D列 本日の所在
E列 本日の所在 付加情報、連絡事項
F列 本日の所在 更新時刻
G列 翌日の所在
H列 翌日の所在 付加情報、連絡事項
I列 翌日の所在 更新時刻
J列 予定の最終有効日

これとは別に、所属部門ごとに氏名を並べて所在を見やすくしたシートを作成した。各人の漢字氏名を検索キーとして、あいうえお順のシートからINDEX関数とMATCH関数を使って情報を反映した。

勤怠程度の情報ならGoogle Apps Script で勤怠フォームを作って出欠確認を自動化するGAS|オフィスの業務を改善・効率化した活用事例を紹介する(1)|非エンジニアのための「Google Apps Script」あたりを参考にした方がいいかもしれない。前者のリンクにも書かれているように、システムを使う場所、組織の都合に適したシステム構築が必要である。

まずはWEBアプリケーションに設定するスクリプトを以下に示す。検索のキーとなるidは、全角数字で入力されても半角数字に変換されて処理される。利用者のリテラシーが高くない場合、全角数字と半角数字の区別がつかない可能性があるためだ。ユーザー数が増えれば、その分だけリテラシーの高くないユーザーを含む可能性が高まる。

GASのHtmlServiceでhtmlファイル側に変数を渡したいときは、13行目のようにする。

//参考ページhttps://www.indetail.co.jp/blog/181219/
//https://dackdive.hateblo.jp/entry/2015/02/01/010540
function doGet(e) {
  // 表示したいHTMLのファイル名を指定(拡張子は記載しない)
  var id = e.parameter.id;
  
  if(!id){
  //idがない場合はid入力欄を表示
  var html = HtmlService.createTemplateFromFile('inputid');
  }else{
  //idがある場合は、現在の記入内容と情報記入欄を表示
  var html = HtmlService.createTemplateFromFile('index');
  html.id = toNumber(id);
  }
  
  //htmlを出力
  return html.evaluate();
}

function doPost(e) {
  //POSTで渡された値を取得
  var id = toNumber(e.parameter.id);
  var target = e.parameter.target;
  
  // SSIDからスプレッドシートの取得
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  
  //id対象者の行数を取得
  var row = rowInterest(id, 'ID');
  //id対象者の情報を取得
  var info = getData(id);
 
  //本日の情報を処理
  if(target === 'today'){
    info.placeToday = e.parameter.whereIs;
    info.commentToday = e.parameter.comment;
    info.dateTodayChange = new Date();
    
    //書き込み配列を用意
    var writeArray = [[info.placeToday,info.commentToday,info.dateTodayChange]]
    var column = 4;
  }
  
  //翌日の情報を処理
  if(target === 'NextDay'){
    info.placeNextDay = e.parameter.whereIs;
    info.commentNextDay = e.parameter.comment;
    info.dateNextDayChange = new Date();
    var dateString = e.parameter.validDate;
    dateString += "T23:59:59+0900";
    info.validDate = new Date(dateString);
    
    //予定の取り消し
    if(info.placeNextDay === 'delete'){
      info.placeNextDay = '';
      info.commentNextDay = '';
      info.validDate = '';
    }
    //書き込み配列を用意
    var writeArray = [[info.placeNextDay,info.commentNextDay,info.dateNextDayChange, info.validDate]]
    var column = 7;
  }
  //書き込み
  sheet.getRange(row, column,1,writeArray[0].length).setValues(writeArray);
  SpreadsheetApp.flush()
  
  // スプレッドシートのデータ挿入後、元の画面に戻す
  var html = HtmlService.createTemplateFromFile('index');
  html.id = id;
  html.info = info;
  return html.evaluate();
}

function getSheet(name){
  // SSIDからスプレッドシートの取得
  var ssId = 'ここにファイルIDを記入する';
  var ss = SpreadsheetApp.openById(ssId);

  // 指定されたシート名からシートを取得して返却
  var sheet = ss.getSheetByName(name);
  return sheet;
}

function getData(id) {
  // 指定したシートからデータを取得
  var data = getSheet('あいうえお順').getDataRange().getValues();
  
  // 該当ユーザーの情報を取得
  var row = rowInterest(toNumber(id), 'ID') ;
  
  var info = {
  ID:data[row -1][0],
  fullname:data[row -1][1],
  fullnameKana:data[row -1][2],
  placeToday:data[row -1][3],
  commentToday:data[row -1][4],
  dateTodayChange:data[row -1][5],
  placeNextDay:data[row -1][6],
  commentNextDay:data[row -1][7],
  dateNextDayChange:data[row -1][8],
  validDate:data[row -1][9]
  };
  info.hasToday = info.placeToday !== '';
  info.hasNextDay = info.placeNextDay !== '';
  
  return info;
}

function rowInterest(keyValue, keyType){
  // SSIDからスプレッドシートの取得
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var data =sheet.getDataRange().getValues();

  var rowInterest = 0;
  var columnInterest = 0;
  if(keyType ==='ID'){
    columnInterest = 0;
  }
  var data =sheet.getDataRange().getValues();

  for(var i=1;i<data.length;i++){
    if(keyValue === data[i][columnInterest]){
      rowInterest = i;
    }
  }
  
  Logger.log(rowInterest);
  return rowInterest + 1;
}

//毎日AM0時から人が動き出すまでの時間に実行する。
//本日の所在を消去し、値を空白にする。
//翌日の所在が記入されていれば、その内容を本日の所在に転記する。
//翌日の所在の最終有効日を当日の日付と比較し、最終日であれば翌日の所在を消去する。

function monitoringDaily(){
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var data =sheet.getDataRange().getValues();
  
  //現在時刻
  var date = new Date();
  
  //本日の所在を消去し、値を空白にする。
  var lastrow = data.length;
  sheet.getRange(4,4,lastrow-3,3).clearContent();
  
  //翌日の所在が記入されていれば、その内容を本日の所在に転記する。
  for(var i=3;i<data.length;i++){
    var info = {
      ID:data[i][0],
      fullname:data[i][1],
      fullnameKana:data[i][2],
      placeToday:data[i][3],
      commentToday:data[i][4],
      dateTodayChange:data[i][5],
      placeNextDay:data[i][6],
      commentNextDay:data[i][7],
      dateNextDayChange:data[i][8],
      validDate:data[i][9]
    };
    info.hasToday = info.placeToday !== '';
    info.hasNextDay = info.placeNextDay !== '';
    
    if(info.hasNextDay){
      //書き込み配列を用意
      var writeArray = [[info.placeNextDay,info.commentNextDay,info.dateNextDayChange]]
      var column = 4;
      sheet.getRange(i + 1, column,1,writeArray[0].length).setValues(writeArray);
    }
    //翌日の所在の最終有効日を当日の日付と比較し、最終日であれば予定を消去する。
    Logger.log(info.validDate < date);
    if(info.validDate < date){
      var column = 7;
      sheet.getRange(i + 1, column,1,4).clearContent();
    }
  }
}

function toNumber(str){
  str = String(str);
  var numVal = str.replace(/[0-9]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 65248);
  });
  return Number(numVal);
}

function getNextDay(){
  var date = new Date();
  date.setDate(date.getDate() + 1);
  var dateNextDay = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
  return dateNextDay;
}

次に、WEBアプリケーションで使用するHTMLのテンプレートを示す。idを未指定の場合はidを入力するページを、idが指定されていれば現在の情報の確認と新しい情報の入力ページを表示する。
formで与える選択肢の値は適切に変更する。日付を入力させる欄は、予め翌日の日付が入るようにした。

//ID入力用
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>IDを入力してください</h2>
    <form method="get" action="ここにWEBアプリケーションのURLを記入する">
      <p>ID: <input type="text" name="id" value="" size="20" /></p>
      <input type="submit" value="送信する">
    
  </form>
  </body>
</html>
//IDをキーとして情報の確認と書き換えをする
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      h2 {background-color:palegreen;}
      table{
        border: solid 1px #000000;
        border-collapse: collapse;
      }
      th {border: solid 1px #000000}
      td {border: solid 1px #000000}
    </style>
  </head>
  <body>
    <table>
      <?
        // スプレッドシートからデータを取得
        // 該当ユーザーの情報を取得
        var info = getData(id);
        
        // 本日の予定の見出し作成
        output.append('<tr>');
        output.append('<th>氏名</th>');
        output.append('<th>本日の所在</th>');
        output.append('<th>本日の所在 連絡事項</th>');
        output.append('<th>本日の所在 更新時刻</th>');
        output.append('</tr>');
        //本日情報に値があれば表示
        if(info.hasToday){
          output.append('<tr>');
          output.append('<th>' + info.fullname + '</th>');
          output.append('<th>' + info.placeToday + '</th>');
          output.append('<th>' + info.commentToday + '</th>');
          output.append('<th>' + Utilities.formatDate(info.dateTodayChange, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('</tr>');
        }else{
          output.append('<tr>');
          output.append('<th>' + info.fullname + '</th>');
          output.append('<th>未入力</th>');
          output.append('<th></th>');
          output.append('<th></th>');
          output.append('</tr>');
        }

        // 翌日の予定の見出し作成
        output.append('<tr>');
        output.append('<th>予定の有効期限</th>');
        output.append('<th>翌日の所在</th>');
        output.append('<th>翌日の所在 連絡事項</th>');
        output.append('<th>翌日の所在 更新時刻</th>');
        output.append('</tr>');
        //本日情報に値があれば表示
        if(info.hasNextDay){
          output.append('<tr>');
          output.append('<th>' + Utilities.formatDate(info.validDate, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('<th>' + info.placeNextDay + '</th>');
          output.append('<th>' + info.commentNextDay + '</th>');
          output.append('<th>' + Utilities.formatDate(info.dateNextDayChange, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('</tr>');
        }else{
          output.append('<tr>');
          output.append('<th></th>');
          output.append('<th>未入力</th>');
          output.append('<th></th>');
          output.append('<th></th>');
          output.append('</tr>');
        }
      ?>
     </table>
   
    <h3>所在情報を更新するには、以下のフォームに記入して、更新ボタンを押してください。</h3>
    <h2>本日の所在情報を登録する</h2>
    
    <form method="post" action="ここにWEBアプリケーションのURLを記入する。末尾はexec?=<?= id ?>">
      <input type="hidden" name="target" value= "today">
      <input type="hidden" name="id" value= "<?= id ?>">
      所在:<label><input type="radio" name="whereIs" value="事業所1">事業所1</label>
      <label><input type="radio" name="whereIs" value="事業所2">事業所2</label>
      <label><input type="radio" name="whereIs" value="事業所3">事業所3</label>
      <label><input type="radio" name="whereIs" value="事業所4">事業所4</label>
      <label><input type="radio" name="whereIs" value="事業所5">事業所5</label><br><br>
	
      <label><input type="radio" name="whereIs" value="退勤" required>退勤</label>
      <label><input type="radio" name="whereIs" value="出張">出張</label>
      <label><input type="radio" name="whereIs" value="休暇">休暇</label>
      <label><input type="radio" name="whereIs" value="未出社">未出社(詳細は下の連絡事項欄に記入する)</label><br><br>
	
      <label>連絡事項: <input type="text" name="comment"></label><br><br>

     <input type="submit" value="本日の所在を更新する">
     </form>
     
  <h2>翌日の所在情報を登録する</h2>
    <form method="post" action="ここにWEBアプリケーションのURLを記入する。末尾はexec?=<?= id ?>">
    <input type="hidden" name="target" value= "NextDay">
    <input type="hidden" name="id" value= "<?= id ?>">
    <label><input type="radio" name="whereIs" value="出張">出張</label>
    <label><input type="radio" name="whereIs" value="休暇" required>休暇</label>
    <label><input type="radio" name="whereIs" value="時差出勤">時差出勤</label>
    <label><input type="radio" name="whereIs" value="delete">予定の取り消し</label><br><br>
    
    <label>連絡事項: <input type="text" name="comment"></label><br><br>
    
    予定の最終有効日: <input type="date" name="validDate" value="<?= getNextDay() ?>"><br><br>
    <input type="submit" value="翌日以降の所在を更新する">
    </form>

  </body>
</html>

メールを読み書きして所在情報を書き換えるスクリプトを以下に示す。社外から所在情報を書き換えたいケースとして、突然の休暇、公共交通機関の事故など出社が遅れる、あるいは退勤処理をし忘れて退勤したという3通りを想定した。1分、5分など、一定時間ごとに発火するトリガーをかけておく。

function getMail(){
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var start = 0;
  var max = 500;
  var threads = GmailApp.search('label:shozai is:unread',start,max);

  for(var n in threads){
    var thread = threads[n];
    
    var msgs = thread.getMessages();
    for(m in msgs){
      var msg = msgs[m];
      var id = msg.getSubject();
      var body = msg.getBody();
      var date = msg.getDate();
      
      
      //所在情報に反映
      id = toNumber(id);
      var row = rowInterest(id, 'ID');
      //メール本文に「休暇」があれば休暇、「退勤」があれば退勤、それ以外は未出社に設定する
      var whereIs = '未出社';
      if ( body.match(/休暇/)) {
        whereIs = '休暇';
      }
      if ( body.match(/退勤/)) {
        whereIs = '退勤';
      }
      //書き込み配列を用意
      var writeArray = [[whereIs,body,date]]
      var column = 4;
      sheet.getRange(row, column,1,writeArray[0].length).setValues(writeArray);

      //メールを既読にする
      thread.markRead();
    }
  }
}

[GAS] フォームの送信時トリガーの多重実行問題を防止する

スプレッドシートをフォームとリンクして、フォームからの書き込みがあったときにスクリプトを動作させるトリガーが「フォームの送信時」トリガーである。これにバグがあるようで、フォームに回答が1回しかないのに、トリガーをかけているスクリプトが複数回実行されることがある。あとから実行されたスクリプトではeに値が入っていないので、ファイルへの書き込むなどに支障がでる。

代表的な回避策としては、スクリプトをスプレッドシートではなくフォームの方に記述し、スクリプトを介してスプレッドシートへの値書き込みを行うというものがある。ほかには、スプレッドシートのスクリプトでsleepを数秒かけたり、eの値が入っていないときに処理を止める条件分岐を付ける手法がある。

フォームのスクリプトの基本的な作り方はGASのonFormSubmit()でハマったことを参照。あとはreferenceでも読んで試してみる。スプレッドシートに書き込むには、appendRowすればいい。フォームへの回答の中身は以下で確認できる。ファイルのアップロードは、Drive上のファイルIDが回答になる。

function onFormSubmit(e){
  var mail = e.response.getRespondentEmail();
  Logger.log(mail);
  var date = e.response.getTimestamp();
  Logger.log(date);
  
  var itemResponses = e.response.getItemResponses();

  for (var i = 0; i < itemResponses.length; i++) {
    var itemResponse = itemResponses[i];
    var title = itemResponse.getItem().getTitle();
    var value = itemResponse.getResponse();
    Logger.log('["%s"] Title "%s", Value "%s"', i, title, value);
  }
}

便利そうなやつ
Googleフォームのプルダウンにスプレッドシートの内容を反映させる

[GAS] GASを使ってもらうようにする導入資料

GASを知らない同僚にGASを使う価値を理解してもらい、学習を始めるきっかけを与えるために有用な資料を集める。

GASの価値を理解してもらう

Google Apps Script (GAS) で毎週 30 分の雑務を自動化した話
毎月 500 時間の社内ルーティンワークを GAS を使って自動化した話
ともにグロービスのYuki Tanabe氏が社内業務にGASを活用したケースを紹介している。ありがちな事務作業が対象で、自職場での活用イメージがわきやすい。

GASの学習を始める最初の一歩

非プログラマの事務員にGASをいきなり書かせるのは難しい。まずはマクロやちょっとした関数として使ってもらう。Google Apps Script ハンズオン資料にあるような具体的なケースを与えて、一緒にプログラムを作る経験を積ませる。慣れてきたらGAS ビギナーが GAS を使いこなすために知るべきこと 10 選で紹介されているような一段階高度な内容を教えるとともに、自分だけでプログラムを描けるように励ます。

【保存版】初心者向け実務で使えるGoogle Apps Script完全マニュアル
実例が非常に豊富

G Suite Developer Apps Script リファレンス
Googleの公式リファレンス。Document, Spreadsheet, Formなど、それぞれのアプリケーションでどのようなスクリプトを書けるのかを調べられる。辞書的に使う。

マンガで分かる JavaScriptプログラミング講座
プログラミング初学者にJavaScriptでプログラムをつくる方法や概念を伝えるのに便利なサイト。Google Apps ScriptはJavaScript 1.6を基に作られているため、プログラムをどのように書くか、どのようなデータ処理が可能なのかはJavaScriptを学ぶ必要がある。書籍版もある。

JavaScript リファレンス
JavaScriptの日本語リファレンス。条件分岐や抽出といったデータ処理はこちらを参照する。GAS独自の機能以外をカバーする。
JavaScriptは広く使われている言語なので、ググれば何かしらヒントが得られるはず。

GAS使わなくても便利な機能があることも同時に教える

非プログラマには、プログラムを書くのは魔法を覚えるようなもの。はじめは何でもできるような気がするが、仕事では投入する時間やリソースと利益を秤にかける必要がある。車輪の再発明をしないためにも、既存の機能を学ぶことも推奨する。

たとえば、Google ドキュメントの音声認識機能(ツール>音声入力)。音声からかなり正確に文章を生成してくれる。まさに魔法のようだ。音声入力はキーボード操作が得意でない層にアピールするだろう。音声入力以外に翻訳機能もある。

[GAS] Google Apps Scriptでハッシュ化する関数

GASで送信したメールのリンクを踏むと受領確認できる仕組みにはURLクエリパラメータを使えばいい。しかし、ユーザーIDのような意味のある値をあからさまにURLに付加するのはやりたくない。そんなときは受け渡すパラメータの値をハッシュ化すればいい。

Google Apps ScriptではcomputeDigest関数でアルゴリズムを指定して簡単にハッシュ値を取得できる。使用可能なアルゴリズムは、MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512, (2019/11/1現在)。

たとえば、スプレッドシートの何行目の値を編集するかをクエリで指示する際に、行数を指定する情報とユーザーIDを結合した文字列をハッシュ化したものをスプレッドシートの同じ行に書き込んでおく。このハッシュ値で検索すれば、あからさまにユーザーIDや行を特定する情報をパラメータに含まなくても、該当する行を選択することができる。

参考
スプレッドシートで覚えるブロックチェーン |「もしかして渡した値」「入れ替わってる!?」

[GAS]Googleフォームの解答欄にあらかじめ答えを入れておく

アンケート調査や申し込み手続きで回答を入力してもらうのに、Googleフォームは非常に有用だ。スプレッドシートに回答を転写してくれるので、入力された値を集計したり、入力値をもとに何かスクリプトを走らせることもできる。スプレッドシートのGASには、わざわざフォームから送信されたとき用のトリガが用意されている。

フォームを利用者の入力インターフェースにして、スプレッドシートのデータを使って条件分岐したいときに、予め一部の回答が入力された状態のフォームに利用者を誘導したい。

実はけっこう簡単にできる。フォームの回答画面はURLの最後がviewformになっているので、この後ろに&で
解答欄のID=予め入力したい回答
をつけてやればよい。
解答欄のIDを確認する簡単な方法は、Chromeでフォームの回答画面を表示(プレビューでいい)して、解答欄のIDを確認したいテキストボックスなどを右クリックする。次いで検証を押すと画面右側にカラムが表示される。ハイライトされている部分からnameを探す。一般のテキストボックスだとname=”entry.数値”となっている。この “entry.数値” が解答欄のIDである。

(フォームのURL)/viewform&entry.1234=hoge
というアドレスにアクセスすると、解答欄のIDが entry.1234 である場所に予めhogeと書かれた状態で始まる。
複数の解答欄に予め回答を入れておくには&でつないでいけばいい。

参考
GOOGLEフォームの初期値

[GAS] GASでセルを結合した表をドキュメントに挿入する

Google Apps Scriptはスプレッドシートだけでなく、ドライブ、ドキュメントなどほかのG Suiteでも使える。スプレッドシートにまとめた情報をもとに、ドキュメントで報告書を自動生成することも可能だ。

これまで手作業で作成していた報告書をGASで作成するにあたって、2つ手の込んだことを求められた。ひとつには、セルを結合して少々複雑な形をした表をつくることだった。ふたつには、単なるテキストではなく、上付きや下付き、斜体といった書式設定を伴う形で表に文書を書き込むことだった。

ひとつ目の「セルを結合した表」は少々厄介であった。GASのリファレンスを読んでみても、セルの結合に相当するMethodがないのだ。試しにドキュメントでセルの結合をした表を作ってGASで読み込んでみると、セルを結合してできたセルは、TableCell.getColSpan()の値が1より大きい値になっていた。しかし、setColSpan()というMethodは存在しない。merge()も期待したような動作をしないドキュメントではなくHTMLファイルとして出力することはできそうだが、いまはドキュメントを書き換えたいので不適当だ。
結局のところ、あるドキュメントファイルにあらかじめセルを結合した表を用意しておいて、新しいドキュメントにコピーするという手法をとることにした。TableRowのレベルで挿入していけば、下の図のような2行を単位とした繰り返し構造の表がつくれる。

GASでセル結合した表を挿入する例

ふたつ目の書式を維持した文書を挿入するのは簡単である。上で挿入した表のセルに対して、転写したい文書のParagraphをappendしてやればよい。

次に示す例では、元になるドキュメントファイルの1行目 (getCHild(0) またはpp[0] )から5行目(getCHild(4)またはpp[4])に書式をいじった文書が入っている。これを段落ごとに読みだしている。また、7行目( getCHild(6))にはテンプレートにしたい表がある。
appendParagraphする文書をどこのドキュメントファイルに置いておくか、スプレッドシートにでもまとめておけば、表の繰り返し単位ごとに適切なドキュメントファイルを読みだして、表の各セルを埋めることができる。
appedTableやappendParagraphをする際には、copy()を付けてやる必要がある。また、 appendParagraphをする際には、 .merge()をつけないと、余分な改行がセル内に残る。

function tryTable(){
  var doc_from = DocumentApp.openById('元になるドキュメントファイルのID'),
  doc_to = DocumentApp.openById('書き込み先になるドキュメントファイルのID'),
  pp = doc_from.getBody().getParagraphs();

  var body = doc_to.getBody();
  //表をつくる
  var table0 = body.appendTable(doc_from.getBody().getChild(6).copy());
  table0.getRow(0).getCell(0).clear().appendParagraph(pp[0].copy()).merge();
  table0.getRow(0).getCell(1).clear().appendParagraph(pp[1].copy()).merge();
  table0.getRow(0).getCell(2).clear().appendParagraph(pp[2].copy()).merge();
  table0.getRow(0).getCell(3).clear().appendParagraph(pp[3].copy()).merge();
  table0.getRow(1).getCell(0).clear().appendParagraph(pp[4].copy()).merge();

  //表のテンプレートを下に伸ばす。例として10回繰り返し。
  for (var i = 0; i < 10; i++) {
    table0.appendTableRow(table0.getRow(0).copy());  
    table0.appendTableRow(table0.getRow(1).copy());
  }
}

参考
【Microsoft Office 英語】日本語/英語の対照表(Excel, Word)


さらに複雑な表を扱う方法を考える。異なるRowのセルを結合したセルではどのようになるだろうか?次の図に2行5列の表のセルを結合した表と、各セルにアクセスする際に使うRowやCellの番地を示した。複雑な形状に結合した表であっても、結合したセルの左上に位置するセルの値が表示されると見なせるようだ。

行(Row)が異なるセルを結合した表の例

自分で作成した表の各セルにアクセスする番地を確認したければ、getChild(index)で各セルの値を読み出すのが簡単だろう。BODY>TABLE>TABLE ROW>TABLE CELL>PARAGRAPH>TEXTという順序で各オブジェクトがぶら下がっている。TABLE CELLまで行けばgetText()でセルの中のテキストを表示できる。また、各要素にぶら下がっている要素の数はgetNumChildren()で取得できるので、あとはforループで読みだせばよい。

ところで、セルを結合すると表示されないセルが出てくるが、ファイルから消えているわけではないようだ。BODYに対してgetParagraphs() をすると、上の図で表示されていないTABLE CELL (R1C0やR1C2~4)に対応するはずのParagraphも取得される。
BODYに直接ぶら下がっている要素だけを取り出したいときは、BODYに対して getChild() とgetNumChildren()を使うのがよさそうだ。

[GAS] GASで送信したメールのリンクを踏むと受領確認できる仕組み

Google Apps Script (GAS)を使って、文書ファイルを指定のメールアドレスに送信する。文書ファイルの内容を確認した旨を自動で取得するため、送信されるメールにはGASへのリンクを張っておき、ワンクリックで受領確認の記録が残るようにする。このとき、文書ファイルのファイルID、受信者のアドレス、受領確認日時が記録されるようにする。

STEP1 承認用のリンクを踏んだ時に動くdoGET関数を定義

承認用のリンクをつくるには、まずdoGET関数を含むプロジェクトを定義し、ウェブアプリケーションとして導入する必要がある。ウェブアプリケーションとして導入するには、doGET関数を定義したのちに、スクリプトエディタのメニュー「公開」から「 ウェブアプリケーションとして導入 」を選択する。作業が完了すると、ウェブアプリケーションにアクセスするためのURLが発行される。

下にスクリプトの例を示す。
URLの最後尾にパラメータとして、スプレッドシートの何行目に承認済みの記録を記入するか (8行目 row)と承認者名 (9行目 SV)を入れるようにする。
12行目はスプレッドシートへの書き込みの例である。8行目で求めたrow行のA列からC列に対して、”recipt”という文字列, rowの中身, supervisor中身を記入する。冒頭で述べた、文書ファイルのファイルID、受信者のアドレス、受領確認日時あたりはここを書き換えればいい。
16,17行目で送信するメール本文を作成する。16行目はプレーンテキストの場合、17行目はHTMLテキストの場合である。受信者の環境によって、いずれかが表示される。
19行目でメールを送信する。送信者は、このスクリプトを実行するアカウントである。宛先はrecipient、メールタイトルはsubject、本文は前述のとおりである。
21行目以降は、ウェブアプリケーションとして導入したURLにアクセスした人に表示される画面を示している。
※「ウェブアプリケーションとして導入」したあとにdoGETの中身を変更した場合は、再度 「ウェブアプリケーションとして導入」 を行う。このときProject VersionをNewにしてから「更新」すること。

//ウェブアプリケーションとして導入する
function doGet(e) {
  var recipient = "受信者のメールアドレス",
      SS_id = "情報を記録するスプレッドシートのファイルID",
      SheetName = "情報を記録するスプレッドシートのシート名",
      Sheet = SpreadsheetApp.openById(SS_id).getSheetByName(SheetName),
      data = Sheet.getDataRange().getValues(),
      row = e.parameter.row,
      supervisor = e.parameter.SV;

  //ファイルに書き込み
  Sheet.getRange(row,1,1,3).setValues([["recipt",row, supervisor]]);

  var subject = '回覧通知';
  
  var plaintxt = "上長の承認が得られたので、報告書を回覧します。";
  var html = '<h1>報告書の回覧通知</h1>'+ '<p>上長の承認が得られたので、報告書を回覧します。</p>';
    
  GmailApp.sendEmail(recipient, subject, plaintxt, { htmlBody: html });

  var display = '<p>報告書の承認を受けつけました。N氏に回覧します。ご協力ありがとうございました。</p>';
  return HtmlService.createHtmlOutput(display);
}

STEP2 承認リンクのURLにパラメータを付加してメールで送信する

STEP1で作成したウェブアプリケーションのURLに、スプレッドシートの情報を読みだしてパラメータを付加させる。

下にスクリプトの例を示す。
7行目のsupervisorを承認依頼メールの受信者にする。data[0][2]すなわちシートのC1セルに入っている値を読みだしている。8,9行目も同様にスプレッドシートのB1セル、A1セルの値を読み込んでいる。
11,12行目で、ウェブアプリケーションにアクセスするURLに、パラメータとしてrowとSVを付加している。パラメータSVならdoGET関数でe.parameter.SVとして受け渡される。複数のパラメータは&で結ぶ。※下のスクリプトでは&amp;となっているが、amp;は除く。
14,15行目はメールに本文を生成している。
17行目は添付したいファイルを呼び出している。ファイルはあらかじめGoogle Driveに入れておく。ファイルIDの調べ方
あとはメールでsupervisorに送る。

//承認依頼のメールを送信する
function myFunction() {
  var SS_id = "情報が記録されているスプレッドシートのファイルID",
      SheetName = "情報が記録されているスプレッドシートのシート名",
      Sheet = SpreadsheetApp.openById(SS_id).getSheetByName(SheetName),
      data = Sheet.getDataRange().getValues(),
      supervisor = data[0][2],
      subject = data[0][1],
      row = data[0][0];
      
  var url = "ウェブアプリケーションのURL(execで終わる)";
      url += '?row=' + row + '&amp;SV=' + supervisor;

  var txt = '承認リンク ' + url;
  var html = '<h1>次のリンクを開いてください</h1>' + '<a href="' + url + '">承認</a>';
 
  var file = DriveApp.getFileById("Google Drive上のファイルのID")
    
    //承認依頼メールを送信する
    GmailApp.sendEmail(supervisor, subject, txt, { htmlBody: html, attachments: file});
}

参考
「Google Apps Script」で社内システムをつくってみた~GASとの出会い編~
GASのワークフローでURLのパラメータで承認・否認を切り替える方法

企業の研究開発における技術伝承のありかた

想定読者 製造業の研究開発に携わる研究者

日本の製造業がかつて世界を席巻したというのは本当の話だろうか。そうだとしたら、日本の製造業の研究開発のレベルはかつてより低下してしまったのではないか。

民間企業の研究所は競合に打ち勝ち利益を上げられるような優れた新商品、新技術を生み出すための専門家集団だ。専門分野ごとに様々な研究者がいて、広範な領域の高度な技術を統合してひとつの素晴らしい商品を生み出す。研究者たちには日々の実験作業を支える助手がいて、研究者は技術の高度化と課題解決に集中する。助手たちは実験作業の手技レベルを向上させることで職人技ともいうべき繊細な実験を行い、新発見の端緒をひらく。そんな研究所を維持できている企業が、いまどれだけあるだろうか。

平成の30年間に人が減った。助手たちは事務系の一般職と同じように派遣労働者に転換された。わずか数年で職場を去る派遣労働者に技術を教えて、信頼できるまでに育ったころにはさようなら。これで職場から職人技が消えた。いわずもがな研究者も減らされた。論文ほど詳細に手順が記されているわけではない社内報告書を読んだところで失われた技術を復元できるわけではない。人がいないのだから、かつては助手がやっくれていた作業も研究者がやるようになった。グローバルな競争のおかげで商品開発の現場に対する要求が弱くなることはないというのに、研究者が商品開発に集中できる環境は減らされている。

日本の人口が減り続けるのだから、かつてのように人的資源が豊富に手に入ることは今後ない。そうであるならば、効率的に技術が伝承され、知見が共有されるシステムを構築して、現役の研究者たちが浪費している時間と費用を削減するほかない。働き方改革をお題目では済まされない。


これまでの技術伝承や知見の共有が効率的でなかったのは、形式知化されていなかった点が大きい。何かあれば詳しい研究者本人に聞きに行けばいい。報告書は成果を上層部にアピールすために書く。詳細な部分は書いてもアピールにならない。自分の暗黙知を文章に起こす時間があれば、その時間を次の商品開発に使った方が効率的だ。人ひとりが持てる知識や技術の量が有限だったとしても、人がたくさんいれば解決する。このような状況で人減らしを行えば、職場を去った人の数だけ技術が失われるのも当然だ。

先人たちの考え方にも一理あるが、暗黙知を形式知に転換するのに要する労力が馬鹿にならない。伝える相手のレベルがわからなければ、どこまで書きおこせばいいいのかわからない。大学で使うような教科書をつくれというのだろか。そんなことはできない。
ここで私が提案したいのは、かつて自分がいまの職場に来たときを思い出し、新人の自分に対して教えるように書くことだ。そこで暗黙の了解とできることであれば、他者が新人の立場でも同じような事柄をすでに身につけているとみなせるだろう。さらに、自分の次の新人が共有できない前提があったとしも自分はそれを説明できるはずだ。そうやって新しい人が来るたびに情報を追加していけば、たいていの人に伝わる内容になるだろう。

書く内容のレベルが決まったとしても、書くべき内容は膨大にあるように思われる。ここで気持ちを萎えさせずに思い出してほしい。自分ですべて書く必要はないことを。他者の文章を剽窃しろというわけではなく、他者の文章を読んで済むなら引用するなり参照するなりすればいい。学部生が勉強するような内容は教科書を読めばいい。ただ、どの教科書を読めばいいのか、何という名前の概念を理解すればいいのかは書いておこう。学ぶ必要はあるが、迷う必要はない。

忙しい中でまとめた資料が使われずに忘れらていくとしたら、そもそもそんな資料を作るべきでなはない。普段の業務を効率化するチートシートや、自習用の資料のような立ち位置で同僚に利用してもらえるような内容を盛り込んでいこう。

最も重要な点は、利用者に資料を改善させられるようにしておくことだ。利用者が不足していると感じた情報をすぐに追加できるようにしておく。内容に誤りがあれば修正できるようにしておく。ここまでくれば、もはや個人のみならずチーム全体の知恵袋として価値を発揮しはじめる。

これが私の考える「暗黙知を形式知へ転換する」ということ。Wikipediaの社内版というイメージがわかりやすいだろう。

windowsでpythonを使いたい

Pythonのインストール

ライブラリのインストール pandasとか

開発環境の整備atomのインストールと パッケージの導入

グラフなど画像や計算結果を表示しんがら実行したいときはjupyterが便利。.ipynb形式を読んだり。atomやVS Codeなどのエディタでもプライグインで対応可能。
Atom と Hydrogen で Jupyter が Atom で動くよ!(インストール、準備、使い方まで)

2次元相関分光の例2Dshige