Google Apps Script（GAS）を利用した業務改善

##はじめに
はじめまして。ドワンゴでニコ生のディレクターをしています。
アドベントカレンダーもQiitaへの投稿も初めてなのですが、なぜかニコニコ●放送という文字列が投稿できずに投稿が遅れてしまいました。難しいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922207/eff83a7b-462f-dea0-ee3a-f5858d4a6dff.png)


今回はチームで業務効率化やプロダクトの改善に役立っているGoogle Apps Script（GAS）について紹介します。
内容としてはディレクター向けになってしまいますが、少しでも参考になれば幸いです。
GASの例を記載してますがかなり冗長だったり不適切な書き方になっている点が多々あると思いますがご容赦ください。（教えてください:pray:）

##GASのいいところ
個人的にGASが便利だなと思っている点がいくつかあります。
###簡単に書ける
Google Apps Script は JavaScriptベースなので私のような非エンジニアでも比較的簡単に扱えますし、Googleで検索すればだいたいやりたいことはできるので、業務の隙間時間や煮詰まった時の気分転換で作成できます。

###Slackに投稿できる
定期的に情報を共有したり、何かを監視してお知らせしたりする用途が多いので簡単にSlackに投稿できるのは便利です。

###スプレッドシートのデータと連携できる
普段スプレッドシートでデータ集計することが多いのでそのデータの値を簡単に利用できたり、収集したデータをスプレッドシートに記載できるのが便利です。

###定期実行できる
GASのトリガー機能を利用すると非常に簡単に関数の定期実行できるので日次、週次のリマインドができます。
また、最小1分間隔でトリガー実行できるので放送開始などリアルタイム性が重視される更新もキャッチできます。

##実際にチームで稼働しているGASの紹介
###前日の実績をSlackで投稿する
####概要
スプレッドシートで集計した昨日の実績数値のサマリをSlackに流すだけの超単純なものです。
####良い点
担当プロダクトの各種実績数値については定期的にチェックはすると思うのですが、日々の数値の変化には疎くなりがちです。
1ヶ月後に調査してみて気づいても、「なんかこの時跳ねてたねー。」で終わってしまうことも多いです…
毎日サービスの利用状況について共有することで、「昨日はいつもよりDAU低いけど何が原因だろう。」とか「有料会員の入会が多いけど何か有力なコンテンツがあったのか？」など早いタイミングで気づいてアクションに繋がるきっかけになれば良いかなと思っています。

####GAS例
内容としては、実績数値を集計しているシートから該当の値を取得してSlackに投稿するだけです。
GASからSlackへの投稿については [SlackApp](https://qiita.com/soundTricker/items/43267609a870fc9c7453 "SlackApp") というライブラリが大変便利なのでこちらを使っています。

```javascript
function summaryYesterday() {
    var slack = {
        token: PropertiesService.getUserProperties().getProperty('SLACK_ACCESS_TOKEN'), // Slackのtoken
        groupId: "#流したいチャンネル名", // メッセージの送信先「@~」で個人「#~」でチャンネル
        userName: "昨日の実績（両OS合算）", // botの名前（なくても良いです）
        iconEmoji: ':bar_chart:' // botのアイコン（なくても良いです）
    }
    var today = new Date(Date.now());
    var yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
    //スプレッドシートから対象のデータを取得
    var ss = SpreadsheetApp.getActiveSpreadsheet()
    var summarySheet = ss.getSheetByName('GASでSLACKに送信する為のシート');
    var dataDate = DateStyle(yesterday); //取得した日付を整形
    var arrSheet = summarySheet.getRange('A1:E11').getValues();
    var install = getSpecifiedNumber(arrSheet, 2);
    var dau = getSpecifiedNumber(arrSheet, 3);
    var lv = getSpecifiedNumber(arrSheet, 4);
    var broadcaster = getSpecifiedNumber(arrSheet, 5);
    var broadcastrate = getSpecifiedNumber(arrSheet, 6);
    var premium = arrSheet[7][2];
    var url = arrSheet[8][2];

    //取得したデータを可読性高く加工
    var summaryData = '*' + dataDate + '*\n' +
        ':inbox_tray: 累計インストール数 : ' + install.cnt + ' (' + install.rate + '%)\n' +
        ':running: DAU : ' + dau.cnt + ' (' + dau.rate + '%)\n' +
        ':movie_camera: 番組数 : ' + lv.cnt + ' (' + lv.rate + '%)\n' +
        ':dancer: 配信者数 : ' + broadcaster.cnt + ' (' + broadcaster.rate + '%)\n' +
        ':chart_with_upwards_trend: 配信率 : ' + broadcastrate.cnt + '%' + ' (' + broadcastrate.rate + '%)\n' +
        ':moneybag: ' + 'プレミアム入会者数 : ' + premium + '\n' +
        '※ ( )は「前週同日比」\n' +
        ':microscope: <' + url + '|' + '詳細はこちらから' + '>';

    //SLACKに送信する（更新があった日のみ送信する）
    var checkDate = EventdayStyle(today);
    var lastUpdated = EventdayStyle(arrSheet[9][2]); //シートの最終更新日を取得
    if (checkDate == lastUpdated) {
        //指定のチャンネルに昨日の実績を送信
        var slackApp = SlackApp.create(slack["token"]); //リソースに追加したSlackAppライブラリの呼び出しとtoken情報の取得
        var slackPost = slackApp.postMessage(
            slack["groupId"],
            summaryData, { username: slack["userName"], icon_emoji: slack["iconEmoji"] }
        )
    };
}
```

####実際の投稿イメージ
前日比などわかると日々の変動がわかって良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922207/b36a1041-a3ed-8f16-0048-7842c1916f4b.png)


###特定の番組の開始を監視しSlackで投稿する
####概要
特定のタグやタイトルがついた番組や、監視したいユーザーのリストを事前にスプレッドシートに記載しておき対象の番組が開始した時にSlackチャンネルにポストする。
####良い点
ニコ生は誰がいつ始めるかわからないので、特定の番組を配信中にキャッチすることが意外と難しいです。
初めて配信したユーザーの番組をSlackでキャッチして初配信ユーザーがどういうところで躓くのかを実際の番組を視聴しながら確認できたり、新しい機能をリリースした際に、いち早くその機能がどのように利用されているか確認したいときに便利です。

####GAS例
基本的にはトリガー機能を使ってお好みの更新頻度で監視したい番組条件で検索APIを叩いて、それを垂れ流すだけで良いのですが、初回配信を抽出するために少し手を加える必要があったのでその部分を紹介します。

ニコ生では配信を開始して1ヶ月の放送者の番組には`ルーキー`という番組タグがつきます。（正確にはユーザーのレベルが一定以下という条件もあるかも？）
事前に直近1ヶ月で`ルーキー`タグのついた番組を放送したユーザーのリストをスプレッドシートに用意しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922207/95aae410-a765-d5c1-1d03-cd51c53f1080.png)

`ルーキー`タグのついた番組があった場合に上記のリストに同一のユーザーがいるか参照し、存在しなかった場合に初回配信としてSlackに投稿します。

```javascript
//配信済みのルーキーか判定する関数
function findRow(val,col){
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var targetSheet = spreadsheet.getSheetByName('超ルーキー');
  var dat = targetSheet.getDataRange().getValues(); 
  for(var i=1;i<dat.length;i++){
    if(dat[i][col-1] === val){
      return i+1;
    }
  }
  return 0;
}
```

####実際に投稿されているイメージ
タイトルやユーザーIDがリンクになっているのですぐに初回配信の番組を見に行くことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922207/6b4cb502-4daa-ce62-05d1-550d7fde164b.png)
実際は、初回配信のユーザーIDをスプレッドシートに追加したり、配信開始から1ヶ月以上経過したユーザーをリストから削除する処理も必要になってきますが、今回は長くなるので割愛します。


###アプリ内に掲載中のインフォ記事をSlackでお知らせする
####概要
新しいインフォが掲載されたり、同じ記事が長時間掲載されている場合にお知らせしてくれるGAS
####良い点
ニコ生アプリではインフォ記事に特定のカテゴリを付けると、障害告知やキャンペーン告知などをアプリ内の目立った位置に記事を露出できるようになっています。
ですが、カテゴリの外し忘れにより長期間古い記事がで続けたり、間違って誰かがカテゴリを付けてしまって不適切な記事が掲載されることがありました。
そこで、毎日特定カテゴリの最新記事が変更されているかどうかを監視するGASを作成し、更新があった場合や同じ記事が一定期間以上掲載されている場合にSlackに投稿するようにしました。

####GAS例
監視したいインフォカテゴリから記事のタイトルとURLと公開日時を取得します。

```javascript
// 各インフォ掲載枠のrss
var feedI = 'https://blog.nicovideo.jp/niconews/category/nicoliveappmenu_ios/feed/index.xml'
var feedA = 'https://blog.nicovideo.jp/niconews/category/nicoliveappmenu_android/feed/index.xml'
var feedIB = 'https://blog.nicovideo.jp/niconews/category/nicoliveappbc_ios/feed/index.xml'
var feedAB = 'https://blog.nicovideo.jp/niconews/category/nicoliveappbc_android/feed/index.xml'

// rssから必要な値を取得する
function getArticleUrl(feedUrl) {
  var xml = UrlFetchApp.fetch(feedUrl).getContentText();
      document = XmlService.parse(xml);
      items = document.getRootElement().getChildren('channel')[0].getChildren('item');
      if( items[0] != undefined ){
        url = items[0].getChild('link').getText(); // 記事URL取得
        pubDate = items[0].getChild('pubDate').getText(); // 作成日時
          title = items[0].getChild('title').getText(); // タイトル取得
      } else {
        // 記事がない場合は empty と返す
        url = 'empty'
        pubDate = 'empty'
        title = 'empty'
        }
  return {url: url, pubDate: pubDate, title: title};
}
```

プロパティに前日の最新記事のURLを記録しておき、当日の最新記事のURLが同じかどうかと、違った場合（先頭の記事が更新された場合）その記事が最近作成されたものなのかを判定する関数を作成します。
プロパティとは、プロジェクトやドキュメントに紐付いてデータを格納しておくスペースみたいなものです。もちろんプロパティを使わずスプレッドシートにデータを記載して管理しても良いと思います。

```javascript
var properties = PropertiesService.getScriptProperties();

// 掲載日数や古い記事出ないかチェックする関数
function checkCount(deviceId) {
  var url = "url"+ deviceId;
  var feedUrl = "feed" + deviceId;
  var count = "count"+ deviceId;
  if(deviceId == 'I'){feedUrl = feedI}else if(deviceId == 'A'){feedUrl = feedA}else if(deviceId == 'IB'){feedUrl = feedIB}else if(deviceId == 'AB'){feedUrl = feedAB};
  var urlYesterday = properties.getProperty(url);
  var countYesterday = properties.getProperty(count);
  var urlNow = getArticleUrl(feedUrl).url;
  var today = new Date(Date.now());
  var todayDt = today.getTime();
  var pubDateNow = getArticleUrl(feedUrl).pubDate;
  var pubDateNowDate = new Date(pubDateNow);
  var pubDateNowDt = pubDateNowDate.getTime();
  var dateDiff = todayDt - pubDateNowDt;
  // 記事がない場合は 記事無しと返す
  if (urlNow == 'empty'){
    properties.setProperty(count, 0);
    return {reason: '記事無し',count: -1, pubDate: pubDateNow,url: urlNow};
  // 前回と記事が同じ場合は掲載中と返す
  }else if (urlNow == urlYesterday){
    var obj = {reason: '掲載中',count: countYesterday, pubDate: pubDateNow,url: urlNow};
    return obj
  // 切り替わった記事が2週以内に作成された場合違う記事と返す
  }else if(dateDiff <= (604800016.56*2)){
      properties.setProperty(url, urlNow);
      properties.setProperty(count, 0);
      return {reason: '違う記事',count: 0, pubDate: pubDateNow,url: urlNow};
    // 切り替わった記事が2週以上前の記事であれば古い記事と返す
    }else if(dateDiff >= (604800016.56*2)){
      properties.setProperty(url, urlNow);
      properties.setProperty(count, 0);
      return {reason: '古い記事',count: 0, pubDate: pubDateNow,url: urlNow};
    } else{
      properties.setProperty(url, urlNow);
      properties.setProperty(count, 0);
      return {reason: '未定義',count: 0, pubDate: pubDateNow,url: urlNow};
    };
}
```

Slackに投稿する関数をトリガーで毎日実行し、その中で先ほどの`checkCount`を呼び出し掲載中ならばプロパティの掲載日数をカウントアップし、掲載が一定日数を超えた場合や、最新記事に更新があった場合はSlackに通知します。

```javascript
  if(reason == '掲載中'){
    // 掲載中ならプロパティをカウントアップする
    countYesterday ++;
    properties.setProperty(count, countYesterday);
    //Logger.log(properties.getProperty(count));
    //掲載中かつcountが7以上だった場合にslackに投稿
    if(arr.count > 6){
      var Slackpost = slackApp.postMessage( slack["groupId"], "▼ " + areaName + "\n" + '<' + arr.url + '|' + getArticleUrl(feedUrl).title + '>' + " が" + parseInt(countYesterday) + "日間掲載されています　"　+ '<' + editStr + '|' + '編集' + '>', {username : slack["userName"],icon_emoji: slack["iconEmoji"]});
      }
    //記事が切り替わった場合にslackに投稿
    } else if(reason == '古い記事'||reason == '違う記事'){
    var Slackpost = slackApp.postMessage( slack["groupId"], "▼ " + areaName + "\n" + DateStyle(date) + " に作成された " + '<' + arr.url + '|' + getArticleUrl(feedUrl).title + '>' + " が掲載されています " + '<' + editStr + '|' + '編集' + '>', {username : slack["userName"],icon_emoji: slack["iconEmoji"]});
    }
}
```

####実際の投稿イメージ
編集リンクをクリックするとそのままインフォ記事の編集画面に遷移できるので、不適切な記事が掲載されている場合はすぐに掲載終了させることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922207/c50e0da8-cd32-6585-f9c5-ebe583f33192.png)

##おわりに
今回ご紹介したGASはチームでKPTを実施してProblemに上がったことを発端に運用が開始されていたりします。
みなさんのチームの問題もGASで解決してくれると嬉しいなと思います。
また、GASに関わらず「うちのチームではこんなことやってます。」みたいなお話もあればぜひお聞きかせください。
いや、おかわり版アドベントカレンダーにはまだまだ空きがあるみたいなのでそこに投稿してください。