tag:crieit.net,2005:https://crieit.net/tags/GAS/feed 「GAS」の記事 - Crieit Crieitでタグ「GAS」に投稿された最近の記事 2023-12-17T10:55:37+09:00 https://crieit.net/tags/GAS/feed tag:crieit.net,2005:PublicArticle/18679 2023-12-17T10:50:09+09:00 2023-12-17T10:55:37+09:00 https://crieit.net/posts/avoid-processing-limit-on-gas 【GAS】処理時間の制限を回避する <p>こんにちは、しきゆらです。<br /> 急な出社が続いてあまり記事を書けていない今日この頃です。<br /> 今回は、以前投稿したGASでファイル一覧を取得するコードを載せましたが、実際の環境で処理回したら処理時間の制限に引っかかったので、回避するように直したのでその旨をメモしておきます。<br /> 前回の記事はこちら。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2023/12/avoid_processing_limit_on_gas/">【GAS】Googleドライブにあるフォルダ・ファイルの一覧を取得する | しきゆらの備忘録 https://shikiyura.com/2023/11/how_to_get_file_list_in_google_drive/</a></p> <p>改めて、GASの制限については以下を参照いただければと思います。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/guides/services/quotas?hl=ja#current_limitations">Google サービスの割り当て  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/guides/services/quotas?hl=ja#current_limitations</a></p> <p>上記を見ると、スクリプトの制限としては1回の処理で6分までとなっています。 今回は、ここを回避していきます。</p> <h2 id="回避の方針"><a href="#%E5%9B%9E%E9%81%BF%E3%81%AE%E6%96%B9%E9%87%9D">回避の方針</a></h2> <p>処理時間の制限としては、処理時間が一定時間を超えたらそこでいったん処理を止める、というだけ。<br /> ただし、それだけだと処理しきれなかった部分が出てくるので、そこもケアしてあげましょう。</p> <p>ざっくり手順としては以下の通り。</p> <ul> <li>定期的に処理時間の確認し、一定時間を超えていたら処理を止める</li> <li>処理途中のデータを保存する</li> <li>処理を続きから実行するためのトリガーを定義する</li> <li>一時停止した場合に前回の処理から再開する</li> </ul> <p>それぞれ見ていきましょう。</p> <p>なお、スクリプトのよって内容が変わると思いますが、ここでの例として前回のファイル・フォルダの一覧を取得する、というものに対してコードを書いていきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2023/11/how_to_get_file_list_in_google_drive/">【GAS】Googleドライブにあるフォルダ・ファイルの一覧を取得する | しきゆらの備忘録 https://shikiyura.com/2023/11/how_to_get_file_list_in_google_drive/</a></p> <p>ファイル・フォルダ一覧を取得するコードはこんな感じ。</p> <pre><code class="javascript">function allFiles(){ const folderId= "xxxxxxxxxx"; const targetDir = DriveApp.getFolderById(folderId); const files = []; // ファイルオブジェクトを保持する配列 const subFolders = [targetDir.getFolders()]; for(let i = 0; i < subFolders.length; i++) { const subFolder = subFolders[i]; while(subFolder.hasNext()) { const folder = subFolder.next(); subFolders.push(folder.getFolders()); folders.push(...allFilesBy(folder)); } } return files; } function allFilesBy(folder) { const files = []; // 対象となるフォルダにあるファイルオブジェクトを保持する配列 const fileIterator = folder.getFiles(); while(fileIterator.hasNext()) { files.push(fileIterator.next()); } return files; } </code></pre> <h2 id="処理時間を回避するコード"><a href="#%E5%87%A6%E7%90%86%E6%99%82%E9%96%93%E3%82%92%E5%9B%9E%E9%81%BF%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89">処理時間を回避するコード</a></h2> <h3 id="処理時間の確認"><a href="#%E5%87%A6%E7%90%86%E6%99%82%E9%96%93%E3%81%AE%E7%A2%BA%E8%AA%8D">処理時間の確認</a></h3> <p>処理時間を取得する機能はなさそうなので、単純に実行タイミングで<code>Date</code>オブジェクトを生成しておき、定期的に現在時刻との差分を求める形で簡易的に確認します。</p> <p>// 処理の開始時点の日付・時刻を取得する</p> <pre><code class="javascript">const startDate = new Date(); ... </code></pre> <p>後は、定期的に処理の中で<code>startDate</code>と現在時刻との差分を求めて処理時間を計算しましょう。</p> <p><code>Date</code>オブジェクトの差分だったり、<code>Date.getTime()</code>を使っても同じように取得できます。 なお、値はミリ秒なので注意が必要です。</p> <pre><code class="javascript">... // ミリ秒で計測する場合 const processingMilliSec = (new Date() - startDate); // 秒に直す場合 const processingSec = (new Date() - startDate) / 1000; ... </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime">Date.prototype.getTime() - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime</a></p> <p>雑に関数にしておきます。 一定時間(ここでは300秒(= 5分)が経過したら処理を止めるようにしています。</p> <pre><code class="javascript">function hoge() { // 処理の開始時点の日付・時刻を取得する const startDate = new Date(); // いろいろな処理 if (limitCheck(startDate)) return ; } function limitCheck(startDate: Date): boolean => { const processingSec = (new Date().getTime() - startDate.getTime()) / 1000; return processingSec >= 300; // 処理時間が300秒より大きい場合はtrueを返す } </code></pre> <p>これにて、一定時間が経過した時点で処理を止める機能は完了です。</p> <h3 id="処理途中のデータを保存する"><a href="#%E5%87%A6%E7%90%86%E9%80%94%E4%B8%AD%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B">処理途中のデータを保存する</a></h3> <p>処理を止めるだけでは、次に実行したときに再開することはできません。 処理を止めたときに、次回実行時に処理途中から再開できるように</p> <p>前述の通り、前回のファイル・フォルダ一覧を取得するコードを例として書いていきます。 前回の記事はこちら。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2023/11/how_to_get_file_list_in_google_drive/">【GAS】Googleドライブにあるフォルダ・ファイルの一覧を取得する | しきゆらの備忘録 https://shikiyura.com/2023/11/how_to_get_file_list_in_google_drive/</a></p> <p>データを保持する先は、以前に紹介したPropertiesServiceを使います。 PropertiesServiceに関しては、過去記事を書いているのでこちらもご覧ください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2021/07/how_to_use_properties_service_at_gas/">【GAS】パスワードやトークンをコードに直書きしない方法 | しきゆらの備忘録 https://shikiyura.com/2021/07/how_to_use_properties_service_at_gas/</a></p> <p>ファイル・フォルダの一覧を取得する場合、保持しなければいけないのは主に以下の3点ですね。</p> <ul> <li>取得したファイル・フォルダのリスト</li> <li>処理途中のファイル・フォルダイテレータ</li> <li>参照予定のフォルダリスト</li> </ul> <p>この3点の中で、最初の「取得したファイル・フォルダのリスト」については、おそらく取得した後で最終的にはスプレッドシートにまとめたりするはずなので、あえて<code>PropertiesService</code>に置いておかなくてもよいかもしれません。</p> <p>一方で、「処理途中のファイル・フォルダイテレータ」や「参照予定のフォルダリスト」は処理途中で終了しなければいけない場合は保持しておかなければ続きから再開ができません。<br /> ということで、この2点を保持する形を作っていきます。</p> <p>イメージ図を置いておきます。<br /> 赤フォルダ配下にあるフォルダを取得するとき、<code>getFolders()</code>で青フォルダたち<code>FolderIterator</code>として取得できます。</p> <p><a href="https://crieit.now.sh/upload_images/b3b529b8f6a5073642d0882136913d91657e55096d865.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b3b529b8f6a5073642d0882136913d91657e55096d865.png?mw=700" alt="image" /></a></p> <p>青フォルダたちを<code>FolderIterator</code>で取得している間に下図の線の位置で時間切れとなった場合、残り2つのフォルダは次回に回さないといけません。<br /> 合わせて、青フォルダ1の配下にある紫フォルダたちを取得する<code>FolderIterator</code>も取得済みなので、こいつらも次回処理するときに見る必要がありますよね。</p> <p><a href="https://crieit.now.sh/upload_images/c6c8e59753fab560266a366bc25aeb18657e55155a660.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c6c8e59753fab560266a366bc25aeb18657e55155a660.png?mw=700" alt="image" /></a></p> <p>ということで上記3点のうち、青フォルダの<code>FolderIterator</code>が「処理途中のファイル・フォルダイテレータ」、紫フォルダの<code>FolderIterator</code>が「参照予定のフォルダリスト」となります。<br /> では、それぞれの保持の仕方を見ていきます。</p> <h3 id="イテレータの保持"><a href="#%E3%82%A4%E3%83%86%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%AE%E4%BF%9D%E6%8C%81">イテレータの保持</a></h3> <p><code>FolderIterator</code>、<code>FileIterator</code>のどちらも<code>getContinuationToken()</code>メソッドが定義されており、イテレータ処理で時間がかかる場合に途中から再開することができるようになっています。<br /> 再開するには、DriveAppクラスに定義されている<code>continueFolderIterator</code>、<code>continueFileIterator</code>にトークンを渡せばよい。</p> <pre><code class="javascript">// トークンの取得 const iteratorToken = fileIterator.getContinuationToken(); // イテレータの再開 const resumeIterator = DriveApp.continueFileIterator(iteratorToken); </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/folder-iterator?hl=ja#getContinuationToken%28%29">Class FolderIterator  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/folder-iterator?hl=ja#getContinuationToken()</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/file-iterator?hl=ja#getContinuationToken%28%29">Class FileIterator  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/file-iterator?hl=ja#getContinuationToken()</a></p> <p>このトークンをPropertiesServiceなどで保持しておけば再開できますね。</p> <h3 id="参照予定のフォルダリストの保持"><a href="#%E5%8F%82%E7%85%A7%E4%BA%88%E5%AE%9A%E3%81%AE%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E4%BF%9D%E6%8C%81">参照予定のフォルダリストの保持</a></h3> <p>前述の通り、フォルダやファイル一覧についてはPropertiesServiceを使って保持します。 なお、参照予定のフォルダ一覧はFolderIteratorが複数ある形なので、これらも上記の通りトークンに変換して保持します。</p> <pre><code class="javascript">const subFolders = []; // 参照予定のFolderIteratorリストを保持する配列 ... // スクリプトプロパティを取得 const scriptProperty = PropertiesService.getScriptProperties(); // 参照予定のFolderIteratorをトークンに変換する const subFolderTokens = subFolders.map(subFolder => subFolder.getContinuationToken()); // 参照予定のリストをJSONに変換して保持 scriptProperty.setProperty("folders", JSON.stringify(subFolderTokens)); </code></pre> <p>処理途中のイテレータも同じく<code>getContinuationToken</code>メソッドでトークンを取得できるので 取得しつつ<code>subFolders</code>の先頭に置いておけば再開できそうですね。</p> <p>ここまでで、一時停止のためのデータ保持が完了です。</p> <h3 id="処理の続きを実行するトリガーを定義する"><a href="#%E5%87%A6%E7%90%86%E3%81%AE%E7%B6%9A%E3%81%8D%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B">処理の続きを実行するトリガーを定義する</a></h3> <p>GASにはトリガーという機能があり、特定の時間やタイミングなどになったら処理を始める、ということを指定できます。<br /> トリガークラスについてはこの辺を参照ください。</p> <p>トリガーの作成は<code>ScriptAppのnewTrigger()</code>で作成できます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/script/script-app?hl=ja#getProjectTriggers%28%29">Class ScriptApp  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/script/script-app?hl=ja#getProjectTriggers()</a></p> <p>トリガー作成時に、トリガー起動時に呼び出す関数名を文字列で指定します。 ここでは、トリガーとして1分後に起動するトリガーを作成します。</p> <pre><code class="javascript">ScriptApp.newTrigger(functionName).timeBased().after(1000 * 60).create(); </code></pre> <p><code>newTrigger()</code>が返してくるのは<code>TriggerBuilder</code>クラスになっています。 詳しくはこちら。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/script/trigger-builder?hl=ja">Class TriggerBuilder  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/script/trigger-builder?hl=ja</a></p> <p><code>timeBased()</code>はトリガーの種類として時間を基準として動くタイプとして指定しています。 <code>after()</code>でトリガーが起動する時間をミリ秒で指定しています。 <code>create()</code>で指定したトリガーを作っているだけ。見ればわかる感じですね。</p> <h3 id="一時停止した場合に前回の処理から再開する"><a href="#%E4%B8%80%E6%99%82%E5%81%9C%E6%AD%A2%E3%81%97%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AB%E5%89%8D%E5%9B%9E%E3%81%AE%E5%87%A6%E7%90%86%E3%81%8B%E3%82%89%E5%86%8D%E9%96%8B%E3%81%99%E3%82%8B">一時停止した場合に前回の処理から再開する</a></h3> <p>前回の処理がある場合は、前回の処理から再開してあげる必要があります。</p> <p>先ほど<code>PropertiesService</code>に保持したデータですが、ここから取得してできるかどうかで判別したり、関数の引数の有無で判別したり、方法はいくつかありそうですがここでは<code>PropertiesService</code>にデータがあるかどうかで判別してみます。</p> <pre><code class="javascript">// スクリプトプロパティを取得 const scriptProperty = PropertiesService.getScriptProperties(); // 前回の処理からの続きとなるデータを取得 const resumeData = scriptProperty.getProperty("folders"); if(resumeData === null) { // 前回のデータがないので、ターゲットとなるフォルダを取得 } else { // 前回のデータがあるので、再開する } </code></pre> <p>再開する場合は、トークンを使ってイテレータの続きを取得します。 上記サンプルのelse部分は以下のような感じになります。</p> <pre><code class="javascript">const tokenJson = JSON.parse(resumeData); const subFolders = tokenJson.map(token => DriveApp.continueFolderIterator(token)); </code></pre> <p>あとは、このトークンから復元したイテレータをひとつづつ取り出して処理を進めればよいですね。</p> <p>まとめてコードを載せると以下のような感じ。</p> <pre><code class="javascript">// 処理時間の制限を超えたかどうかのフラグ let limitFlag = false; function allFiles(){ const functionName = "allFiles"; const startDate = new Date(); let subFolders = []; // 配下にあるサブフォルダを保持する配列 let files = []; // 中断データを取得する const resume = getResume(functionName); if (resume === null) { // 中断データがない場合は、初期フォルダから処理を開始する const folderId= "xxxxxxxxxx"; const targetDir = DriveApp.getFolderById(folderId); subFolders.push(targetDir.getFolders()); files.push(...Array.from(allFilesBy(targetDir, startDate))); } else { // 中断データがある場合は、トークンからFolderIteratorに変換する const tokensJson = JSON.parse(resume); subFolders = tokensJson.map(token => DriveApp.continueFolderIterator(token)); } for(let i = 0; i < subFolders.length; i++) { const subFolder = subFolders[i]; while(subFolder.hasNext()) { const folder = subFolder.next(); subFolders.push(folder.getFolders()); files.push(...allFilesBy(folder)); if(limitFlag || checkLimit(startDate)) break; } // 一定の処理時間を超えた場合、処理途中のFolderIteratorをトークンに変換して保存する if(limitFlag || checkLimit(startDate)){ const tokens = [subFolder.getContinuationToken()]; const subFolderTokens = subFolders.map(subFolder => subFolder.getContinuationToken()); setResume(functionName, JSON.stringify(tokens.concat(subFolderTokens))); break; } } // 今回処理した結果を返す return files; } function allFilesBy(folder) { const files = []; // 対象となるフォルダにあるファイルオブジェクトを保持する配列 const fileIterator = folder.getFiles(); while(fileIterator.hasNext()) { files.push(fileIterator.next()); } return files; } function checkLimit(startDate) { const processingSec = (new Date().getTime() - startDate.getTime()) / 1000; limitFlag = processingSec >= 300 return limitFlag; } // 中断データを取得する function getResume(functionName) { const scriptProperty = PropertiesService.getScriptProperties(); const properties = scriptProperty.getProperty(functionName); scriptProperty.deleteProperty(functionName); return properties; } // 中断データを保持し、トリガーを設定する function setResume(functionName, data) { const scriptProperty = PropertiesService.getScriptProperties(); scriptProperty.setProperty(functionName, data); setTrigger(functionName); } // トリガーを設定する function setTrigger(functionName) { let triggers = ScriptApp.getProjectTriggers(); for(let trigger of triggers) { if(trigger.getHandlerFunction() === functionName){ ScriptApp.deleteTrigger(trigger); } } ScriptApp.newTrigger(functionName).timeBased().after(1000 * 60).create(); } </code></pre> <p>これで、処理時間の制限に引っかかるような長時間の処理が必要な場合、これを回避して処理させることができるようになりました。<br /> 処理時間の制限があるのでGASを使うのはきびしいな、というような場合に参考にしていただければ幸いです。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、GASの処理時間の制限を回避するため、以下の項目を組み合わせて回避してみました。</p> <ul> <li>処理時間の計測</li> <li>処理途中のデータをPropertiesServiceへ保存</li> <li>処理再開のためのトリガー設定</li> <li>前回の処理途中からの再開</li> </ul> <p>私の場合は、とあるフォルダ配下にあるファイルたちのオーナー一覧を取得してほしい、というお題が来たので上記のようなコードを書いていました。 RubyなどからAPI経由でデータ取得するよりも、GAS上で書く方がシンプルでしたが、ざっと書いたところ処理時間の制限に阻まれたのでリファレンス等を読みつつ変更した結果が今回の時期の内容になります。</p> <p>GASでコードを書いた方がシンプルだが、処理にどの程度時間がかかるかわからない というような場合でもGASを使って処理させることができるようになるので、参考になればうれしいです。</p> <p>今回は、ここまで。</p> <p>おわり</p> しきゆら tag:crieit.net,2005:PublicArticle/18641 2023-11-04T21:30:36+09:00 2023-11-04T21:30:36+09:00 https://crieit.net/posts/how-to-get-file-list-in-google-drive 【GAS】Googleドライブにあるフォルダ・ファイルの一覧を取得する <p>こんにちは、しきゆらです。<br /> 今回は、GASでGoogleドライブにあるファイル・フォルダの一覧を取得する方法をメモしておきます。</p> <h2 id="ファイル・フォルダの一覧を取得するコード"><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%BB%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%81%AE%E4%B8%80%E8%A6%A7%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89">ファイル・フォルダの一覧を取得するコード</a></h2> <h3 id="すべてのフォルダを取得する"><a href="#%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">すべてのフォルダを取得する</a></h3> <p>すべてのフォルダを取得する場合、以下だけです。</p> <pre><code class="JS">const folders = DriveApp.getFolders(); </code></pre> <p><code>DriverApp.getFolders()</code>でGoogleドライブにあるフォルダのイテレータを取得できます。<br /> ドライブ内にあるすべてのファイルが欲しければ、このイテレータ内でファイル一覧を取得していけば可能ですね。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/drive-app?hl=ja#getfolders">Class DriveApp  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/drive-app?hl=ja#getfolders</a></p> <p>なお、<code>Folder</code>クラスにも直下にあるフォルダ一覧を取得できる<code>getFolders()</code>メソッドが生えているので、同じように使えます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/folder?hl=ja#getfolders">Class Folder  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/folder?hl=ja#getfolders</a></p> <h3 id="フォルダ直下にあるファイル一覧を取得する"><a href="#%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E7%9B%B4%E4%B8%8B%E3%81%AB%E3%81%82%E3%82%8B%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E4%B8%80%E8%A6%A7%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">フォルダ直下にあるファイル一覧を取得する</a></h3> <p>上記のような<code>FolderIterator</code>から<code>Folder</code>クラスのインスタンスを取得します。<br /> <code>Folder</code>クラスには、直下にあるファイルのイテレータを取得するメソッドがあるのでこれを使います。</p> <pre><code>const folder = folders.next(); // FolderIteratorからFolderオブジェクトを取得 const fileIterator = folder.getFiles(); const files = []; // ファイルオブジェクトを格納する配列 while(fileIterator.hasNext()) { files.push(fileIterator.next()); } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/folder?hl=ja#getfiles">Class Folder  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/folder?hl=ja#getfiles</a></p> <p>これらを組み合わせれば、特定のフォルダ配下のすべてのファイル一覧を取得することができますね。 やってみましょう。</p> <h2 id="特定のフォルダ配下にあるすべてのファイルを取得する"><a href="#%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E9%85%8D%E4%B8%8B%E3%81%AB%E3%81%82%E3%82%8B%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">特定のフォルダ配下にあるすべてのファイルを取得する</a></h2> <h3 id="ターゲットとなるフォルダを取得する"><a href="#%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">ターゲットとなるフォルダを取得する</a></h3> <p>まずは、GASからターゲットとなるフォルダを取得します。</p> <pre><code>// https://drive.google.com/drive/u/0/folders/xxxxxxのxxxxxxx部分 const folderId= "xxxxxxxxxx"; const targetDir = DriveApp.getFolderById(folderId); </code></pre> <p><code>targetDir</code>はGASの<code>Folder</code>クラスのインスタンスが入っています。</p> <p>Folderクラスについては以下を参照。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/folder?hl=ja">Class Folder  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/folder?hl=ja</a></p> <h3 id="フォルダ一覧を取得する"><a href="#%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E4%B8%80%E8%A6%A7%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">フォルダ一覧を取得する</a></h3> <p>前述の通り、<code>Folder</code>クラスには配下にあるフォルダ一覧を取得するメソッドが定義されているのでそれを使います。</p> <pre><code>const folderIterator = targetDir.getFolders(); // Folderのイテレータオブジェクトが返ってくる while (folderIterator.hasNext()) { // 参照していないFolderがあればループする folders.push(folderIterator.next()); // .next()でFolderを取得 } </code></pre> <p><code>FolderIterator</code>クラスは以下を参照。<br /> よくあるイテレータで、<code>hasNext()</code>で次の要素があるかどうかを確認しつつ、次の要素があれば<code>next()</code>で取得する形です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/drive/folder-iterator?hl=ja">Class FolderIterator  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/reference/drive/folder-iterator?hl=ja</a></p> <p>ただし、ここで取得できるのは<code>targetDir</code>直下にあるフォルダのみです。<br /> <code>targetDir</code>配下すべてを取得するにはちょっと工夫してあげる必要がります。</p> <pre><code>const subFolders = [targetDir.getFolders()]; for(let i = 0; i < subFolders.length; i++) { const subFolder = subFolders[i]; while(subFolder.hasNext()) { const folder = subFolder.next(); subFolder.push(folder.getFolders()); // フォルダ一覧を取得し、次に参照する } } </code></pre> <p>配列に破壊的に追加しつつ<code>for</code>でループ処理するので、だいぶお行儀がよくない印象ですがいい方法が思いつきませんでした。<br /> <code>forEach</code>を使うと、追加された要素を参照してくれなかったので<code>for</code>で回しています。</p> <p>ファイルの一覧を取得するコードの全体像はこんな感じ。<br /> 最終的にはfilesの配列にファイルオブジェクトが集まるので、ここに対してあれこれすればよい。<br /> 3重ループになっているので、ファイル取得部分を関数に分けるなどすればもう少し見やすくなるかなと思います。</p> <pre><code>const folderId= "xxxxxxxxxx"; const targetDir = DriveApp.getFolderById(folderId); const files = []; // ファイルオブジェクトを保持する配列 const subFolders = [targetDir.getFolders()]; for(let i = 0; i < subFolders.length; i++) { const subFolder = subFolders[i]; while(subFolder.hasNext()) { const folder = subFolder.next(); subFolders.push(folder.getFolders()); const fileIterator = folder.getFiles(); while(fileIterator.hasNext()) { files.push(fileIterator.next()); } } } </code></pre> <p>フォルダの一覧の場合は、<code>fileIterator</code>のループが不要で、<code>subFolder.next()</code>を配列に詰めればOK。<br /> もっとこう書けばええやん、というご指摘等あればコメントいただければ嬉しいです。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、GASでGoogleドライブにあるとあるフォルダ配下にあるすべてのファイルを取得する方法をメモしました。<br /> GASは結構機能が用意されているので、Googleサービスに対してやろうと思ったことは気軽に実装できそうな印象です。</p> <p>ただし、GASには実行時間の制限などもあるので、この辺に気をつけて使いましょう。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/guides/services/quotas?hl=ja">Google サービスの割り当て  |  Apps Script  |  Google for Developers https://developers.google.com/apps-script/guides/services/quotas?hl=ja</a></p> <p>今回は、ここまで。</p> <p>おわり</p> しきゆら tag:crieit.net,2005:PublicArticle/17600 2021-08-14T18:58:28+09:00 2021-08-14T18:58:28+09:00 https://crieit.net/posts/GAS-611793c436908 【分かる方教えてください】GASで、スプレッドシートに入力した値と、現在の日(年・日時ではなく、「日」のみの値が合致しているかどうか) が同じであれば、チャットワークにその値を表示する というプログラムを作成しています。 <p>プログラム自体にエラーは出ていないのですが、スプレッドシートに「14」と入力しているけれど(本日の日と合致しているけれど)、<br /> チャットワークにelseの値(15)がに表示されてしまうため、正しく日を拾えていないかと思います。<br /> 理由が分かれば教えていただきたいです。よろしくお願いします。</p> <p>以下コード</p> <p>===</p> <p>function myFunction() {</p> <p>//スプレッドシートの、特定の範囲を読み込む<br /> var sheet = SpreadsheetApp.getActiveSheet();<br /> var x = sheet.getRange(2,1).getValue();</p> <p>//日付の定義(日を取得する)<br /> var date = new Date<br /> var day = date.getDate();<br /> var yesterday = date.getDate(day-1);<br /> var tomorrow = date.getDate(day+1);</p> <p>//入力した日と条件が合致していれば、(CWに値を表示する)<br /> if( x > yesterday && x < tomorrow)<br /> {</p> <p>x=x;</p> <p>}</p> <p>else{</p> <p>x=x+1;</p> <p>}</p> <p>var client = ChatWorkClient.factory({token: '8314e1a8f4c4e0c5891d1d90f731e85b'});<br /> client.sendMessage({room_id:100266334, body:x});</p> <p>}</p> tanakaQiita tag:crieit.net,2005:PublicArticle/16680 2021-02-10T11:54:19+09:00 2021-02-10T17:43:56+09:00 https://crieit.net/posts/GAS-SpreadSheet-UI [GAS]SpreadSheetにカスタムメニューを追加して簡単なUIを作る <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>GASでカスタムメニューを追加するスクリプトについてです。</p> <p>画像のようにスプレッドシートのメニューバー(ツールバー)の一番右に「カスタムメニュー」という項目が新しく追加され、クリックしたときの処理をカスタマイズすることができます。</p> <p><a href="https://crieit.now.sh/upload_images/ea884fcaf2dd48cd3c3dc75aca7e67d360234aa61d6f9.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ea884fcaf2dd48cd3c3dc75aca7e67d360234aa61d6f9.png?mw=700" alt="スクリーンショット 2021-02-09 23.15.06.png" /></a></p> <p>今回カスタムメニューを作って実装する内容は「最終行へアクティブセルを移動する」というシンプルなものです。<br /> 例えば僕は特定のPC作業をしたときのログをスプレッドシートを作っているのですが、行数が増えてくると一番下までスクロールして入力するのが面倒になります。クリックするだけで一番下に飛べると便利だなーと思って作ってみました。</p> <h2 id="全体のスクリプト"><a href="#%E5%85%A8%E4%BD%93%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88">全体のスクリプト</a></h2> <p>まずは今回作成するスクリプトの全体像です。</p> <p>スプレッドシートのコンテナバインドスクリプトを作り、以下のスクリプトを記述します。<br /> その後、スプレッドシートのページをリロードすると、カスタムメニューが反映されるはずです。</p> <pre><code class="javascript">// 現在のスプレッドシート const SS = SpreadsheetApp.getActiveSpreadsheet(); // ファイルが開いたときに動作する処理 function onOpen() { SS.addMenu('カスタムメニュー', [ {name: '最新行へ', functionName: 'moveToLastRow_'} ]); } // 最終行にアクティブセルを移動する関数 function moveToLastRow_(){ const sheet = SS.getActiveSheet(); const lastRow = sheet.getLastRow(); const lastRange = sheet.getRange(lastRow + 1, 1); SpreadsheetApp.setActiveRange(lastRange); } </code></pre> <p>それでは作り方とカスタマイズの仕方を見ていきます。</p> <h2 id="1. onOpen()関数を作る"><a href="#1.+onOpen%28%29%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%9C%E3%82%8B">1. onOpen()関数を作る</a></h2> <p>まずは<code>onOpen()</code>という名前で、ファイルが開かれたときに動作する関数を作っておきます。</p> <pre><code class="javascript">// 現在のスプレッドシート const SS = SpreadsheetApp.getActiveSpreadsheet(); // ファイルが開いたときに動作する処理 function onOpen() { // [TODO] カスタムメニューを追加する処理 } </code></pre> <p>SpreadSheetオブジェクトの定義も最初にしておきましょう。のちの関数でもこのオブジェクトを使うため、グローバルスコープに記述しています。</p> <p>このように<code>onOpen()</code>という名前で関数を作ると、ファイルが開いたときに動作する処理を作ることができます。トリガーを設定する必要などありません。<br /> こういった特定のイベントが起こったときに処理を実行する関数を「イベントハンドラ」と呼ぶそうです。</p> <h2 id="2. メニューの選択肢を追加する"><a href="#2.+%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%81%AE%E9%81%B8%E6%8A%9E%E8%82%A2%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">2. メニューの選択肢を追加する</a></h2> <p>続いてカスタムメニューをクリックした時の選択肢を追加していきます。<br /> スプレッドシートオブジェクトの<code>addMenu()</code>メソッドを使い、先ほどのコードに以下のように記述します。</p> <pre><code class="javascript">// ファイルが開いたときに動作する処理 function onOpen() { // カスタムメニューを追加する処理 SS.addMenu('カスタムメニュー', [ {name: '最新行へ', functionName: 'moveToLastRow_'} ]); } </code></pre> <p>この<code>addMenu()</code>の第一引数には、メニューバーに表示させる文字列を書きます。<br /> ここでは「カスタムメニュー」としておきましょう。</p> <p>第二引数には、追加したいメニューをオブジェクトの配列で書いてあげます。<br /> このオブジェクトに<code>name</code>と<code>functionName</code>というプロパティを持たせることで、メニューに表記する文字列と、選択されたときに実行する関数名を指定することができます。</p> <pre><code class="javascript">{name: '<メニューに表記する文字列>', functionName: '<関数名>'} </code></pre> <p>ここで指定する関数は後ほど作るので、現段階では実装予定の名前で大丈夫です。<br /> ここでは<code>moveToLastRow_</code>という関数名を指定しておきました。</p> <p>注意点としては、関数名には丸かっこをつけずに書くことです。<br /> もし丸カッコをつけてしまうと、その関数が実行されて戻り値が<code>functionName</code>の値に設定されてしまうことになります。今回は関数名を文字列として指定したいので、丸かっこをつけないで文字列リテラルで書くのが正解になります。</p> <p>この<code>addMenu()</code>の第二引数に指定するオブジェクトを増やしていくことで、どんどんメニューを追加することもできます(後ほど備考として記載します)。</p> <h2 id="3. メニューを選択したときに動作する関数を作る"><a href="#3.+%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E5%8B%95%E4%BD%9C%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%9C%E3%82%8B">3. メニューを選択したときに動作する関数を作る</a></h2> <p>最後に、先ほど関数名に指定した<code>moveToLastRow_()</code>関数を新たに定義すればOKです。<br /> ここでは現在のシートの「最終行の次の行の1列目」にアクティブセルを移動させています。</p> <pre><code class="javascript">// 最終行にアクティブセルを移動する関数 function moveToLastRow_(){ const sheet = SS.getActiveSheet(); const lastRow = sheet.getLastRow(); const lastRange = sheet.getRange(lastRow + 1, 1); SpreadsheetApp.setActiveRange(lastRange); } </code></pre> <p><strong>アクティブセルの移動先を指定したい場合</strong><br /> もし移動させるセルを変更したい場合は、上のコードの5行目の<code>getRange()</code>に渡す引数を変えてあげましょう。</p> <pre><code class="javascript">const lastRange = sheet.getRange(lastRow + 1, 1); </code></pre> <p>例えば、現在のシートの「最終行の3列目」などとしたい場合は、次のようにカスタマイズすればOKです。</p> <pre><code class="javascript">const lastRange = sheet.getRange(lastRow, 3); </code></pre> <p>以上で実装は完了です。<br /> スクリプトを保存して、スプレッドシートを開いているページをリロードして少し待つと、カスタムメニューが追加されているかと思います。</p> <h2 id="備考"><a href="#%E5%82%99%E8%80%83">備考</a></h2> <p><strong>カスタムメニューの選択肢を増やしたい場合</strong><br /> <code>onOpen()</code>に書いた<code>SS.addMenu()</code>の中身のオブジェクトを増やしてみましょう。</p> <pre><code class="javascript">function onOpen() { // カスタムメニューを追加する処理 SS.addMenu('カスタムメニュー', [ {name: '最新行へ', functionName: 'moveToLastRow_'}, {name: 'マスタ転記', functionName: 'copyFromMaster_'}, {name: 'ほげほげ', functionName: 'hogehoge'} ]); } </code></pre> <p>配列の要素としてオブジェクトを追加していく形になるので、注意してくださいね。<br /> 構文エラー(SyntaxError)が出てしまう場合は、オブジェクトの書き方やカンマの有無など要チェックです。</p> <p><strong>スプレッドシートを開いたときに最終行に移動させたい場合</strong><br /> 今回はカスタムメニューを使って最終行へ移動するスクリプトを書きましたが、ファイルを開いたときにアクティブセルを移動させるやり方でも良いかもしれません。<br /> そうしたい場合は、<code>onOpen()</code>に直接今回の<code>moveToLastRow_()</code>の中身の処理を書いてあげればOKです。<br /> ただし、セル数にもよりますが起動が重くなってしまうかもしれないので、ご注意ください。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>カスタムメニューは、スプレッドシートを使っていて「ボタンをクリックしたら一括で処理するようにしたい」というような場合にシンプルなUIを作ることができて、とても便利です。<br /> 他にもスプレッドシートの関数で転記できるような作業でも、GASのカスタムメニューを使った実装をすることでシートを軽くしたりもできるので、よく活用しています。</p> <p><strong>その他の実装アイデア</strong><br /> - カスタムメニューを実行すると、マスターデータから必要な情報を探して一括で転記する()<br /> - シートでメール送信先リストを作り、カスタムメニューをクリックするとメールが一括送信されるようにする</p> Massa tag:crieit.net,2005:PublicArticle/16373 2020-12-16T15:24:50+09:00 2020-12-24T10:21:10+09:00 https://crieit.net/posts/GAS-Python 農家だけど色々作ったので振り返る2020 <p>こちらの記事は <a href="https://crieit.net/">Crieit</a> のアドベントカレンダー <a href="https://crieit.net/advent-calendars/2020/crieit">なんでも</a> の15日目の記事として、書かせていただきます。<br /> (日付を間違えていて一日ずれての投稿になってしまったのは秘密です...)</p> <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>今年2020年は、本業の農業のかたわら色々なツール制作に取り組んできました。<br /> 仕事に活きるもの、生活に活きるもの、プログラミング学習に活きるもの、また作ったけど活かせなかったものなどなど。言語は主にGASとPython。<br /> この機会に、ノンプログラマーがプログラミングを学んで作ったものを振り返って整理しておきたいと思います。</p> <p>昨年からプログラミングをやっていて、今年の2月ごろにGASでちょっとしたプログラムで身近なことが便利になる、というのを知ってから、だいぶプログラミングが楽しくなったのを覚えています。</p> <h2 id="制作したもの"><a href="#%E5%88%B6%E4%BD%9C%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE">制作したもの</a></h2> <h3 id="GASでのツール制作"><a href="#GAS%E3%81%A7%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB%E5%88%B6%E4%BD%9C">GASでのツール制作</a></h3> <h4 id="(1)LINEbot:ゴミ出しリマインダ"><a href="#%EF%BC%88%EF%BC%91%EF%BC%89LINEbot%EF%BC%9A%E3%82%B4%E3%83%9F%E5%87%BA%E3%81%97%E3%83%AA%E3%83%9E%E3%82%A4%E3%83%B3%E3%83%80">(1)LINEbot:ゴミ出しリマインダ</a></h4> <p>練習のために作ってみた、家族で共有するためのbotです。燃えるゴミ・燃えないゴミなどの収集日の前日の夜にリマインダを飛ばしてくれる、というだけのもの。<a target="_blank" rel="nofollow noopener" href="https://developers.line.biz/ja/services/messaging-api/">Messaging API</a>を使って実装し、放置していて今も動いています。</p> <p><a href="https://crieit.net/posts/GAS-LINE">【GAS】家族で使えるゴミ出しリマインダ LINEチャットボットを作った - Crieit</a></p> <h4 id="(2)LINEbot:農作業記録アシスタント"><a href="#%EF%BC%88%EF%BC%92%EF%BC%89LINEbot%EF%BC%9A%E8%BE%B2%E4%BD%9C%E6%A5%AD%E8%A8%98%E9%8C%B2%E3%82%A2%E3%82%B7%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%88">(2)LINEbot:農作業記録アシスタント</a></h4> <p>こちらは2月ごろにかなり力を入れて制作した、LINE上で対話形式で作業日報を入力していくツールです。入力した内容はGoogleCalendarに登録されます。<br /> <a target="_blank" rel="nofollow noopener" href="https://developers.line.biz/ja/services/messaging-api/">Messaging API</a>を使いラインのトーク画面に表示できるリッチメニューの実装を学習。<br /> LINEはITリテラシーが高くない一般の方々にも有用なツールだと実感します。</p> <p>ただ、このbotを興味持ってくれた人数名にテストユーザーになってもらうためセッティングしたのですが、使用のハードルはやはり高そうだなーという印象でした。そもそも「日誌はアナログで取っており基本的に満足している」というところに刺さっていくのは、素人の思いつきでは厳しいものがあるんだろうな、と思います。ツール制作にこだわらず、別のアプローチが必要です。</p> <p>個人的にもこちらは現在運用はしておらず、農作業の記録は<a target="_blank" rel="nofollow noopener" href="https://www.agri-note.jp/">アグリノート</a>という既存のサービスを活用しています。</p> <p><a href="https://crieit.net/posts/GAS-LINE-bot">GASで「農作業記録アシスタント」LINE bot作った(紹介編) - Crieit</a><br /> <a href="https://crieit.net/posts/GAS-LINE-bot-5e59f5af2ba2b">GASで「農作業記録アシスタント」LINE bot作った(技術・設計編) - Crieit</a></p> <p><a href="https://crieit.now.sh/upload_images/1903de9c56e588a3690fd8581790b2bc5fd9a8f14222f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1903de9c56e588a3690fd8581790b2bc5fd9a8f14222f.png?mw=700" alt="704d5676a24bb5091ddb305676ac996f5e55e34d74b65.png" /></a></p> <h4 id="(3)Gmail添付ファイル保存ツール"><a href="#%EF%BC%88%EF%BC%93%EF%BC%89Gmail%E6%B7%BB%E4%BB%98%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E4%BF%9D%E5%AD%98%E3%83%84%E3%83%BC%E3%83%AB">(3)Gmail添付ファイル保存ツール</a></h4> <p>業務資料を整理するため制作。農協からの情報の多くはFAXで送られてきますが、その内容がpdfファイルでメール添付でGmailに添付されて送られます。その添付ファイルをDriveに保存する作業を自動化しようと思って着手。<br /> 4月に主にネット情報をもとにコピペで制作し、バグが出て放置していたものを、現在は言語を学び直しながら改善中です。Driveにファイル名によりフォルダ分けして格納し、個人で使ってるSlackへ通知とリンクが送られるようにもしてみました。</p> <p><a href="https://crieit.now.sh/upload_images/7e391def0f93fbc5e90f01dfd1adde8a5fd9a91111134.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7e391def0f93fbc5e90f01dfd1adde8a5fd9a91111134.png?mw=700" alt="スクリーンショット 2020-12-16 14.49.08.png" /></a></p> <h3 id="Pythonでのツール制作"><a href="#Python%E3%81%A7%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB%E5%88%B6%E4%BD%9C">Pythonでのツール制作</a></h3> <h4 id="(1)LINEbot:共同作業グループ用の作業時間記録・集計ツール"><a href="#%EF%BC%88%EF%BC%91%EF%BC%89LINEbot%EF%BC%9A%E5%85%B1%E5%90%8C%E4%BD%9C%E6%A5%AD%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E7%94%A8%E3%81%AE%E4%BD%9C%E6%A5%AD%E6%99%82%E9%96%93%E8%A8%98%E9%8C%B2%E3%83%BB%E9%9B%86%E8%A8%88%E3%83%84%E3%83%BC%E3%83%AB">(1)LINEbot:共同作業グループ用の作業時間記録・集計ツール</a></h4> <p>7月に制作。初めて他者に使ってもらって良い評価をもらった、満足度の高いツールです。<br /> 自分の所属している地域の共同作業グループで使うための入力支援ツールを<a target="_blank" rel="nofollow noopener" href="https://developers.line.biz/ja/services/messaging-api/">Messaging API</a>を使って作りました(LINE大好きすぎる)。</p> <p>サーバーとして<a target="_blank" rel="nofollow noopener" href="https://jp.heroku.com/free">Heroku</a>を利用(正しい表現かちょっとわからない)。入力データはDB代わりにSpreadsheetと連携しています。シート操作のライブラリはgspreadを使いました。クラウドサーバにデプロイ時のライブラリ依存周りの設定、Spreadsheet設定でOAuth認証情報をクラウドサーバの環境変数に入れるというような部分がなかなか難しかった記憶です(そもそも環境変数とは、の段階からではありました)</p> <p>現場では作業した人が、LINEで作業開始・終了の時間をタップだけで入力できて、後から時給計算する際もLINEで集計結果が返ってくるようになっています。これまで紙で管理して電卓で集計していたのが、相当楽になりました。<br /> グループでの作業は年に1回、収穫時期が来れば必ず必要になる作業なので、LINE botも毎年改善しながら運用を続けていきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://note.com/agrifeel_labo/n/nf391d0b8e477">業務で活用するLINEbot制作にあたって意識した事|Massa|note</a></p> <h4 id="(2)卸売市場価格を取得するためのスクレイピングツール"><a href="#%EF%BC%88%EF%BC%92%EF%BC%89%E5%8D%B8%E5%A3%B2%E5%B8%82%E5%A0%B4%E4%BE%A1%E6%A0%BC%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%E3%83%84%E3%83%BC%E3%83%AB">(2)卸売市場価格を取得するためのスクレイピングツール</a></h4> <p><a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/community-nonpro-semi/">ノンプロ研</a>で受講したPython講座きっかけで、Seleniumを使ったWebサイトのスクレイピングに取り組みました。作っている農産物の価格動向を毎日見られるようにしたいなと思いました。</p> <p>7月、まずは初めはpdfのダウンロードをしてDropboxへ入れるだけのツールを制作しましたが、ダウンロードしっぱなしで全然活用できず。<br /> 次に11月には収集する対象を市況データのまとまった別のWebサイトに変更し、その日の欲しい市況データをSpreadsheetに入れる & 自分のSlackへ送信、というもの。</p> <p>定期実行には、Windowsのバッチファイルとタスクスケジューラを使用してローカルで実行する方法、クラウドのHerokuに上げて<a target="_blank" rel="nofollow noopener" href="https://devcenter.heroku.com/articles/scheduler">Heroku Scheduler</a> を使用する方法を試しました。ローカルの方がセッティングは楽ですが、PC起動時にしかスクリプトが動かせないというデメリットがあります(やりようはあるのかな?)。クラウドはデプロイができれば任せっきりで楽ですが、HerokuでSeleniumを動かす設定に一手間かかりました。一長一短で、習得コストも考慮して運用方法は考えなければなあと学習。</p> <p>こちらは今後、Slackに送られてくるデータをもっと見やすいように工夫したいです。</p> <p><a href="https://crieit.now.sh/upload_images/60d1eba48a433808894d7fe3c013737c5fd99fba5487e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/60d1eba48a433808894d7fe3c013737c5fd99fba5487e.png?mw=700" alt="スクリーンショット 2020-12-16 14.48.28.png" /></a></p> <h4 id="(3)楽天市場の価格調査"><a href="#%EF%BC%88%EF%BC%93%EF%BC%89%E6%A5%BD%E5%A4%A9%E5%B8%82%E5%A0%B4%E3%81%AE%E4%BE%A1%E6%A0%BC%E8%AA%BF%E6%9F%BB">(3)楽天市場の価格調査</a></h4> <p>卸売価格からのアプローチだけでなく、直販価格からのアプローチもしてみたくて取り組み。<br /> 9月は統計に興味が向いていて、Pandasの扱い方を覚え、データの収集と加工、価格分析の基礎的なところをやってみました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://webservice.rakuten.co.jp/api/ichibaitemsearch/">楽天商品検索API</a>で特定のキーワードに合致する商品情報を取得する</li> <li>取得したデータを使える形に加工して分析する</li> </ul> <p>統計学的な難しいことはわからないのですが、最終的に平均値・中央値などの簡単な統計量を算出し、価格が重量に線型近似していることを確かめ、平均的なキロ単価を算出することができました(正確では無いかと思いますが、参考程度には十分な数値だと思います)。</p> <p>現場の人間がデータを扱うには、ExcelやSpreadsheetなどの表計算ソフトの扱いに慣れているかどうかが大事だなと思いました。「データ分析=Python, Pandas」みたいなイメージを持っていましたが、それよりも「何を目的としてやるか」があり、そのために「どのツール(手段)でやるか」という考え方で、習得コストのかからないやり方を選ぶのも大事なんだろうなと思います。</p> <p>こちらは<a target="_blank" rel="nofollow noopener" href="https://m-ohsaki.stores.jp/">農産物のネットショップ</a>開設のための価格調査に直結したので、ひとまず成果につながった分析だったかなあと思っています。</p> <p><a href="https://crieit.net/posts/Python-IT-API-csv">Python農家のITマーケティング試行錯誤日記1.楽天商品検索APIを使って商品情報をcsv出力する - Crieit</a><br /> <a href="https://crieit.net/posts/IT-2-Python">Python農家のITマーケティング試行錯誤日記2.商品情報の収集とデータ加工を行う - Crieit</a><br /> <a href="https://crieit.net/posts/IT-3-Python">Python農家のITマーケティング試行錯誤日記3.商品価格の集計と分析を行う - Crieit</a></p> <p>⬇️商品「メークイン」の箱の重量(kg)と価格の中央値(円)の関係<br /> <a href="https://crieit.now.sh/upload_images/af1e6345e0af0b9d1c933d8d366b6e3a5fd9aa406ef6c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/af1e6345e0af0b9d1c933d8d366b6e3a5fd9aa406ef6c.png?mw=700" alt="スクリーンショット 2020-12-16 15.33.02.png" /></a></p> <p>⬇️近似曲線から商品「メークイン」のキロ単価を計算<br /> <a href="https://crieit.now.sh/upload_images/30bc7229b89779b2024968f1159d5be35fd9aa38451a7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/30bc7229b89779b2024968f1159d5be35fd9aa38451a7.png?mw=700" alt="スクリーンショット 2020-12-16 15.32.23.png" /></a></p> <h4 id="(4) GUI付きテキストファイル作成ツール"><a href="#%EF%BC%88%EF%BC%94%EF%BC%89+GUI%E4%BB%98%E3%81%8D%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E4%BD%9C%E6%88%90%E3%83%84%E3%83%BC%E3%83%AB">(4) GUI付きテキストファイル作成ツール</a></h4> <p>自分用に開発作業のログを残すのを便利にするために作りました。Python標準ライブラリの<code>tkinter</code>を使ってGUIを作って、プルダウンを選びボタンを押すと選んだ内容に応じたテンプレートが入ったテキストファイルが作成される、という仕組みです。またちょっとマニアックなこともトライしていて、同じく標準ライブラリの<code>subprocess</code>を使って、ボタンを押すとGithubへプッシュするコマンドを実行するようなコマンド操作も学習しました。<br /> 作成したPythonスクリプトはpyファイルにしてデスクトップに置き、ダブルクリックで実行できるようにしています。</p> <p>こちらは現在も運用中です。</p> <p><a href="https://crieit.now.sh/upload_images/8f9d86893218bd481d69e41113c1a2755fd9a142a9cfe.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8f9d86893218bd481d69e41113c1a2755fd9a142a9cfe.png?mw=700" alt="スクリーンショット 2020-12-16 14.53.27.png" /></a></p> <h4 id="(5)PDF文字抽出ツール/自炊用PDF結合・加工ツール"><a href="#%EF%BC%88%EF%BC%95%EF%BC%89PDF%E6%96%87%E5%AD%97%E6%8A%BD%E5%87%BA%E3%83%84%E3%83%BC%E3%83%AB%EF%BC%8F%E8%87%AA%E7%82%8A%E7%94%A8PDF%E7%B5%90%E5%90%88%E3%83%BB%E5%8A%A0%E5%B7%A5%E3%83%84%E3%83%BC%E3%83%AB">(5)PDF文字抽出ツール/自炊用PDF結合・加工ツール</a></h4> <p>PythonでPDFを扱う<code>PyPDF2</code>、文字抽出には<code>pdfminer</code>というライブラリを使用。講座のPDFテキストを特定の文字があるページだけ画像化する…ということをしています。いくつかPDFファイルを触ってみたのですが、PDFファイルの文字を扱うのは一般的には難しくて、自分で作ったファイルなら良いけど、既存のPDFを扱うのはあまり考えない方が良いかな…と思いました。<br /> また最近書籍の自炊をしているとPDF結合や分割をしたくなったので、短いコードを作ってJupyter Notebook上で実行できるような、ちょっとした便利コード集みたいなものも、スキャン中の時間を生かして作りました。笑</p> <p>慣れるとこういうちょっとしたツールがすぐにできる、というのがPythonの良いところだなあ…。<br /> PDF操作に便利なコード集も整理して記事にしておきたいです。</p> <p><a href="https://crieit.net/posts/PDF-Python">PythonPDFから特定の文字列を抽出&ページを画像として保存するツール - Crieit</a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>アウトプットとしてはこんな感じでしょうか。かなり作ったんだなあ…。<br /> こうやって振り返ってみると、設計時点の反省点が明確に見えてきたり、「もう少しこう改善したいというのも整理されて良いなと思いました。</p> <p>制作の動機が自分自身のために向いているものが多く、勉強ために作ったようなものもあります。<br /> 別に悪いことでないのですが、どうしても「作る」のが好きでそこに拘ってしまうことがあり、おかげで機能がコテコテになってしまったり、メンテナンス性の悪いスクリプトになっている気がします。</p> <p>一方で振り返って良い点としては、LINEやSlackで情報を受け取るような明確な「アウトプット」が見られるツールの作り方を習得できたことです。外作業が多いと「スマホで見られる」というのも重要に感じます。</p> <h3 id="今後作っていきたいもの"><a href="#%E4%BB%8A%E5%BE%8C%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%84%E3%81%8D%E3%81%9F%E3%81%84%E3%82%82%E3%81%AE">今後作っていきたいもの</a></h3> <p>必ずしもツール制作だけじゃなくて既存のデータの「整理」というのがキーワードなのですが、ざっくり</p> <ul> <li>農場データの整理(クラウドの利用)</li> <li>既存のサービスの活用(農作業日誌、温度etc....csvファイルの活用や部分的な自動化・可視化)</li> <li>現場で使えるツール(どこを改善したいか?誰のため?)</li> </ul> <p>技術的にも興味のあるものにもっと取り組んでいきたいのですが、来年はまたもうちょっと広い視点で農業経営に役に立つ・農村地域や人の役に立つツールを作れたらな、と思います。<br /> IoTを活用してデータを蓄積できるような環境も作ってみたいです。</p> <p>勉強の幅がプログラミング以外にも広くて、どこに集中して勉強していけば良いか…。楽しく頑張ります。</p> Massa tag:crieit.net,2005:PublicArticle/16340 2020-12-11T23:10:59+09:00 2020-12-12T10:26:24+09:00 https://crieit.net/posts/GAS-Javascript-n-d [GAS][Javascript]指定の年月の「第n d曜日」を配列で取得する <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>GASでのゴミ出しリマインダ用に「第n d曜日」を計算するアルゴリズムを作りました。<br /> 面白かったので、まとめておきます。</p> <p>考え方としては、</p> <ul> <li>(1)指定した年月の「d曜日」をすべて配列で取得する</li> <li>(2)その配列から「第n d曜日」だけを抜き出して配列で取得する</li> </ul> <p>という順番で実装していきます。</p> <p>特にDate型の扱いと、配列メソッドの使い方が勉強になりました。<br /> 配列メソッドは、配列に要素として含まれているかどうかをブール値で返す<code>includes()</code>メソッドと、配列の要素のうち引数に指定した関数をみたすものだけを配列として取り出す<code>filter()</code>メソッドあたりの理解がミソになるかと思います。</p> <p>このあたりの練習問題としても、やってみたい方は是非おすすめです!</p> <h2 id="(1)指定した年月の「d曜日」をすべて配列で取得する"><a href="#%EF%BC%88%EF%BC%91%EF%BC%89%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E5%B9%B4%E6%9C%88%E3%81%AE%E3%80%8Cd%E6%9B%9C%E6%97%A5%E3%80%8D%E3%82%92%E3%81%99%E3%81%B9%E3%81%A6%E9%85%8D%E5%88%97%E3%81%A7%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">(1)指定した年月の「d曜日」をすべて配列で取得する</a></h2> <p>まずは、指定した年月の指定した曜日の一覧を配列で取得してみたいと思います。<br /> 例えば「今月の水曜日」と指定してやると、2020年12月の水曜日に該当する</p> <pre><code>第1水曜 2020/12/2 第2水曜 2020/12/9 第3水曜 2020/12/16 第4水曜 2020/12/23 第5水曜 2020/12/30 </code></pre> <p>これらをDate型の配列という形で作ってみよう、という訳です</p> <p>「変数<code>date</code> = 今日の日付(Date型)」と「変数<code>day</code> = 取得したい曜日(数値)」を指定して、目的の配列を作ってみたいと思います。</p> <p>GAS(Javascript)のDate型では、0が日曜日、1が月曜日…、6が土曜日に対応しています。</p> <p><code>const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];</code><br /> 今回は必要ありませんが、こんな定数を用意しておくと、今後なんか活用できる機会があるかも?<br /> 「あれ、0は日曜だっけ?月曜だっけ?」という備忘のためにスクリプト内に記述しておいても良いかもしれません。</p> <p>今回は「水曜日」が欲しいので、<code>day = 3</code>としてみます。</p> <p>スクリプトは以下の通りです。</p> <pre><code class="javascript">function mainFunction() { const date = new Date(); //今日の日付 const day = 3; //水曜日 const year = date.getFullYear(); const month = date.getMonth(); const days = []; for (let i = 1; i <= 31; i++){ const tmpDate = new Date(year, month, i); if (month !== tmpDate.getMonth()) break; //月代わりで処理終了 if (tmpDate.getDay() !== day) continue; //引数に指定した曜日以外の時は何もしない days.push(tmpDate); } console.log(days); } </code></pre> <p>上の<code>getDays()</code>関数を実行してログを確認してみると、「2020年12月の水曜日」を配列で得ることができます。</p> <pre><code>[ Wed Dec 02 2020 00:00:00 GMT+0900 (日本標準時), Wed Dec 09 2020 00:00:00 GMT+0900 (日本標準時), Wed Dec 16 2020 00:00:00 GMT+0900 (日本標準時), Wed Dec 23 2020 00:00:00 GMT+0900 (日本標準時), Wed Dec 30 2020 00:00:00 GMT+0900 (日本標準時) ] </code></pre> <p>こちらの記事を全力でパクらせていただきましたw<br /> <a target="_blank" rel="nofollow noopener" href="https://etauthenonprogrammercoder.tumblr.com/post/186638513489/gas%E7%89%B9%E5%AE%9A%E3%81%AE%E6%9C%88%E3%81%AE%E7%89%B9%E5%AE%9A%E3%81%AE%E6%9B%9C%E6%97%A5%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">etau the non programmer coder — GAS特定の月の特定の曜日を取得する</a></p> <h3 id="関数に切り分ける"><a href="#%E9%96%A2%E6%95%B0%E3%81%AB%E5%88%87%E3%82%8A%E5%88%86%E3%81%91%E3%82%8B">関数に切り分ける</a></h3> <p>こんな感じで<code>getDays()</code>関数に切り分けて、再利用しやすいようにしてみました。<br /> <code>mainFunction()</code>内で日付と曜日を指定して実行すると、同様の結果が得られます。</p> <pre><code class="javascript">function mainFunction(){ const date = new Date(); const day = 3; //水曜日 const days = getDays(date, day); console.log(days); } /* *その年月のd曜日を取得する関数 * * @param {Date} date - 調べたい年月の日付(Date型) * @param {number} day - 取得したい曜日を表す数値(0:日曜日〜6:土曜日) * @return {days} ある年月のday曜日の日付の入った配列 */ function getDays(date, day) { const year = date.getFullYear(); const month = date.getMonth(); const days = []; for (let i = 1; i <= 31; i++){ const tmpDate = new Date(year, month, i); if (month !== tmpDate.getMonth()) break; //月代わりで処理終了 if (tmpDate.getDay() !== day) continue; //引数に指定した曜日以外の時は何もしない days.push(tmpDate); } return days; } </code></pre> <h2 id="(2)その配列から「第n d曜日」だけを抜き出して配列で取得する"><a href="#%EF%BC%88%EF%BC%92%EF%BC%89%E3%81%9D%E3%81%AE%E9%85%8D%E5%88%97%E3%81%8B%E3%82%89%E3%80%8C%E7%AC%ACn+d%E6%9B%9C%E6%97%A5%E3%80%8D%E3%81%A0%E3%81%91%E3%82%92%E6%8A%9C%E3%81%8D%E5%87%BA%E3%81%97%E3%81%A6%E9%85%8D%E5%88%97%E3%81%A7%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">(2)その配列から「第n d曜日」だけを抜き出して配列で取得する</a></h2> <p>いくつかアプローチが考えられます。</p> <p>・先ほど作った「d曜日」を返す<code>getDays()</code>関数を加工して「第n d曜日」の配列を返す関数を作る<br /> ・メイン関数<code>mainFunction()</code>の中で配列<code>days</code>を加工して「第n d曜日」の配列を作る</p> <p>後者のやり方でシンプルにできます。<br /> 新たに変数<code>weeks</code>を定義して、配列で欲しい週を指定してあげます。<br /> (隔週に限らず、欲しい番号を指定してみましょう)</p> <pre><code class="javascript">function mainFunction2(){ const date = new Date(); const day = 3; //水曜日 const weeks = [2, 4]; //第2週目、第4週目 を指定 const days = getDays(date, day); const specifiedDays = days.filter((v, i) => weeks.includes(i+1)); console.log(specifiedDays); } </code></pre> <p>スクリプトの中身については、後ほど文法の説明とともに(ちゃんと説明できてるかどうかわからない)解説を載せておきます。<br /> この関数を実行してログを表示すると、指定した第2・第4水曜日だけの配列ができています。やったね!</p> <pre><code>[ Wed Dec 09 2020 00:00:00 GMT+0900 (日本標準時), Wed Dec 23 2020 00:00:00 GMT+0900 (日本標準時) ] </code></pre> <h3 id="備考"><a href="#%E5%82%99%E8%80%83">備考</a></h3> <p><code>weeks</code>に0, 10などのあり得ない数値を入れてやっても、特に不具合が出ることなく、該当する数値のみをとった配列が出来上がるようです。<br /> これを利用すると、例えば<code>weeks = [1, 3, 5]</code>などと指定しておけば、5週目のあるなしに関わらず欲しい配列が手に入りますね。</p> <p>また、この「第n d曜日」を配列にする処理も他の関数に切り分けて作ってはみましたが、あまりその関数を頻繁に使う機会が思い浮かびませんでした。。。<br /> なので、今回作ってみたスクリプトとしてはここまで。</p> <h3 id="解説:配列のincludes()とfilter()メソッドの活用"><a href="#%E8%A7%A3%E8%AA%AC%EF%BC%9A%E9%85%8D%E5%88%97%E3%81%AEincludes%28%29%E3%81%A8filter%28%29%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E6%B4%BB%E7%94%A8">解説:配列のincludes()とfilter()メソッドの活用</a></h3> <p>GAS(Javascript)の文法について。知っていたら飛ばしてください。</p> <p>ポイントはこの1行。</p> <pre><code class="javascript">const specifiedDays = days.filter((v, i) => weeks.includes(i+1)); </code></pre> <p>ちょっとよくわかりませんね。</p> <p>先に、今回使う配列のメソッドの最低限の使い方だけ整理しておきます。</p> <div class="table-responsive"><table> <thead> <tr> <th>メソッド</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>arr.filter()</td> <td>配列arrの要素のうち、引数に指定したをみたすものだけを抽出した配列として返す</td> </tr> <tr> <td>arr.includes()</td> <td>引数に指定したが配列arrに含まれているかどうかをブール値で返す</td> </tr> </tbody> </table></div> <p>さて...先ほどの1行は、全体の構文としては<br /> <code>const specifiedDays = days.filter(なんかの関数);</code></p> <p>という構造になっています。<br /> この構造だけみると実はシンプルで、<code>specifiedDays</code>という配列に、先ほど作った指定した曜日が全部入った配列<code>days</code>から<code>filter()</code>メソッドで「なんらかの処理」をして要素を抽出した配列を返していることになります。</p> <p>この「なんらかの処理」というのが、<code>filter()</code>メソッドの引数に指定した関数です。</p> <p><code>(v, i) => weeks.includes(i+1)</code></p> <p>こいつは「アロー関数」という関数リテラルの書き方ですね。<br /> 矢印の左側が仮引数、右側が具体的な処理です。<br /> (仮引数v, iはvalue, indexに相当します)</p> <p>ざっくり「<code>weeks</code>で指定した数値がインデックス担っているものだけ、<code>days</code>から要素を抽出している」というような処理になっています。</p> <p>今回の場合はこれにより第2、第4曜日だけを<code>days</code>から抽出して、<code>specifiedDays</code>を作っています。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>飲み会から派生したネタでした。<br /> 実用的かつ気になっていた配列のメソッドを扱う、とても良い練習になりました。<br /> メソッドの解説のあたりはだいぶ飛ばしてしまっているので、もう少しうまく説明できないかな…。思いついたら修正してみます。</p> <h3 id="MEMO"><a href="#MEMO">MEMO</a></h3> <p>参考記事<br /> <a target="_blank" rel="nofollow noopener" href="https://etauthenonprogrammercoder.tumblr.com/post/186638513489/gas%E7%89%B9%E5%AE%9A%E3%81%AE%E6%9C%88%E3%81%AE%E7%89%B9%E5%AE%9A%E3%81%AE%E6%9B%9C%E6%97%A5%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">etau the non programmer coder — GAS特定の月の特定の曜日を取得する</a></p> <p>参考書籍(pp202-213 配列を取り扱う - Arrayオブジェクト)<br /> <a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/gp/product/4798063339/ref=dbs_a_def_rwt_hsch_vapi_taft_p1_i4">Amazon.co.jp: 詳解! Google Apps Script完全入門第2版 ~GoogleアプリケーションとGoogle Workspaceの最新プログラミングガイド: 高橋宣成: 本</a></p> Massa tag:crieit.net,2005:PublicArticle/15830 2020-04-15T12:01:05+09:00 2020-04-15T12:01:05+09:00 https://crieit.net/posts/nippo-command 浜辺美波が日報を投稿してくれるSlackコマンドをつくりました <p>私は日報を忘れてしまう人間です。<br /> もう少し正しく言うとめんどくさく思ってしまう人間です。</p> <p>今日閉じたIssueと取り組んでいるIssueを全部コピペしてきたりとか。</p> <p>今働いているチームのSlackには #nippo チャンネルがあり、メンバーはみんなここにその日やったこととかを投稿しています。</p> <p>今回つくったのはそんな毎日の日報をもう少し楽ちんにして、本質である"振り返る"という行為だけに留めたいと言うものです。<br /> 言い換えると何をやったかは自動で出力できるようにしたい。</p> <p>その名も「浜辺美波」です。</p> <p><code>/nippo 今日は足が冷えて冷えて大変でした。</code></p> <p>とうつと#nippoにこのように投稿してくれます。</p> <p><img src="https://imjn.me/images/nippo-command.png" alt="screenshot" /></p> <p>仕組みとしてはSpreadSheetにそれぞれのメンバーのSlackのユーザーIDとGitHubIDを保存してあり、GASでSlack commandから送られてくるユーザー情報と照会してGitHubから関連のIssueを取得してきています。<br /> GitHub GraphQL API初めて使いましたが便利すぎてびっくりしました。<a target="_blank" rel="nofollow noopener" href="https://developer.github.com/v4/explorer/">Explorer</a>が本当に便利。</p> <h2 id="メリット"><a href="#%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88">メリット</a></h2> <h3 id="日報を書くのが楽ちんで少し楽しい"><a href="#%E6%97%A5%E5%A0%B1%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%AE%E3%81%8C%E6%A5%BD%E3%81%A1%E3%82%93%E3%81%A7%E5%B0%91%E3%81%97%E6%A5%BD%E3%81%97%E3%81%84">日報を書くのが楽ちんで少し楽しい</a></h3> <p>明らかに日報にさく時間が減りました。振り返りコメントに全神経を集中させることができるので最高に面白いコメントができるようになりそうです。</p> <h3 id="Issueを書く文化"><a href="#Issue%E3%82%92%E6%9B%B8%E3%81%8F%E6%96%87%E5%8C%96">Issueを書く文化</a></h3> <p>私以外のエンジニアメンバーは今の会社が初めてのエンジニアとしての勤務先でなかなかチーム開発に不慣れな人が多いです。その中でひとつ大変なのがIssueをきちんと書いてもらうこと。更新してもらうこと。<br /> 今回の<code>/nippo</code>コマンドによってIssueの役に立っている感がすごくあるので多少Issueをきちんと更新する文化に貢献してくれるのではないかと期待しています。</p> <h2 id="コード Google App Script"><a href="#%E3%82%B3%E3%83%BC%E3%83%89+Google+App+Script">コード Google App Script</a></h2> <pre><code>function doPost(e) { var spreadsheet = SpreadsheetApp.openById('SpreadSheetのID'); var sheet = spreadsheet.getActiveSheet(); var username = e.parameter.user_name; var comment = comment = e.parameter.text; var row = findRowForSlackUsername(sheet,username); var githubId = sheet.getRange(row, 2).getValue(); var freeeId = sheet.getRange(row, 3).getValue(); if(comment.length === 0) { return ContentService.createTextOutput("⚠️`/nippo 振り返りコメント`の形式でコメントも書いて!"); } if(githubId.length === 0 || freeeId.length === 0) { return ContentService.createTextOutput("⚠️あなたのアカウント情報がまだちゃんと用意されてないみたい。超絶イケメンなimjnに聞いてみて!"); } var resForClosed = fetchIssues(githubId, "closed"); var resForWIP = fetchIssues(githubId, "open"); var closedIssues = JSON.parse(resForClosed.getContentText()).data.search.edges; var wipIssues = JSON.parse(resForWIP.getContentText()).data.search.edges; var issuesClosedToday = getIssuesClosedToday(closedIssues); var outputString = `今日もおつかれさまでした。 ${githubId}の日報です💙\n\n`; outputString += `💪 *今${githubId}が取り組んでいるIssue* ↓\n` if(wipIssues.length > 0) { var wipIssueString = ""; for(var i=0;i<wipIssues.length;i++){ var wipIssue = wipIssues[i]; wipIssueString += `${wipIssue.node.title} (${wipIssue.node.url})\n`; } outputString += "```" + wipIssueString + "```"; } else { outputString += `> ${githubId}がアサインされているタスクはありません。タスク待ち!`; } outputString += `\n\n🤗 *今日${githubId}がCloseしたIssue* ↓\n` if(issuesClosedToday.length > 0) { var issueString = ""; for(var i=0;i<issuesClosedToday.length;i++){ var theIssue = issuesClosedToday[i]; issueString += `${theIssue.node.title} (${theIssue.node.url})\n`; } outputString += "```" + issueString + "```"; } else { outputString += `> ${githubId}が今日CloseしたIssueはありません`; } outputString += `\n\n✍️ *振り返りコメント* ↓\n` outputString += '```' + comment + '```'; const webhookUrl = "Slackのwebhook URL"; const data = { 'attachments': [{ 'color': '#0086CC', 'text' : outputString, }] }; const payload = JSON.stringify(data); const options = { 'method' : 'POST', 'contentType' : 'application/json', 'payload' : payload }; UrlFetchApp.fetch(webhookUrl, options); return ContentService.createTextOutput("#nippoに投稿したよ!❤️"); } function findRowForSlackUsername(sheet,val){ var lastRow=sheet.getDataRange().getLastRow(); for(var i=1;i<=lastRow;i++){ if(sheet.getRange(i,1).getValue() === val){ return i; } } return 0; } function fetchIssues(githubId, status) { const query = 'query {\ search(last: 10, query: "org:オーガニゼーションID is:issue is:' + status + ' assignee:' + githubId + ' sort:updated-desc", type: ISSUE) {\ edges {\ node {\ ... on Issue {\ title\ url\ closedAt\ }\ }\ }\ }\ }'; const option = buildGraphqlRequest(query); return UrlFetchApp.fetch("https://api.github.com/graphql", option); } function buildGraphqlRequest(graphql) { return { method: "post", contentType: "application/json", headers: { Authorization: "bearer GitHubのアクセストークン", }, payload: JSON.stringify({ query: graphql }), }; } function getIssuesClosedToday(issues) { var now = new Date(); var closedIssues = []; for(var i=0;i<issues.length;i++){ var issue = issues[i]; var closedAt = new Date(issue.node.closedAt); if(closedAt.getFullYear() === now.getFullYear() && closedAt.getMonth() === now.getMonth() && closedAt.getDate() === now.getDate()) { closedIssues.push(issue); } } return closedIssues; } </code></pre> <p>せっかくfreeeも使ってるのでまた時間がある時に勤務時間とかも含められるようにしたいと思います。</p> <p>今月中に旅行領域で新規プロダクトを出します。こんなご時世ですが、がんばる。</p> imjn tag:crieit.net,2005:PublicArticle/15738 2020-02-27T23:28:56+09:00 2020-03-06T23:45:06+09:00 https://crieit.net/posts/GAS-LINE-messaging-API GAS & LINE messaging APIの学習便利帳 <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>ノンプログラマの僕がGAS(Google Apps Script)とLINE messaging APIを活用したツールを制作するにあたり、全くよくわからない状態から何となくそれらしいものが作れるようになるまでに学習した、主な教材とブログをまとめておきます。<br /> これからGASを習得したい! LINE botを制作したい! という方の情報収集の参考にしてもらえたら嬉しいです。</p> <h2 id="(1)GASの習得のために"><a href="#%EF%BC%88%EF%BC%91%EF%BC%89GAS%E3%81%AE%E7%BF%92%E5%BE%97%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AB">(1)GASの習得のために</a></h2> <h3 id="Udemy教材"><a href="#Udemy%E6%95%99%E6%9D%90">Udemy教材</a></h3> <p>●<a target="_blank" rel="nofollow noopener" href="https://www.udemy.com/course/gas_for_operational_efficiency/">ビジネスパーソンに贈る業務効率化大全 〜Google Apps Scriptによる業務の自動化〜 | Udemy</a></p> <p>もはやGAS入門の決定版と言いたい。実際に業務効率化にすぐに活用できるスクリプトを書きながら、GASの基本から段階を追って体系的に習得できます。<br /> とても解説がわかりやすいし、一通りの必要な操作(Googleカレンダー、Googleフォーム、Googleメール、LINEやSlackへの通知、スプレッドシート・ドキュメントの扱いなど)を実戦形式で学べます。<br /> 最後は駆け足ながらGASでのWebスクレイピングの講座もあるので、これを足がかりにもっと深く学びたくなりました。</p> <h3 id="ブログ"><a href="#%E3%83%96%E3%83%AD%E3%82%B0">ブログ</a></h3> <p>●<a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/">いつも隣にITのお仕事 | 毎日の業務が楽チンに!</a></p> <p>書籍「詳解!GoogleAppsScript完全入門」の著者さま。こちらも基本から解説してくれていて文章がとてもわかりやすく、コードだけでなくプログラミング共通の基本の考え方に通じる学びがあります。<br /> 作業をしていると「GAS + やりたいこと」「Javascript + やりたいこと」で検索して解決することが多いのですが、その際によくヒットしてきます。<br /> GASのPropaty Serciceで秘匿情報を隠すという考え方と実際のやり方はこちらで学びました。この初心者向けGASのシリーズで体系的にGASを学ぶことができるのも面白そうです。<br /> ちなみに僕は次にこちらに連載されている「Gmailの添付ファイルをGoogleドライブに自動保存する」というツールの作成に挑戦してみようと思っています。</p> <p>→ <a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-property-store/">【初心者向けGAS】プロパティストアの概要とスクリプトプロパティの編集方法</a><br /> → <a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-gmail-attachment-drive/">Google Apps ScriptでGmailの添付ファイルをGoogleドライブに保存する</a></p> <h2 id="(2)GASでLINE messaging APIを操作するために"><a href="#%EF%BC%88%EF%BC%92%EF%BC%89GAS%E3%81%A7LINE+messaging+API%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB">(2)GASでLINE messaging APIを操作するために</a></h2> <h3 id="Udemy教材"><a href="#Udemy%E6%95%99%E6%9D%90">Udemy教材</a></h3> <p>●<a target="_blank" rel="nofollow noopener" href="https://www.udemy.com/course/line-bot-x/">はじめてのLINEBOTの作り方 秘書ボットを作ろう | Udemy</a></p> <p>こちらはGASでLINE botを制作するプロセスを学べる講座。<br /> コードの解説ももちろんありますが、それよりも初めてLINE botを作る際にサンプルコードを活用して「動くものを作って流れを体感する」という部分でとても勉強になりました。<br /> 基本の「おうむ返しボット」(ユーザーがLINEで投稿したメッセージをそのまま返答する)からスタートして、Googleカレンダーと連携して予定を追加したり毎日決まった時間に予定をLINEに通知してくれる「秘書ボット」を作成するという講座。作成したボットはそのまま自分でも活用しています。</p> <p>僕はLINE bot制作にあたってこの講座を一番最初に学んだのですが、それによりLINE messaging APIの動きを体感することができました。<br /> 細かいコード部分はこの講座だけだと理解が難しいのですが、作りたいものを作っていく過程で改めてこのサンプルコード読み解いていくという作業を行いました。<br /> GASのCache Serviceというものを利用してLINE botの処理を分岐する方法はこの講座で知ることができました。</p> <h3 id="ブログ"><a href="#%E3%83%96%E3%83%AD%E3%82%B0">ブログ</a></h3> <p>●<a target="_blank" rel="nofollow noopener" href="https://arukayies.com/">30歳からの「くら」のブログ</a></p> <p>LINE messaging APIをGASのコードに落とし込むための書き方をだいぶ参考にしました。<br /> 特に「日時選択アクション」「クイックリプライ」「テンプレートメッセージ」などを使うためのシンプルなコードが紹介されているので、公式ドキュメントだけだとどうGASで書いたら良いかわからなかったものも実際にコードに落とし込むことができます。</p> <p>●<a target="_blank" rel="nofollow noopener" href="https://note.com/_hasekatsu">HASEKATSU|note</a></p> <p>GAS + LINE botに特化して数年にわたり独学を続けてきた血と涙と汗の結晶を感じます!<br /> これまでに学んできた大量のコードを公開&販売しつつ、noteの記事では惜しみなく実際のコードと積み上げてきた情報を出して解説しています。フレームワーク的なものも自作していてやばい。これからも楽しみです。</p> <p>今回何よりも僕が助かったのが、リッチメニューをLINE公式アカウントからではなくLINE messaging APIを使ってGASのスクリプトで実装するやり方です。これは他にほとんど情報がなく、大いに頼ることになりました。<br /> またCoconaraでのサービス提供の実績もあり、実践的に色々試行錯誤している姿勢を僕も学んでトライしていきたいと思います。</p> <p>→ <a target="_blank" rel="nofollow noopener" href="https://note.com/_hasekatsu/n/n83afa5ffee56">Postmanでリッチメニューを設定する方法【LINE Messaging API×GAS】|HASEKATSU|note</a><br /> → <a target="_blank" rel="nofollow noopener" href="https://note.com/_hasekatsu/n/n37fc2985aa0d?creator_urlname=_hasekatsu">GASでLINE Messaging APIのリッチメニューオブジェクトをGoogleスライドから作成する【実戦GoogleAppsScriptプログラミング講座~天空闘技場~】|HASEKATSU|note</a><br /> → <a target="_blank" rel="nofollow noopener" href="https://note.com/_hasekatsu/n/n66277fe34638?creator_urlname=_hasekatsu">GASでLINE Messaging APIのリッチメニュー画像を反映させる【実戦GoogleAppsScriptプログラミング講座~天空闘技場~】|HASEKATSU|note</a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>以上です。<br /> 入門(何も知らない段階)〜初級(基礎を理解する段階)〜中級(実際に制作する段階)に到るまで、これらの教材とブログ記事で学べばかなりいいところまで行けると思います。<br /> 自分がこれまで学ばせていただいた感謝の意も込めて。ありがとううう(そしてこれからもよろしくお願いします)</p> Massa tag:crieit.net,2005:PublicArticle/15714 2020-02-11T00:26:28+09:00 2020-07-13T22:02:25+09:00 https://crieit.net/posts/GAS-API-ID 【GAS】コードにAPIトークンやIDのベタ書きを避ける!(プロパティサービスの活用) <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p><a href="https://crieit.net/posts/GitHub-GAS">GASでの開発でGithubリポジトリと連携する方法について</a>書きましたが、リポジトリにプッシュする前に、一般に公開しないほうが良い情報が入っていないか気をつける必要があります。例えば、</p> <ul> <li>APIトークン</li> <li>メールアドレス</li> <li>なんかのパスワード</li> <li>特定のIDやURL</li> </ul> <p>これまでコードにLINE messanger APIのチャンネルアクセストークンや自身のメールアドレス、LINEのIDなどをベタ書きしており、Githubのリポジトリにアップする際にどうするのが正しいのか悩みました。リポジトリをprivateに設定すれば大丈夫かな?とかやってみたんですが、なんか落ち着かないしスマートじゃない気が。</p> <p>調べてみると、どうやらGASでは「プロパティ」として公開したくない情報を管理できるやり方があるようです。<br /> またメンターさんに教えていただいたところによると、一般的には秘匿情報を</p> <ul> <li>プロパティファイルに記述する</li> <li>環境変数に入れる</li> </ul> <p>とするのが対策になってくるようです。どの言語でも関係なくこの考え方は理解しておく必要がありそうですね(個人の趣味レベルでもセキュリティ意識は大事だと思う)。</p> <p>というわけで今回はGASのプロパティサービスを活用することで対応してみたので、それについてまとめてみました。</p> <h2 id="GASのプロパティサービスとは"><a href="#GAS%E3%81%AE%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A8%E3%81%AF">GASのプロパティサービスとは</a></h2> <p><strong>イメージ</strong></p> <ul> <li>APIトークンやメールアドレスなど一般に公開したくない情報をコードに書く必要がある!</li> <li>そんな時にコードにこれらの情報をベタ書きせず別の場所(プロパティストア)に入れておいて、コードではそこから情報を取り出す命令だけ書いておく(プロパティサービス)。</li> <li><p>これによって隠したい情報を見れないようにして安全に管理できるよ!</p> <p>用語<br /> ざっくり解説</p> <p>プロパティストア<br /> IDなどの公開したくない情報を格納しておく場所のこと。情報に名前をつけて格納しておくことができる。権限がないと中身を見ることはできない。</p> <p>プロパティサービス<br /> プロパティストアに格納した情報をGASスクリプトから取り出したり編集したりすることができる仕組み。PropatiesServiceやPropertiesなどをコードに書くことで色々できる。</p></li> </ul> <p>誤解を恐れずわかりやすさ重視でまとめるとこんなイメージです。<br /> (詳細は<a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-property-store/">参照リンク1</a>がとてもわかりやすく大いに参考にさせていただきました)</p> <p>ちなみにプロパティストアにも「スクリプトプロパティ」「ユーザープロパティ」「ドキュメントプロパティ」と3種類ありますが、詳しくはここでは割愛。初めのうちは「スクリプトプロパティ」だけを考えて大丈夫でしょう。</p> <h2 id="実装手順"><a href="#%E5%AE%9F%E8%A3%85%E6%89%8B%E9%A0%86">実装手順</a></h2> <p>具体的に実装してみます。とってもかんたん。</p> <h3 id="1.問題のコード"><a href="#%EF%BC%91%EF%BC%8E%E5%95%8F%E9%A1%8C%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89">1.問題のコード</a></h3> <p>例えばこういうコード。</p> <pre><code>var ACCESS_TOKEN = "hogehoge1234......"; var calendar = CalendarApp.getCalendarById("hogehoge"); </code></pre> <p>このAPIトークンの中身(hogehoge1234......の部分)はGithubに公開したりブログに書いちゃったりすると誰かに使われてしまう可能性がありそうです。<br /> またカレンダーID(hogehoge)も個人情報なので出来れば隠したいですね。</p> <h3 id="2.プロパティを登録する"><a href="#%EF%BC%92%EF%BC%8E%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B">2.プロパティを登録する</a></h3> <p>詳細は<a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-property-store/">参照リンク1</a>を参考に。</p> <p>(1)GASのスクリプトエディタのメニューで、「ファイル」>「プロジェクトのプロパティ」を開く<br /> (2)「スクリプトのプロパティ」タブを選択<br /> (3)「+行を追加」をクリック<br /> (4)左の欄に値を取り出す時に使う名前(キー)、右の欄にAPIトークンなどの値を入れて「保存」をクリック</p> <p><img width="734" alt="スクリーンショット 2020-02-08 22.14.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499640/cfa64e88-4e4b-eed3-a126-069850ee2b9d.png"></p> <p>この名前の付け方は何でも良いのですが、一目で何を意味しているのかわかる名前にするのが良いですね。<br /> ここでは「アクセストークン」と「カレンダーID」をそれぞれ登録しておきましょう(参考画像はちょっと違うプロパティを登録してますがすんません)。</p> <p>※(余談)変数に使用する記号やキャメルケースorスネークケースなどあるので、一貫した命名規則を簡単に学んでおくと名付けで迷うことが無くなるかと思います。<br /> 僕はJavascriptの命名規則の一つとしてざっくり「変数名はキャメルケース」「定数名はスネークケース」と学んだので、今回のプロパティは定数と考えてスネークケースで名付けてあります(詳しくは調べてみてね)。</p> <h3 id="3.登録したプロパティをコードに記述する方法"><a href="#%EF%BC%93%EF%BC%8E%E7%99%BB%E9%8C%B2%E3%81%97%E3%81%9F%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AB%E8%A8%98%E8%BF%B0%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95">3.登録したプロパティをコードに記述する方法</a></h3> <p>詳細は<a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-properties-script-property/">参照リンク2</a>を参考に。</p> <p>プロパティストアの登録が終わったら、問題のコードを書き換えてみます。</p> <pre><code>var ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("ACCESS_TOKEN"); var calendar = PropertiesService.getScriptProperties().getProperty("CALENDAR_ID"); </code></pre> <p>最初のコードと全く同じ内容になるのですが、うまく情報を隠して記述することができました!<br /> ただこれだと少し冗長なので次のようにスッキリと書くこともできます。<br /> (プロパティが2つだけだと余計長くなってる感じですが…。プロパティを複数使う場合はだいぶスッキリするはずです)</p> <pre><code>var prop = PropertiesService.getScriptProperties().getProperties(); var ACCESS_TOKEN = prop.ACCESS_TOKEN; var calendar = CalendarApp.getCalendarById(prop.CALENDAR_ID); </code></pre> <h2 id="解説・補足"><a href="#%E8%A7%A3%E8%AA%AC%E3%83%BB%E8%A3%9C%E8%B6%B3">解説・補足</a></h2> <p>最終的に書き換えたコードの解説をざっくりとしてみます。</p> <pre><code>var prop = PropertiesService.getScriptProperties().getProperties(); </code></pre> <p>まずプロパティサービスを使うためには、<code>PropertiesService</code>というクラスを使用します。<br /> 次に<code>getScriptProperties</code>というメソッドを使用して、プロパティストアに登録したスクリプトプロパティ達を取得します。そこから<code>getPropaties()</code>というメソッドを使うことで、変数<code>prop</code>にオブジェクトとしてプロパティ達をセットしています。<br /> (この<code>prop</code>には登録したプロパティ(キーと値のセット)が全部入っている、というイメージですかね)</p> <pre><code>var ACCESS_TOKEN = prop.ACCESS_TOKEN; var calendar = CalendarApp.getCalendarById(prop.CALENDAR_ID); </code></pre> <p>登録したAPIトークンやIDなどの値を実際に取り出すには、先ほど用意した<code>prop</code>変数を使って<code>prop.キー名</code>という形で自由に取り出すことができます。やったね!</p> <p>クラスやメソッドは色々あるので、詳しくは公式ドキュメントを参考に。<br /> なかなか初学者の方にとっては慣れるまではドキュメントの見方が分からず難しいかと思うので、少し慣れた段階や詰まった時に見るようにするといいかもしれません。僕もまだ苦手です。<br /> <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/properties?hl=ja">Properties Service  |  Apps Script  |  Google Developers</a></p> <h2 id="あとがきMEMO"><a href="#%E3%81%82%E3%81%A8%E3%81%8C%E3%81%8DMEMO">あとがきMEMO</a></h2> <p>どうしても独学ベースだと、セキュリティ面まで意識するのってなかなか難しいんじゃないでしょうか(自分だけ?)<br /> 今回のような「秘匿情報を別の場所に格納しておく」という視点は当たり前のことなのかもしれませんが、調べていて目からウロコでした。<br /> 今回具体的なGASのプロパティサービスの使い方を書きましたが、それだけに限らない大事な視点だなーと思ってまとめてみました。抽象的な概念と具体的な使い方をセットで覚えておくと理解が深まりますね。</p> <h3 id="参考にしたサイト"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88">参考にしたサイト</a></h3> <ul> <li><p>参照リンク1<br /> <a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-property-store/">【初心者向けGAS】プロパティストアの概要とスクリプトプロパティの編集方法</a></p></li> <li><p>参照リンク2<br /> <a target="_blank" rel="nofollow noopener" href="https://tonari-it.com/gas-properties-script-property/">【初心者向けGAS】スクリプトプロパティを操作してそのデータを取り出す方法</a></p></li> <li><p>GAS公式ドキュメントのProperties Servise<br /> <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/apps-script/reference/properties?hl=ja">Properties Service  |  Apps Script  |  Google Developers</a></p></li> </ul> Massa tag:crieit.net,2005:PublicArticle/15707 2020-02-04T12:25:53+09:00 2020-02-08T21:35:09+09:00 https://crieit.net/posts/GitHub-GAS テキストエディタとGitHubでコード管理できるGAS開発環境を作る <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>GASでの開発に取り組む中で、コードをテキストエディタで書き、Githubで進捗を管理したいなと思いました。</p> <p>普通にGASで開発する場合は、ブラウザからGoogleDriveのスクリプトエディタを起動してその中でコードを記述します。<br /> このブラウザ上で簡単に書けてすぐに実行できるという開発環境いらずなのがGASの良いところなのですが、<br /> このコードをローカルのテキストファイルとして作り、Githubを使って管理できたら良いなあと思ったわけです。</p> <p>僕の場合は<br /> - 以前に書いたコードと見比べたい時に、テキストファイルだと参照しやすい<br /> - Githubにコードを残してポートフォリオやメンターの方と共有したい<br /> - 以前Githubのプルリクを使って開発する流れを教わったので活用したい(行き当たりばったりの開発にならないように)</p> <p>そんなところから自分のやりやすいように開発環境を作ってしまえと考えてやってみた結果、<br /> 1. VSCodeでコードを作成・修正してGithubにpush<br /> 2. GASのscriptエディタでそれをpullして実行<br /> 3. 1に戻ってデバッグ</p> <p>という流れで作業をすることができるようになり、<br /> 例えば学習教材から学んだサンプルコードと見比べて吟味したりコピペしたり学びながらコードを書くのがとても便利になりました。</p> <p>正直この開発環境がベストプラクティスかどうかは怪しいですが、ノンプログラマの僕が最低限の使い方ができれば良し!という感じでやっています。<br /> 同じく最低限で使えれば良いよーという方は参考にしてみたらいただけたら嬉しいです。<br /> また、もっと良いやり方があるよーというのがあれば教えていただけると嬉しいです。</p> <h3 id="この記事の対象者"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E5%AF%BE%E8%B1%A1%E8%80%85">この記事の対象者</a></h3> <ul> <li>GASを既に使っている方</li> <li>gitやGithubの基本的な使い方もある程度理解している方</li> </ul> <h3 id="使用ツール等"><a href="#%E4%BD%BF%E7%94%A8%E3%83%84%E3%83%BC%E3%83%AB%E7%AD%89">使用ツール等</a></h3> <ul> <li>Google Chrome(ブラウザ)</li> <li>VSCode(テキストエディタ)</li> <li><p>Github<br />  <br /> GASでのgithubとの連携にはChrome拡張機能を使用します。<br /> 前提として、以下のアカウント登録やインストールなどは先に済ませておきます。それぞれのやり方はここでは割愛。</p></li> <li><p>Githubのアカウントは登録済み</p></li> <li>GitをPCにインストール済み(<code>$ git --version</code>でバージョンが出ればOK)</li> </ul> <h2 id="開発環境づくり"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%A5%E3%81%8F%E3%82%8A">開発環境づくり</a></h2> <h3 id="1.プロジェクトフォルダとファイルの作成"><a href="#%EF%BC%91%EF%BC%8E%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%81%A8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90">1.プロジェクトフォルダとファイルの作成</a></h3> <p>これから制作するプロジェクトのフォルダと、コードを記述するファイルを作っておきます。</p> <p>プロジェクトフォルダはどこに作っても大丈夫です(試しに作るならデスクトップで良いでしょう)。<br /> そしてその中に<code>main.js</code>という空のテキストファイルをjs拡張子で作成しておきます。<br /> (実際には拡張子は<code>.gs</code>なんですが、ここは<code>.js</code>に。理由は後ほど)</p> <p>※(余談)プロジェクトフォルダの名前や保存先は自由に決めて問題ないですが、あまり階層を深くしないのと、プロジェクトは一箇所(僕の場合は<code>Documents/Projects</code>)にまとめておき、その中にプロジェクトごとにフォルダを作るのが便利かなって思います。</p> <h3 id="2.ターミナルでgitの初期設定とGithubとの連携"><a href="#%EF%BC%92%EF%BC%8E%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%81%A7git%E3%81%AE%E5%88%9D%E6%9C%9F%E8%A8%AD%E5%AE%9A%E3%81%A8Github%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA">2.ターミナルでgitの初期設定とGithubとの連携</a></h3> <p>ここから、<a target="_blank" rel="nofollow noopener" href="https://www.micknabewata.com/entry/github/vscode-sync-after-coding">参照リンク1</a>を大いに参考にさせて頂いてます。<br /> ターミナル(Mac)を起動してコマンドを打ち込んでいっても良いのですが、せっかくなのでVSCodeのターミナル機能を使ってみます。</p> <p>VSCodeを開いたら、「表示」タブから「外観>パネルを表示>ターミナル」もしくは「ターミナル」タブから「新しいターミナル」で開けます。これでターミナルも別窓にならないのでとても便利。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499640/97122f54-6533-f932-9c12-5987a0a0bca7.png" alt="スクリーンショット 2020-02-04 11.52.51.png" /></p> <p>さて、このターミナルにコマンドを打ち込んでいって、先ほど作成したプロジェクトフォルダに入ります。<br /> フォルダを作った場所によって違いますが、僕の場合は<code>Documents/Projects/(プロジェクト名)</code>がプロジェクトフォルダなので、</p> <pre><code>$ cd Documents $ cd Projects $ cd (プロジェクト名) </code></pre> <p>次にgitの初期化を行い</p> <pre><code>$ git init </code></pre> <p>GitHubとの連携のための初期設定を行っていきます。</p> <pre><code>$ git config --global user.name "ユーザー名を入力" $ git config --global user.email "メールアドレスを入力" </code></pre> <p>ダブルクォーテーション("")の中には、自分のGithubにアカウント登録しているユーザー名とメールアドレスを入力するようにします。エラーが出ないようなら成功。</p> <h3 id="3.Githubでリポジトリ作成"><a href="#%EF%BC%93%EF%BC%8EGithub%E3%81%A7%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E4%BD%9C%E6%88%90">3.Githubでリポジトリ作成</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/">GitHub</a>にログインして<code>New</code>ボタンからリポジトリを作っておきます(ここでReadMeを作っておくと良いようですが僕はすっ飛ばしてしまいました)。</p> <p>リポジトリが完成したら、とりあえずHTTPSを選択(下の画像の赤丸)<br /> <code>…or push an existing repository from the command line</code><br /> の下の2行のコマンドをコピーしておきます。一番右のマークをクリックしたら一発でコピーできます。<br /> (下の画像の赤四角の部分)<br /> <img width="1025" alt="スクリーンショット 2020-01-30 21.54.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499640/b9b6e1da-d0f4-f242-53b6-dc89e3e4ae52.png"></p> <p>※(余談)ほんとはSSHを選択して鍵の作成や設定をしておくと安全だったりするっぽいのですが今回はHTTPSで。もうちょっと勉強しておきます。</p> <h3 id="4.リモートリポジトリの登録・コミットとプッシュ"><a href="#%EF%BC%94%EF%BC%8E%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E7%99%BB%E9%8C%B2%E3%83%BB%E3%82%B3%E3%83%9F%E3%83%83%E3%83%88%E3%81%A8%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5">4.リモートリポジトリの登録・コミットとプッシュ</a></h3> <p>ターミナルに戻り、一旦今の状態でコミット。<br /> コミットメッセージは最初はとりあえず"First Commit"とでもしておきます。</p> <pre><code>$ git add . $ git commit -m "First Commit" </code></pre> <p>次に、ローカルディレクトリにリポートリポジトリを登録します。<br /> 先ほど作成したGithubリモートリポジトリからコピーした2行のコマンドを入れます。</p> <pre><code>$ git remote add origin https://github.com/アカウント名/リポジトリ名.git $ git push -u origin master </code></pre> <p>するとユーザーネームとパスワードを聞かれるので、Githubのアカウントに使用しているものを入力します。</p> <pre><code>(上のコマンドを入力後の応答) Username for 'https://github.com': (Githubのユーザーネームを入力) Password for 'https://ユーザーネーム@github.com': (Githubのパスワードを入力) </code></pre> <p>もしパスワード入力に失敗したりしても、慌てずに<code>$ git push -u origin master</code>と入れれば再度入力できます。</p> <p>ユーザーネームとパスワード入力に成功すれば、見事リモートリポジトリの登録完了!</p> <h3 id="5.Chrome拡張機能の導入・GASでGithubと連携"><a href="#%EF%BC%95%EF%BC%8EChrome%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%81%AE%E5%B0%8E%E5%85%A5%E3%83%BBGAS%E3%81%A7Github%E3%81%A8%E9%80%A3%E6%90%BA">5.Chrome拡張機能の導入・GASでGithubと連携</a></h3> <p>次にGASのスクリプトエディタ側でGithubと連携し、push/pullをできるようにします。<br /> これにはChromeの拡張機能である「Google Apps Script GitHub アシスタント」を利用します。<br /> この導入には<a target="_blank" rel="nofollow noopener" href="https://qiita.com/20731057hh/items/7f76f9e53e9da5c85ae9">参照2</a>を参考にさせていただきました。</p> <p>導入は簡単で、まず<a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/category/extensions?hl=ja">Chrome ウェブストア</a>から「Google Apps Script GitHub アシスタント」を検索してインストール(Chromeに追加)します。<br /> 次にGASのスクリプトエディタを開いて、<code>Login SCM</code>をクリックして、<code>username</code>と<code>password</code>を入力すればGithubと連携が完了です(<code>Repository</code>が選択可能な状態になっていればOKです)。<br /> <img width="844" alt="スクリーンショット 2020-01-31 17.21.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499640/7c119fa4-470c-9fe8-17f4-383a3436112b.png"></p> <h3 id="6.GASでいったんpullしてみる"><a href="#%EF%BC%96%EF%BC%8EGAS%E3%81%A7%E3%81%84%E3%81%A3%E3%81%9F%E3%82%93pull%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">6.GASでいったんpullしてみる</a></h3> <p><code>Repository</code>タブから今回作成したGithubリポジトリを選択して、<br /> その右のほうにある⬇︎ボタンをクリックしてpullしておきます。<br /> <strong>(⬇︎がpull、⬆︎がpush)</strong></p> <p>ただ、そのままpullしようとしても上手くいきません。上部に「<code>There is nothing to pull</code>」とアラートが出てしまいます。</p> <p><code>Repository</code>の右の方にある歯車マークをクリックして、<code>File type to sync</code>の項目を<code>.js</code>に変更します。<br /> これはテキストファイルを作った時に<code>js</code>拡張子で作っていたためです。こうすることで<code>gs</code>ファイルとして読み込むことができます。</p> <p>これにより⬇︎をクリックするとちゃんとpullできるようになっています。</p> <p>以上で設定は完了です!わーい!</p> <h2 id="開発の流れ"><a href="#%E9%96%8B%E7%99%BA%E3%81%AE%E6%B5%81%E3%82%8C">開発の流れ</a></h2> <p>以降、</p> <ol> <li>VSCodeでコードを作成・修正してGithubにpush</li> <li>GASのscriptエディタでそれをpullして実行</li> <li>1に戻ってデバッグ</li> </ol> <p>を繰り返して開発を進めていきます。</p> <p>VSCodeでテキストファイルを読み込んで、実際にコーディングしていきましょう。<br /> ある程度コードを書いていき、ここらでGASで実行してみたいなーと思ったら、ターミナルでプロジェクトフォルダに入ってることを確認してから、</p> <pre><code>$ git add . $ git commit -m "(コミットメッセージを入力)" $ git push </code></pre> <p>続いてGASのスクリプトエディタ側で、⬇︎クリックでpullして、関数の実行なりアプリ公開なりしてみてください。<br /> しっかり「テキストエディタ&Github&GASのスクリプトエディタ」が連携しているはずです!!</p> <p>※(余談)コミットの管理とかプッシュはVSCodeの「ソース管理画面」なるものからクリックで直感的にできるようです。僕はターミナルを叩きたくて使ってませんが、そのやり方も<a target="_blank" rel="nofollow noopener" href="https://www.micknabewata.com/entry/github/vscode-sync-after-coding">参照リンク1</a>を参考にできそうです。</p> <h2 id="解説・補足"><a href="#%E8%A7%A3%E8%AA%AC%E3%83%BB%E8%A3%9C%E8%B6%B3">解説・補足</a></h2> <p>ただこのやり方、開発の際に注意しなければいけないのが、コンフリクトが起きないように気をつけることです。</p> <p>例えばデバッグする際にGASのスクリプトエディタ側のコードを修正してしまうと、テキストエディタで記述しているものと差異が生まれてしまいます。<br /> その都度push&pullするなら良いのですが、うっかり忘れがちなので結構面倒です。</p> <p>僕はひとまず<br /> <strong>「コードの修正は必ずテキストエディタの方で行う」</strong><br /> と決めて、GASスクリプトエディタでは基本的にコードを直接いじらないようにして回避しています(いじるとしたらあくまで関数の動作チェックのため)。</p> <p>そこがちょっと煩わしいのですが、一応今の所は不自由なく目的を果たしています。<br /> この辺上手く使いこなしている方がいれば、ぜひやり方を教えてください。</p> <p>また、テキストエディタはVSCodeに限る必要はなさそうなので、自身の慣れたテキストエディタでアレンジしてみても面白そうです。</p> <h2 id="あとがきMEMO"><a href="#%E3%81%82%E3%81%A8%E3%81%8C%E3%81%8DMEMO">あとがきMEMO</a></h2> <p>今回のやり方はさほど手間もかからずに1時間程度でできるし、自分の使い慣れているツール・やり方に落とし込む作業というのは、ハマればとても楽しいものですね。</p> <p>gitでのバージョン管理やGithubはこれまでも使っていたものの、調べて書いてある通りにやっていただけで実際によくわかっていなかったので、これを機に理解が深まった気がします。</p> <h3 id="参考にしたサイト"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88">参考にしたサイト</a></h3> <ul> <li><p>参照リンク1<br /> <a target="_blank" rel="nofollow noopener" href="https://www.micknabewata.com/entry/github/vscode-sync-after-coding">【初心者向け】Visual Studio Codeで書いたコードをGitHubに公開する方法 - 鍋綿ブログ</a></p></li> <li><p>参照リンク2<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/20731057hh/items/7f76f9e53e9da5c85ae9">GASをGitHubで簡単管理可能なChrome拡張機能 ~導入時に躓いたこと~ - Qiita</a></p></li> </ul> Massa tag:crieit.net,2005:PublicArticle/15697 2020-01-24T17:37:21+09:00 2020-01-24T18:25:11+09:00 https://crieit.net/posts/GAS-LINE 【GAS】家族で使えるゴミ出しリマインダ LINEチャットボットを作った <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>日常のゴミ出しって難題ですよね。</p> <p>「今日燃えるゴミの日なのに、ゴミ出し忘れた!!」<br /> 「プラごみ出し忘れて今日で1ヶ月…また2週間後まで出せない…」</p> <p>いや、できるのが普通当たり前のことなのかもしれないのですが、自分のような日常生活に支障をきたす程度の面倒くさがり+曜日感覚のない人間にとっては、ゴミを定期的に出すというのはかなりのハードルです。<br /> 一人暮らしの時にはアパートの入り口は基本ゴミの山。<br /> 結婚してからも自分の力でゴミ出しができた試しがなく妻には頭が上がらない。</p> <p>男女問わずこんな<del>ダメ人間</del>多忙なビジネスパーソンが多いと僕は信じています…。</p> <p>ともあれ、そんなやっかいなゴミ出し問題を解決するために、家族や夫婦で活用できるアプリを制作しました。</p> <p><em>「ゴミ出し日の前日にリマインドしてくれるLINEチャットボット」</em></p> <p>一人暮らしで活用してもよし、旦那さんが奥さんのために率先してゴミ出ししてもよし、夫婦で共有してもよし、子供にゴミ出しを意識づけさせるために使ってもよし。</p> <h2 id="使用した技術"><a href="#%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E6%8A%80%E8%A1%93">使用した技術</a></h2> <ul> <li>GAS(Google Apps Script)</li> <li>LINE Messanger API</li> </ul> <h2 id="1.チャットボット詳細"><a href="#%EF%BC%91%EF%BC%8E%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%83%9C%E3%83%83%E3%83%88%E8%A9%B3%E7%B4%B0">1.チャットボット詳細</a></h2> <p>改めて、このチャットボットで出来ることはとてもシンプルで、このLINE botアカウントと友達になっておくことで<br /> 「ゴミ出し日の前日夜9時にリマインダが送られてくる」<br /> というものです。ただそれだけ。</p> <p>曜日によって送る情報を変えることができるので、自分の地域のゴミ収集曜日を入れておくことでそれに応じてリマインドしてくれます。</p> <p><em>↓いろいろ通知してくれるLINE bot 「にゃんこ通信」</em><br /> <a href="https://crieit.now.sh/upload_images/971c1a50bdb9a973c3b0ebc0cde519595e2aab1a5c401.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/971c1a50bdb9a973c3b0ebc0cde519595e2aab1a5c401.png?mw=700" alt="Screenshot_20200117-181309.png" /></a></p> <p><em>↓こんな感じに勝手にチャットが飛んできてお知らせしてくれます。うんこうんこ</em><br /> <a href="https://crieit.now.sh/upload_images/bbea6d5ef62c776c07a3c530541a9e015e2aab36dfdda.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/bbea6d5ef62c776c07a3c530541a9e015e2aab36dfdda.png?mw=700" alt="Screenshot_20200117-181124.png" /></a></p> <h2 id="3.色々な情報をグローバルで定義しておく"><a href="#%EF%BC%93%EF%BC%8E%E8%89%B2%E3%80%85%E3%81%AA%E6%83%85%E5%A0%B1%E3%82%92%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%81%A7%E5%AE%9A%E7%BE%A9%E3%81%97%E3%81%A6%E3%81%8A%E3%81%8F">3.色々な情報をグローバルで定義しておく</a></h2> <p>Line Developersのチャンネル作成やプッシュメッセージの登録などが必要ですが、<br /> 今回はコードのみ紹介。</p> <pre><code>//チャンネル情報・ラインユーザー情報の取得 var CHANNEL_ACCESS_TOKEN = 'ここにアクセストークンを入力'; var USER_ID_M = 'ここにラインユーザーIDを入力'; var USER_ID_L = 'ここにラインユーザーIDを入力'; </code></pre> <p>まず、チャンネルアクセストークンと、リマインダメッセージを送りたいラインのユーザーIDを変数に入れます。</p> <p>これらはプログラム中で変更することがないので定数constに入れたいなあと思うのですが、どうやらconstはグローバルでは定義できないようです(<a target="_blank" rel="nofollow noopener" href="https://teratail.com/questions/226375">参考リンク1</a>)<br /> (定数は関数内でプライベートでのみ定義)<br /> 頭に書いておきたいので変数varで定義しておきます。</p> <p>チャンネルアクセストークンと自身のLINEユーザーIDについては、LINE Developersのチャンネル情報で見れるので、それを利用します。</p> <p>ちょっと手間がかかるのが複数人でこのbotを共有する場合。<br /> 僕の場合は妻とbotを共有したいので、メッセージの送り先として妻のLINEユーザーID(ラインで友達検索に使うラインIDとは別物です)を設定しなければなりません。</p> <p>ところが、調べているとこれが簡単に取得できないことが発覚。<br /> 一旦置いておいて次項で。</p> <pre><code>//ユーザー入力項目 var weekday = ["日", "月", "火", "水", "木", "金", "土"]; var garbageCalendar = { "日":"", "月":"燃やすゴミ", "火":"", "水":"資源ゴミ", "木":"燃やすゴミ", "金":"", "土":"" }; </code></pre> <p>次にユーザー入力項目ですが、<br /> このgarbageCalendar変数に、オフジェクトでゴミ収集の曜日とゴミの種類を入れておきます。<br /> 地域のゴミ収集カレンダーから持って来ましょう。</p> <h2 id="3.他人のラインユーザーIDの取得"><a href="#%EF%BC%93%EF%BC%8E%E4%BB%96%E4%BA%BA%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BCID%E3%81%AE%E5%8F%96%E5%BE%97">3.他人のラインユーザーIDの取得</a></h2> <p>これには<a target="_blank" rel="nofollow noopener" href="https://www.pre-practice.net/2018/11/line-botuserid-groupid-roomid.html">参考サイト1</a>をそのまま活用させてもらいました。</p> <p>取得したユーザーIDをグローバル変数に入れておきます。</p> <h2 id="4.リマインダのメイン処理"><a href="#%EF%BC%94%EF%BC%8E%E3%83%AA%E3%83%9E%E3%82%A4%E3%83%B3%E3%83%80%E3%81%AE%E3%83%A1%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86">4.リマインダのメイン処理</a></h2> <p>次に、リマインダのメッセージなどを記述して送信するための<code>garbageReminder()</code>関数というものを作ります。</p> <pre><code>//リマインダのメイン処理 function garbageReminder() { //翌日の日時を取得 var date = new Date(); date.setDate(date.getDate() + 1); //翌日の曜日を取得して、ゴミの種類を取得 var dayNum = date.getDay(); var day = weekday[dayNum]; var garbageType = garbageCalendar[day]; //明日がゴミ収集日にリマインドメッセージを送信 var message = "【ゴミ出し通信】\n"; if(garbageType != ""){ message += " 明日は${day}曜日、「${garbageType}」の日です。\n出すゴミをチェックしよう!".replace('${day}',day).replace('${garbageType}',garbageType); sendToLine(message); }else{ }; } </code></pre> <p>前半では、今日の日付から曜日を取得してわちゃわちゃして翌日のゴミの種類を判別してます。</p> <p><code>new Date()</code>でまず今日の日付情報を取得できるので、その変数に<code>date.setDate(date.getDate() + 1);</code>とすることで、翌日の日付情報を取得しています。<br /> その取得した日付情報<code>date</code>に<code>getDay()</code>メソッドを使うことで曜日を取り出すことができます。曜日は数字になっているので注意。<br /> 曜日からゴミの種別を取ってくるときは、グローバルで定義しておいた配列とオブジェクトを使用します。</p> <p>後半は、メッセージを送信する処理になります。<br /> 翌日がゴミ出し曜日の場合にのみ、変数<code>message</code>に曜日やゴミの種別が入った文字列が入ります。<br /> 変数を文字列として送るときは<code>replase</code>メソッドを活用。<br /> また改行を入れたいときは<code>\n</code>(記号はバックスラッシュ)を使います。</p> <p>最後に<code>sendToLine(message)</code>という関数(次項に記述)を利用して、先ほどの変数<br /> <code>message</code>の値をラインに送信しています。</p> <h2 id="5.ラインに送信する処理"><a href="#%EF%BC%95%EF%BC%8E%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%AB%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86">5.ラインに送信する処理</a></h2> <p>前項の最後の、実際にラインに送信するための色々な情報を記述する<br /> <code>sendToLine()</code>関数を作ります。</p> <pre><code>//ラインに送信する処理 function sendToLine(message){ //色々なパラメータを設定 var url = 'https://api.line.me/v2/bot/message/multicast'; var headers = { 'Content-Type':'application/json; charset=UTF-8', 'Authorization':'Bearer ' + CHANNEL_ACCESS_TOKEN }; var data={ "to":[USER_ID1, USER_ID2], "messages":[{ "type":"text", "text":message }] }; var options = { 'method':'post', 'headers':headers, 'payload':JSON.stringify(data) }; //LINE messanger APIを叩く UrlFetchApp.fetch(url, options); } </code></pre> <p>最後の<code>UrlFetchApp.fetch(url, options);</code>がGASでAPIを叩く命令となります。<br /> かっこの中は「どこへ、どの情報を送る」というイメージです。</p> <p>そのために必要なものをその前に変数に入れて記述しています。<br /> 詳しくはLINE Developersの公式ドキュメント(<a target="_blank" rel="nofollow noopener" href="https://developers.line.biz/ja/reference/messaging-api/#send-push-message">Messaging APIリファレンス</a>)を参照。</p> <p><code>var url</code>にはHTTPリクエストを送るURLを記述します。<br /> このurlの末尾を<code>multicast</code>としていることで、送り先を複数に設定しています。<br /> この実際に送るのが<code>var data</code>の<code>to</code>に配列で複数設定しているユーザーIDになっています。</p> <p>例えばこのbotを自分だけが使用できれば良いという場合は、<br /> HTTPリクエストを送るurlの末尾を<code>push</code>にして、<code>var data</code>の<code>to</code>には自分のユーザーIDだけを指定しておけば良いですね。</p> <p>詳しい説明は省いてしまいますが、このようにLINEのmessaging APIを叩くための諸々のパラメータを<code>url</code>,<code>headers</code>,<code>data</code>, <code>options</code>に入れているわけです。</p> <p>ここまでできたら、トリガーを登録することでリマインド機能が完成です!</p> <h2 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h2> <ul> <li>LINEのユーザーIDを調べる方法をコピペで実施しているので、コードを自分の頭で書いて理解してみようと思います。</li> <li>というか、そもそもLINEユーザーIDをいちいち入れなくてもいい可能性はないか…?</li> </ul> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>普段使っているラインを入り口にして色々できないかなーと考えるようになって<br /> 日常生活で欲しかったちょっとしたものを実際に作ってみた次第です。</p> <p>これに夫婦で使いたい機能をちょっとずつ付け足していきたいと思っていて、<br /> 家計の金銭管理とか、買い物リストとか、直接ラインでやりとりするのは少し手間だったり精神的負担があったり…というようなほんと些細なことをツールで出来るようにしたいなーと思っています。</p> <p>いずれリッチメニューなんかも作れるようになりたいですね。</p> <p>夫婦中がちょっとだけアップするアプリを目指します。笑</p> <h2 id="参考にしたサイト"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88">参考にしたサイト</a></h2> <ul> <li>参考1<br /> <a target="_blank" rel="nofollow noopener" href="https://teratail.com/questions/226375">Google Apps Script - Google Apps Scriptでグローバルな定数を定義できない|teratail</a></li> <li>参考2<br /> <a target="_blank" rel="nofollow noopener" href="https://www.pre-practice.net/2018/11/line-botuserid-groupid-roomid.html">Google Apps Script試行錯誤Blog: LINE BOTでuserId, groupId, roomIdを取得したい</a></li> <li>LINE Developers公式ドキュメント(プッシュメッセージ)<br /> <a target="_blank" rel="nofollow noopener" href="https://developers.line.biz/ja/reference/messaging-api/#send-push-message">Messaging APIリファレンス</a></li> </ul> <h2 id="MEMO"><a href="#MEMO">MEMO</a></h2> <p>Githubに全体のコードをあげています。<br /> 書き方はあまりスマートでない部分もあるかと思うので、その点はお手柔らかに。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/Massasquash/LINEbot-for-life/blob/master/push.gs">LINEbot-for-life/push.gs at master · Massasquash/LINEbot-for-life · GitHub</a></p> <p>また、LINE botのアイコンの画像はこちらのものを使用させていただいてます。感謝。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.sozailab.jp/sozai/detail/14638/">勉強猫 | 無料イラスト素材|素材ラボ</a></p> Massa tag:crieit.net,2005:PublicArticle/15457 2019-10-07T09:13:01+09:00 2019-10-07T09:31:08+09:00 https://crieit.net/posts/GAS-5d9a830d875a5 GASでいいねしたツイートをいいねの数とリツイートの数と一緒に記録するやつ <p>特定のユーザーのいいねしたツイート一覧を、ツイートに付いたいいねの数とリツイートの数も一緒に記録したくなって作りました。トリガー機能を使うと、定期的にいいねが記録されるようになります。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yanagiha/items/40c7f0cc140eace11bd8">GASで自分のツイートを取得してスプレッドシートに記録するやつ<br /> </a><br /> この記事も合わせて読むとわかりやすいかもしれません。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406061/d311791c-7655-7d87-1dba-cb3ee543444d.png" alt="スクリーンショット 2019-10-07 3.40.44.png" /></p> <p>このように記録されます。</p> <pre><code class="javascript"><br /><br />function getFav(){ var service = twitter.getService(); var json = service.fetch("https://api.twitter.com/1.1/favorites/list.json?screen_name=いいねを取得したいアカウントのscreen name&count=100"); var array = JSON.parse(json); var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName('いいねを記録したいシートの名前'); var lastRow = sheet.getLastRow() +1; var lastId = sheet.getRange("F2").getValue(); for(var i = 0; i <= array.length -1; i++) { var int = parseInt(i); if(i === 0){ var recId = array[int]["id"]; sheet.getRange("G2").setValue(recId); } var id = array[int]["id"]; if(id > lastId){ var time = array[int]["created_at"]; var userId = array[int]["user"]["id_str"]; var json = service.fetch("https://api.twitter.com/1.1/users/show.json?user_id="+userId+"&include_entities=false"); var array2 = JSON.parse(json); var screenName = array2["screen_name"]; var text = array[int]["text"]; var favorite_count = array[int]["favorite_count"]; var retweet_count = array[int]["retweet_count"]; sheet.getRange(lastRow,1).setValue(time); sheet.getRange(lastRow,2).setValue(screenName); sheet.getRange(lastRow,3).setValue(text); sheet.getRange(lastRow,4).setValue(favorite_count); sheet.getRange(lastRow,5).setValue(retweet_count); sheet.getRange(lastRow,6).setValue(id); } lastRow = lastRow + 1; } </code></pre> <p>いいねを100件取得して、idが以前記録したツイートより大きければ記録する…といった感じです。<br /> 何かあったら気軽にコメントください。</p> yanagiha tag:crieit.net,2005:PublicArticle/15456 2019-10-07T09:10:43+09:00 2019-10-07T09:10:43+09:00 https://crieit.net/posts/GAS GASで自分のツイートを取得してスプレッドシートに記録するやつ <p>急にツイ消ししたくなった場合に備えて、自分のツイートをグーグルスプレッドシートに記録しておくことにしました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/golyat/items/6b430986dbfd8dd1c239">GASでTwitterの投稿とタイムライン取得</a><br /> こちらの記事を参考にして書いてみました。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406061/43b4b722-3b85-bb5e-be81-133be3f4d987.jpeg" alt="20190828150010.jpg" /><br /> 画像のように記録されます。</p> <pre><code class="javaScript"><br /> var twitter = TwitterWebService.getInstance( '**********', // 作成したアプリケーションのConsumer Key '**********' // 作成したアプリケーションのConsumer Secret ); // 認証を行う(必須) function authorize() { twitter.authorize(); } // 認証をリセット function reset() { twitter.reset(); } // 認証後のコールバック(必須) function authCallback(request) { return twitter.authCallback(request); } function getMyTweets() { var service = twitter.getService(); var json = service.fetch("https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=ツイートを取得したいユーザーのid&count=30"); var array = JSON.parse(json); var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName('ツイートを保存したいシートの名前'); var lastRow = sheet.getLastRow() +1; var lastId = sheet.getRange("D2").getValue(); for(var i = 0; i <= array.length -1; i++) { var int = parseInt(i); if(i === 0){ var recId = array[int]["id"]; sheet.getRange("D2").setValue(recId); } var id = array[int]["id"]; if(id > lastId){ var time = array[int]["created_at"]; var text = array[int]["text"]; sheet.getRange(lastRow,1).setValue(time); sheet.getRange(lastRow,2).setValue(text); sheet.getRange(lastRow,3).setValue(id); } lastRow = lastRow + 1; } } </code></pre> <p>自分のツイートを最新のやつから30件まで取得して、idが以前取得したツイートより大きければスプレッドシートに書き込む……という感じです。</p> <p>ここ違うよ〜とかもっと良い書き方あるよ〜って場合は気軽にコメントください。</p> yanagiha tag:crieit.net,2005:PublicArticle/14859 2019-03-06T00:48:44+09:00 2019-03-28T12:23:25+09:00 https://crieit.net/posts/Slack-GAS SlackのファイルをGASで自動削除する <p>Unityゲーム開発者ギルドというコミュニティを運営しているんだけど、<br /> 無料版で使っているSlackのファイルストレージがギリギリになってた。</p> <p><a href="https://crieit.now.sh/upload_images/9ba5a50ad15a3c66d2ef0e24bb8b8c485c7e990fd0111.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9ba5a50ad15a3c66d2ef0e24bb8b8c485c7e990fd0111.png?mw=700" alt="スクリーンショット 2019-03-06 0.28.01.png" /></a></p> <blockquote> <p>Unityギルドについてはこちら -> https://twitter.com/naichilab/status/1061929632037892096</p> </blockquote> <p>このままじゃファイルアップロードできなくなっちゃうので、古いファイルは自動削除されるようにした。<br /> とりあえず30日以上経過したファイルは問答無用で消していこう。</p> <p>(Slackのメッセージも今のペースだと2週間たらずで消えちゃうから30日経過したら削除で十分だろうと判断)</p> <p>GASで下記スクリプトを作成。プロジェクトのプロパティでレガシートークンをセットして1時間トリガーを仕掛けた。</p> <blockquote> <p>1時間ごとに起動して、30日以上経過したファイルを10件検索&削除。</p> </blockquote> <p>削除後の反映は最大48時間かかるらしいので様子見〜。</p> <pre><code class="js">function deleteOldFiles() { var res = filesList(30, 10); if(res.files.length == 0){ Logger.log("削除対象ファイルは見つかりませんでした。"); return; } res.files.forEach(function(file){ Logger.log("削除します。 ID=" + file.id + ", Size=" + file.size); filesDelete(file.id) Utilities.sleep(50);//sleep 50msec }); } //ref. https://api.slack.com/methods/files.list function filesList(days, count){ var params = { token: PropertiesService.getScriptProperties().getProperty("SLACK_ACCESS_TOKEN"), ts_to: elapsedDaysToUnixTime(days), count: count } return execute('files.list', params); } //ref. https://api.slack.com/methods/files.delete function filesDelete(id){ var params = { token: PropertiesService.getScriptProperties().getProperty("SLACK_ACCESS_TOKEN"), file: id } return execute('files.delete', params); } function execute(apiName, params){ var options = { 'method': 'POST', 'payload': params } var res = UrlFetchApp.fetch('https://slack.com/api/' + apiName,options); return JSON.parse(res.getContentText()); } function elapsedDaysToUnixTime(days){ var date = new Date(); var now = Math.floor(date.getTime()/ 1000); // unixtime[sec] return now - 8.64e4 * days + '' // 8.64e4[sec] = 1[day] 文字列じゃないと動かないので型変換している } </code></pre> naichi