モバイルアプリエンジニアの山下です。
LCLでは業務や情報収集の中で定期的な作業を行う際にGoogle Apps Script(以下、GAS)を利用した自動化をしています。
GASとは、クラウド上でスクリプトを実行できるサービスです。スプレッドシートをはじめ、Googleが提供している色々なサービスと連携することができます。
又、外部のサービスでも提供されているAPIを利用して操作することができるため幅広い用途で使えます。
そして、今回は以下の3つの活用方法を紹介したいと思います。
- Gmailの本文から値を取得してスプレッドシートに書き込む
- Google CalendarのスケジュールをChatWorkに投稿する
- 毎朝、前日分のRSSをChatWorkに投稿する
Gmailの本文から値を取得してスプレッドシートに書き込む
この操作は、Fabricのデイリーサマリーを集計するスクリプトで扱っています。
Fabricの管理画面では、Crashlyticsなどの情報が過去90日間しか遡れないためスプレッドシートへ書き溜めるようにしています。
スクリプトの処理の流れは以下です。事前にGmailで対象のメールにラベルを付ける必要があります。
- Gmailから未読のFabricのデイリーサマリーのメールを取得
- メールから日付と本文中にある特定の値を取得
- 日付と値をスプレッドシートの最後に書き込み
- 取得したメールを既読に変更
それでは実装方法を紹介していきます。
実装
1. Gmailから未読のFabricのデイリーサマリーのメールを取得
Gmailを操作するにはGmailAppクラスを使います。
スレッドを取得するメソッドはいくつか存在しますが、今回は未読を条件に含めたいためsearch
メソッドを使います。
var start = 0; // 参照を始めるポジション var max = 20; // 取得するメールの最大件数 var threads = GmailApp.search('label:Fabric is:unread', start, max);
取得したスレッドは配列になっているため、ループを回して取得します。
スレッドからはgetMessages
メソッドでメッセージ(メール)を取得できます。
for( var n in threads ) { var thread = threads[n]; var messages = thread.getMessages(); // メッセージを取得 for (m in messages) { var message = messages[m]; } }
2. メールから日付と本文中にある特定の値を取得
メッセージも配列で渡されるので同じようにループを回して取得します。
メッセージに対してはgetPlainBody
メソッドでHTML要素を除いたbodyの文字列を取得できるため、今回はこれを利用して値を取得します。
Class GmailMessage | Apps Script | Google Developers
var message = messages[m]; var date = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyy/MM/dd'); var body = message.getPlainBody();
取得したbodyの文字列から正規表現で特定の値を抜き出します。
抜き出したい文字列は以下のようになっています。
Crash-Free Users: Yesterday: 99.8% Delta: ▲0.1%
上の形に対応した処理をメソッド化して、以下のように取得します。
function myFunction() { ... for (m in messages) { ... var body = message.getPlainBody(); // 正規表現で値を取得 var crashFreeUsers = fetchData(body, 'Crash-Free Users:'); } } function fetchData(string, prefix) { var reg = new RegExp(prefix + '\\W+Yesterday:\\W+.*?\\d.+'); var data = string.match(reg)[0] .replace(prefix, '') .replace('\\W+', '') .replace('Yesterday:', '') .replace('\\d.+', '') .replace(/[ \n\r]/g, ''); return data; }
3. 日付と値をスプレッドシートの最後に書き込み
ここまでで日付と値は取得できたので次にスプレッドシートへ書き込みます。
まず、取り扱うスプレッドシートを取得します。スプレッドシートの取得方法には2パターンあります。
スプレッドシートがGASのスクリプトに紐付いているパターン
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet(); var sheet = spreadSheet.getSheetByName('sheet1');
スプレッドシートがGASのスクリプトに紐付いていないパターン
var sheetId = "<スプレッドシートのID>"; var spreadSheet = SpreadsheetApp.openById(sheetId); var sheet = spreadSheet.getSheetByName('sheet1');
本記事では触れませんが別途ツールを使うことでGASもバージョン管理ができます。しかし、そのツールを使う場合は、対象のスクリプトが他サービスと紐付いていない必要があるため、後々管理をする予定であれば紐付けない実装することをおすすめします。
話が逸れましたが、次にスプレッドシートの最後の行に書き込みを行います。
これはappendRow
メソッドを使うことで最後の行の計算を行わず、純粋に追記することができます。
... for (m in messages) { ... sheet.appendRow([ date, crashFreeUsers, ]) }
4. 取得したメールを既読に変更
最後にメッセージに対してmarkRead
メソッドを呼んで、メールを既読にします。
... for (m in messages) { ... message.markRead(); }
全体コード
全体コードを載せておきます。必要な箇所を書き換えればそのまま使用できます。
var SHEET_ID = "<スプレッドシートID>"; function myFunction() { var sheet = getSheet('<シート名>'); var threads = getThreads('label:Fabric is:unread'); // "Fabric"ラベルが付与された未読メール取得 var length = threads.length; // スレッド数を取得 // 昇順に登録するために反転 threads.reverse(); for( var n in threads ) { var thread = threads[n]; var messages = thread.getMessages(); // メッセージを取得 for (m in messages) { var tmpArr = new Array(); var message = messages[m]; var date = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyy/MM/dd'); var body = message.getPlainBody(); var dailyActiveUsers = fetchData(body, 'Daily Active Users:'); var dailyNewUsers = fetchData(body, 'Daily New Users:'); var monthlyActiveUsers = fetchData(body, 'Monthly Active Users:'); var crashFreeUsers = fetchData(body, 'Crash-Free Users:'); var sessions = fetchData(body, 'Sessions:'); var timeInAppPerUser = fetchData(body, 'Time in App per User:'); /* * スプレッドシートに挿入するデータ列数 * [ * Date, * Daily Active (k), * Daily New Users, * Monthly Active Users, * Crash-free Users (%), * Total Sessions (k), * Time in App per User (m) * ] */ sheet.appendRow([ date, dailyActiveUsers, dailyNewUsers, monthlyActiveUsers, crashFreeUsers, sessions, timeInAppPerUser ]) // メールを既読に変更 message.markRead(); } } } // 正規表現で値を取得 function fetchData(string, prefix) { var reg = new RegExp(prefix + '\\W+Yesterday:\\W+.*?\\d.+'); var data = string.match(reg)[0] .replace(prefix, '') .replace('\\W+', '') .replace('Yesterday:', '') .replace('\\d.+', '') .replace(/[ \n\r]/g, ''); return data; } // メールのスレッドを取得 function getThreads(filter) { var start = 0; // 参照を始めるポジション var max = 20; // 取得するメールの最大件数 return GmailApp.search(filter, start, max); // 今回は未読を条件に含めたいためsearchメソッドを使う } // スプレッドシートを取得 function getSheet(sheetName) { var spreadSheet = SpreadsheetApp.openById(SHEET_ID); return spreadSheet.getSheetByName(sheetName); }
最後にトリガーをセットすることで、毎日自動でスプレッドシートが更新されます。
Google CalendarのスケジュールをChatWorkに投稿する
エンジニアチームでは、リモート勤務や半フレックスタイム制で始業時間を柔軟に調整できる環境となっています。そのため、対面での朝会は行わず、当日のタスクやスケジュールは始業後にChatWorkの専用のチャンネルへ投稿するようにしています。
投稿するためには毎朝当日のタスクをAM/PMに分けて書き出す必要があるのですが、私はGoogle Calendarに数日後のタスクを予め書き込んでいるため、それを元に自動で投稿するようにしています。
スクリプトの処理の流れは以下です。
- Google Calendarから当日のスケジュールを取得
- 内容を確認(MTGや業務時間外スケジュールを除く)
- ChatWork用に投稿内容を加工
- ChatWorkに投稿
事前準備
ChatWorkを利用する方はAPIトークンが必要です。
取得方法
APIトークン発行ページを開き、パスワードを入力後に表示されるAPI Tokenをコピーします。
実装
1. Google Calendarから当日のスケジュールを取得
Google Calendarを操作するにはCalendarAppクラスを使います。
getEventsForDay
メソッドで指定日のイベントを取得できます。
var today = new Date(); var calendar = CalendarApp.getCalendarById('<カレンダーID>'); var events = calendar.getEventsForDay(today);
2. 内容を確認(MTGや業務時間外スケジュールを除く)
MTGや在宅勤務などのイベントは共有しないため除外します。
上で取得したイベントは配列になっているため、ループを回して個別に取得します。
getTitle
メソッドでイベントのタイトルを取得し、除外キーワードが含まれているか確認します。
Class CalendarEvent | Apps Script | Google Developers
function myFunction() { ... // イベントの数だけ繰り返し for(var i = 0; i < events.length; i++) { var title = events[i].getTitle(); // 除外キーワードを含む場合は処理を飛ばす if(hasIgnoreKeyword(title)) { continue; } // do something } } // 除外キーワード判定 function hasIgnoreKeyword(title) { var ignore_keywords = ["MTG", "モブプロ", "在宅勤務"]; var i = ignore_keywords.length; while(i--) { if (title.indexOf(ignore_keywords[i]) !== -1) { return true; } } return false; }
3. ChatWork用に投稿内容を加工
ChatWorkへは、以下の形式になるように投稿します。
イベントはもちろん時系列になっているので、文字列を追加するだけです。
var amflg = true; var pmflg = true; // 当日のイベントを取得 var events = getTodayEvent(); // チャットワークに送る文字列 var body = "[info][title]本日やること[/title]" // イベントの数だけ繰り返し for(var i = 0; i < events.length; i++) { var title = events[i].getTitle(); var desc = events[i].getDescription(); var startTime = events[i].getStartTime(); var endTime = events[i].getEndTime(); // 除外キーワードを含む if(hasIgnoreKeyword(title) { continue; } // AM/PMの表記を追加 if(startTime.getHours() <= 12 && amflg) { body += "-AM-\n"; amflg = false; } else if (startTime.getHours() > 12 && pmflg) { body += "\n-PM-\n"; pmflg = false; } // 項目を追加 body += '■ ' + title + '\n'; // 詳細を追加 if(desc != '') { body += '・ ' + desc + '\n'; } } body += '[/info]';
4. ChatWorkに投稿
最後にChatWorkへ投稿します。事前準備したAPIトークンと投稿先のチャンネルIDをセットします。
var payload = { 'body': body }; var options = { 'method': 'post', 'headers': { 'X-ChatWorkToken': <APIトークン> }, 'payload': payload }; UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + <ルームID> + '/messages', options);
全体のコード
上記の処理に加え、休日と時間の判定を加えた全体コードを貼ります。
var CALENDAR_ID = '<Google Calendar ID>' var CHATWORK_TOKEN = '<APIトークン>'; var CHATWORK_ROOM_ID = <ルームID>; var IGNORE_KEYWORDS = ["MTG", "モブプロ", "在宅勤務"]; function myFunction() { if(isHoliday()) { return; } var amflg = true; var pmflg = true; // 当日のイベントを取得 var events = getTodayEvent(); // チャットワークに送る文字列 var body = "[info][title]本日やること[/title]" // イベントの数だけ繰り返し for(var i = 0; i < events.length; i++) { var title = events[i].getTitle(); var desc = events[i].getDescription(); var startTime = events[i].getStartTime(); var endTime = events[i].getEndTime(); // 除外キーワードを含む or 19時以降の予定は飛ばす if(hasIgnoreKeyword(title) || startTime.getHours() > 19) { continue; } // AM/PMの表記を追加 if(startTime.getHours() <= 12 && amflg) { body += "-AM-\n"; amflg = false; } else if (startTime.getHours() > 12 && pmflg) { body += "\n-PM-\n"; pmflg = false; } // 項目を追加 body += '■ ' + title + '\n'; // 詳細を追加 if(desc != '') { body += '・ ' + desc + '\n'; } } body += '[/info]'; postChatWork(body); } // ChatWorkへ投稿 function postChatWork(body) { var payload = { 'body': body }; var options = { 'method': 'post', 'headers': { 'X-ChatWorkToken': CHATWORK_TOKEN }, 'payload': payload }; UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + CHATWORK_ROOM_ID + '/messages', options); } // Google Calendarの当日のイベントを取得 function getTodayEvent() { var today = new Date(); var calendar = CalendarApp.getCalendarById(CALENDAR_ID); return calendar.getEventsForDay(today); } // 休日判定 function isHoliday() { var today = new Date(); // 土日判定 var weekInt = today.getDay(); if(weekInt <= 0 || 6 <= weekInt){ return true; } // 祝日判定 var calendarId = "ja.japanese#holiday@group.v.calendar.google.com"; var calendar = CalendarApp.getCalendarById(calendarId); var todayEvents = calendar.getEventsForDay(today); if(todayEvents.length > 0){ return true; } return false; } // 除外キーワード判定 function hasIgnoreKeyword(title) { var i = IGNORE_KEYWORDS.length; while(i--) { if (title.indexOf(IGNORE_KEYWORDS[i]) !== -1) { return true; } } return false; }
以上でトリガーを設定することで毎朝自動でチャットが投稿されます。
毎朝、前日分のRSSをChatWorkに投稿する
エンジニアはそれぞれ方法で情報収集をしていますが、チャットへ流すことで話題となり話が膨らみやすくなったり、個人の収集媒体ではキャッチアップできなかった場合に備えて情報を抑えやすい環境にしています。
RSS用のURLをスプレッドシートに書き込むことで、毎朝GASで前日分の記事を取得してChatWorkへ投稿されます。
スクリプトの処理の流れは以下です。
- スプレッドシートから読み込み
- RSS取得
- ChatWork用に投稿内容を加工
- ChatWorkに投稿
今回は対象のRSS URLに加え、APIトークンとRoom IDもsheetから取得しようと思います。
事前準備
まず、スプレッドシートに必要な情報を書き込みます。
ChatWorkを利用する方はAPIトークンが必要です。
前節でAPIトークンを取得済の方はそのまま使いまわせます。
取得方法
APIトークン発行ページを開き、パスワードを入力後に表示されるAPI Tokenをコピーします。
実装
1. スプレッドシートから読み込み
getRange
でセルを指定し、getValue
で値を取得します。
var spreadSheet = SpreadsheetApp.openById(<シートID>); var sheet = spreadSheet.getSheetByName(<シート名>); // APIトークンとRoom IDを取得 var token = sheet.getRange("B1").getValue(); var room_id = sheet.getRange("B2").getValue();
次にURLを取得します。A列5行目以降を全て読み込みます。読み込んだ後は不要な配列を削除するために一手間加えています。
var values = sheet.getRange("A" + ":" + 5).getValues(); var urls = new Array(); // 配列を整理 for (var i = 0; i < values.length; i++) { if (i >= startIndex) { if(values[i] != null && values[i] != "") { urls.push(values[i]); } } }
2. RSS取得
RSSの取得はfetchPostAtYesterday
メソッドで行います。
URLが不正だったり、アクセスに不具合が生じるとエラーになるためtry-catch文で制御します。
function myFunction() { // 前日の日付を取得 var yesterday = getYesterday(); // 前日の記事を取得 var entries = new Array(); urls.forEach(function(url) { try { var posts = fetchPostAtYesterday(url[0], yesterday)[0] } catch(e) { Logger.log(e); } if (posts != null) { entries.push(posts); } }); } /** * feedURLから記事情報を取得 * @param {String} feedUrl * @return {Array.<*>} */ function fetchPostAtYesterday(feedUrl, yesterday) { var response = UrlFetchApp.fetch(feedUrl); var rssXML = response.getContentText(); var document = XmlService.parse(rssXML); var root = document.getRootElement(); var ns_rss = XmlService.getNamespace('http://purl.org/rss/1.0/'); var ns_dc = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/'); var ns_atom = XmlService.getNamespace('http://www.w3.org/2005/Atom'); var ns_rdf = XmlService.getNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'); var rootTagName = root.getName().toLowerCase(); var entries = []; // 記事のリストを取得 switch (rootTagName) { case 'rdf': // 1.0 entries = root.getChildren('item', ns_rss); break; case 'feed': // atom entries = root.getChildren('entry', ns_atom); break; case 'rss': // 2.0 entries = root.getChild('channel').getChildren('item'); break; default: return false; } return entries.map(function(entry, index) { var title; var link; var pubDate; switch (rootTagName) { case 'rdf': // 1.0 title = entry.getChild('title', ns_rss).getText(); link = entry.getChild('link', ns_rss).getText(); pubDate = entry.getChild('date', ns_dc).getText(); break; case 'feed': // atom title = entry.getChild('title', ns_atom).getText(); link = entry.getChild('link', ns_atom).getAttribute('href').getValue(); // pubDate = entry.getChild('published', ns_atom).getText(); // 普段はこっちの方を利用する pubDate = entry.getChild('updated', ns_atom).getText(); // 今回はQiita用にupdatedを利用する break; case 'rss': // 2.0 title = entry.getChild('title').getText(); link = entry.getChild('link').getText(); pubDate = entry.getChild('pubDate').getText(); break; } // 投稿日が昨日なら var entryDate = Utilities.formatDate(new Date(pubDate), 'Asia/Tokyo', 'YYYY-MM-dd'); if (yesterday == entryDate) { return { 'title': title, 'link': link } } return ''; }).filter(function(item) { return item !== ''; }); } // 前日の日付を取得 function getYesterday() { var date = new Date(); date.setDate(date.getDate() - 1); return Utilities.formatDate(date, 'Asia/Tokyo', 'YYYY-MM-dd'); }
RSS取得の処理は以下の記事を参考にさせていただきました。
TypetalkとGoogle App Scriptを組み合わせて、RSSのBOTを作ってみる。
3. ChatWork用に投稿内容を加工
例のごとく、読みやすいように加工していきます。今回は以下のように投稿します。
// 記事が存在すればChatWorkへ投稿 if (entries.length != 0) { var body = '[info][title]' + yesterday + 'に公開された記事[/title]\n'; for(var e in entries) { body += entries[e].title; body += '\n'; body += entries[e].link; body += '\n\n'; } body += '[/info]'; }
4. ChatWorkに投稿
最後にChatWorkへ投稿します。前節を参考にしてください。
全体のコード
上記に加え、はてなブックマーク数の取得しています。
var SHEET_ID = "<シートID>"; var CHATWORK_TOKEN = ''; var CHATWORK_ROOM_ID = null; function myFunction() { var sheet = getSheet('rss'); // スプレッドシートから情報を取得 CHATWORK_TOKEN = sheet.getRange("B1").getValue(); CHATWORK_ROOM_ID = sheet.getRange("B2").getValue(); var urls = getColumValues(sheet, "A", 5); // 前日の日付を取得 var yesterday = getYesterday(); // 前日の記事を取得 var entries = new Array(); urls.forEach(function(url) { try { var posts = fetchPostAtYesterday(url[0], yesterday)[0] } catch(e) { Logger.log(e); } if (posts != null) { entries.push(posts); } }); // 記事が存在すればChatWorkへ投稿 if (entries.length != 0) { var body = '[info][title]' + yesterday + 'に公開された記事[/title]\n'; for(var e in entries) { body += entries[e].title; // はてブ数を取得 var hatena_count = fetchHatenaBookmarkCount(entries[e].link) if(hatena_count != '') { body += " / はてブ数: " + hatena_count; if(hatena_count >= 50) { body += "⭐"; } } body += '\n'; body += entries[e].link; body += '\n\n'; } body += '[/info]'; postChatWork(body); } } /** * feedURLから記事情報を取得 * @param {String} feedUrl * @return {Array.<*>} */ function fetchPostAtYesterday(feedUrl, yesterday) { var response = UrlFetchApp.fetch(feedUrl); var rssXML = response.getContentText(); var document = XmlService.parse(rssXML); var root = document.getRootElement(); var ns_rss = XmlService.getNamespace('http://purl.org/rss/1.0/'); var ns_dc = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/'); var ns_atom = XmlService.getNamespace('http://www.w3.org/2005/Atom'); var ns_rdf = XmlService.getNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'); var rootTagName = root.getName().toLowerCase(); var entries = []; // 記事のリストを取得 switch (rootTagName) { case 'rdf': // 1.0 entries = root.getChildren('item', ns_rss); break; case 'feed': // atom entries = root.getChildren('entry', ns_atom); break; case 'rss': // 2.0 entries = root.getChild('channel').getChildren('item'); break; default: return false; } return entries.map(function(entry, index) { var title; var link; var pubDate; switch (rootTagName) { case 'rdf': // 1.0 title = entry.getChild('title', ns_rss).getText(); link = entry.getChild('link', ns_rss).getText(); pubDate = entry.getChild('date', ns_dc).getText(); break; case 'feed': // atom title = entry.getChild('title', ns_atom).getText(); link = entry.getChild('link', ns_atom).getAttribute('href').getValue(); // pubDate = entry.getChild('published', ns_atom).getText(); // 普段はこっちの方を利用する pubDate = entry.getChild('updated', ns_atom).getText(); // 今回はQiita用にupdatedを利用する break; case 'rss': // 2.0 title = entry.getChild('title').getText(); link = entry.getChild('link').getText(); pubDate = entry.getChild('pubDate').getText(); break; } // 投稿日が昨日なら var entryDate = Utilities.formatDate(new Date(pubDate), 'Asia/Tokyo', 'YYYY-MM-dd'); if (yesterday == entryDate) { return { 'title': title, 'link': link } } return ''; }).filter(function(item) { return item !== ''; }); } // 前日の日付を取得 function getYesterday() { var date = new Date(); date.setDate(date.getDate() - 1); return Utilities.formatDate(date, 'Asia/Tokyo', 'YYYY-MM-dd'); } // スプレッドシートを取得 function getSheet(sheetName) { var spreadSheet = SpreadsheetApp.openById(SHEET_ID); return spreadSheet.getSheetByName(sheetName); } // 指定したカラムの値を取得 function getColumValues(sheet, columnName, startIndex) { var values = sheet.getRange(columnName + ":" + columnName).getValues(); // 配列を整理 var result = new Array(); for (var i = 0; i < values.length; i++) { if (i >= startIndex) { if(values[i] != null && values[i] != "") { result.push(values[i]); } } } return result; } // はてなブックマーク数を取得 function fetchHatenaBookmarkCount(url) { if(url == '') { return ''; } var hatena_api_url = "http://b.hatena.ne.jp/entry.count?url=" try { return UrlFetchApp.fetch(hatena_api_url + encodeURIComponent(url)).getContentText(); } catch (e) { Logger.log(e); } return ''; } // ChatWorkへ投稿 function postChatWork(body) { var payload = { 'body': body }; var options = { 'method': 'post', 'headers': { 'X-ChatWorkToken': CHATWORK_TOKEN }, 'payload': payload }; UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + CHATWORK_ROOM_ID + '/messages', options); }
最後にトリガーを設定します。 以上で毎朝、前日分のRSSを一覧で投稿されるようになります。
最後に
簡単なサービスの連携であればIFTTTやZapierで十分ですが、GASのハードルも高くないので運用してみてはいかがでしょうか。