モバイルアプリエンジニアの山下です。

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( /[

\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' ); 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:' ); 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( /[

\r]/g , '' ); return data; } function getThreads(filter) { var start = 0; var max = 20; return GmailApp.search(filter, start, max); } 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 ; } } } 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 ; } if (startTime.getHours() <= 12 && amflg) { body += "-AM-

" ; amflg = false ; } else if (startTime.getHours() > 12 && pmflg) { body += "

-PM-

" ; pmflg = false ; } body += '■ ' + title + '

' ; if (desc != '' ) { body += '・ ' + desc + '

' ; } } 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(); if (hasIgnoreKeyword(title) || startTime.getHours() > 19) { continue ; } if (startTime.getHours() <= 12 && amflg) { body += "-AM-

" ; amflg = false ; } else if (startTime.getHours() > 12 && pmflg) { body += "

-PM-

" ; pmflg = false ; } body += '■ ' + title + '

' ; if (desc != '' ) { body += '・ ' + desc + '

' ; } } body += '[/info]' ; postChatWork(body); } 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); } 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(<シート名>); 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); } } ); } 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' : entries = root.getChildren( 'item' , ns_rss); break ; case 'feed' : entries = root.getChildren( 'entry' , ns_atom); break ; case 'rss' : 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' : title = entry.getChild( 'title' , ns_rss).getText(); link = entry.getChild( 'link' , ns_rss).getText(); pubDate = entry.getChild( 'date' , ns_dc).getText(); break ; case 'feed' : title = entry.getChild( 'title' , ns_atom).getText(); link = entry.getChild( 'link' , ns_atom).getAttribute( 'href' ).getValue(); pubDate = entry.getChild( 'updated' , ns_atom).getText(); break ; case 'rss' : 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用に投稿内容を加工

例のごとく、読みやすいように加工していきます。今回は以下のように投稿します。

if (entries.length != 0) { var body = '[info][title]' + yesterday + 'に公開された記事[/title]

' ; for ( var e in entries) { body += entries [ e ] .title; body += '

' ; body += entries [ e ] .link; body += '



' ; } 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); } } ); if (entries.length != 0) { var body = '[info][title]' + yesterday + 'に公開された記事[/title]

' ; 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 += '

' ; body += entries [ e ] .link; body += '



' ; } body += '[/info]' ; postChatWork(body); } } 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' : entries = root.getChildren( 'item' , ns_rss); break ; case 'feed' : entries = root.getChildren( 'entry' , ns_atom); break ; case 'rss' : 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' : title = entry.getChild( 'title' , ns_rss).getText(); link = entry.getChild( 'link' , ns_rss).getText(); pubDate = entry.getChild( 'date' , ns_dc).getText(); break ; case 'feed' : title = entry.getChild( 'title' , ns_atom).getText(); link = entry.getChild( 'link' , ns_atom).getAttribute( 'href' ).getValue(); pubDate = entry.getChild( 'updated' , ns_atom).getText(); break ; case 'rss' : 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 '' ; } 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のハードルも高くないので運用してみてはいかがでしょうか。