LINE Messaging API + GAS + Googleドライブ OCR で家計のスナップショットをグラフ化

# はじめに
詳細な家計簿をつけるのは億劫だけど、資産の増減くらいは最低限把握しておきたいところ。

そこで大まかな資産推移を捕捉するために、勉強がてら LINE Messaging API と GoogleAppsScript、
Google ドキュメントのOCR機能を使用して家計のスナップショットを可視化してみました。
愛用しているマネーフォワードMEの資産残高画面のスクリーンショットを撮影・bot に投稿することで
以下のような資産推移グラフを自動で作成します。
![asset_graph_mosaic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310513/b88a5bb0-b4ff-195f-e6f5-8173fcab6b93.png)

# 処理の流れ
1. マネーフォワードMEの資産残高のスクリーンショットを撮影し、LINE botに投稿
<img width="375" alt="asset_ss_mosaic.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310513/856d8b10-b5d5-88b7-d96c-d62adf1c5b06.jpeg">

1. GAS を hook し、投稿画像を Drive に保存
1. Google ドキュメント の OCR 機能を使用し、画像から残高部分を抜粋
1. 加工して Spreadsheet に転記
1. グラフを作成、その画像URLを LINE bot にて返信

という感じです。2.〜 5. の部分を GAS で自動化しています。
Google ドキュメントの OCR の精度がかなり良いため、特に苦労なく求めていた機能を実現できました。

# 事前準備
LINEデベロッパーへの登録やGoogle アカウントでの認証が必要になります。以下の記事などを参考にしました。

 - [LambdaではじめてのLINE Botを作る](https://dev.classmethod.jp/etc/lambda-line-bot-tutorial/)
 - [Google Apps ScriptでLINE BOTつくったら30分で動かせた件](https://qiita.com/hakshu/items/55c2584cf82718f47464)

GASのなかで Drive API を呼び出す必要があるので、有効化しておきましょう。

 - [Google APIコンソールを使わずにDrive APIを有効にする](https://qiita.com/tanaike/items/1d49f73ab05403d0fe4c)

また投稿した画像やグラフ画像の保存用にDrive上に専用フォルダを作成しておくと便利です。
このとき作成したフォルダのURL末尾の部分がコード中で使用するフォルダのIDとなります。
なお作成した画像などは都度削除しているので、このフォルダにデータが蓄積されることはありません。

 - [フォルダを作成してファイルを整理する | Google ドライブ の使い方](https://www.appsupport.jp/drive/create-folder/)

# コード

```javascript:Code.gs
var CHANNEL_ACCESS_TOKEN = "YOUR_ACCESS_TOKEN";
var spreadsheet = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID");
var sheet = spreadsheet.getActiveSheet();

function doPost(e) {
  var events = JSON.parse(e.postData.contents).events;
  var url =
    "https://api.line.me/v2/bot/message/" + events[0].message.id + "/content/";
  var image = getImage(url).getBlob();
  var fileName = Utilities.formatDate(new Date(), "Asia/Tokyo", "YYYY-MM-dd");
  var requestBody = {
    title: fileName,
    mimeType: "image/jpeg",
    parents: [{ id: "YOUR_FOLDER_ID" }]
  };

  // 投稿画像をもとにOCRにかけたドキュメントファイルを作成
  Drive.Files.insert(requestBody, image, { ocr: true });

  var folder = DriveApp.getFolderById("YOUR_FOLDER_ID");
  var doc = folder.getFilesByType("application/vnd.google-apps.document").next();
  var docId = doc.getId();
  var lines = DocumentApp.openById(docId).getBody().getText().split("\n");

  // 残高部分のみ抜粋
  var text = [];
  lines.forEach(function(l) {
    if (l.match(/¥[1-9]/)) text.push([l.slice(1)]);
  });
  folder.removeFile(doc);

  // 既に記録がある列の隣に記録
  for (var i = 1; ; i++) {
    if (sheet.getRange(1, i).getValue() != "") continue;
    sheet.getRange(1, i, 1, 1).setValue(fileName);
    sheet.getRange(2, i, text.length, 1).setValues(text);
    break;
  }

  // グラフ部分の画像URLを取得
  var graphImg = sheet.getCharts()[0].getBlob();
  folder.createFile(graphImg).setName(fileName);
  var img = folder.getFiles().next();
  var imgUrl = img.getUrl();
  folder.removeFile(img);

  reply(events[0], imgUrl);
}

function getImage(url) {
  return UrlFetchApp.fetch(url, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    method: "GET"
  });
}

function reply(e, url) {
  var message = {
    replyToken: e.replyToken,
    messages: [
      {
        type: "text",
        text: url
      }
    ]
  };
  var replyData = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    payload: JSON.stringify(message)
  };
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", replyData);
}
```

# まとめ
LINE bot + GAS の組み合わせはちょっとしたツールを作るのに最高ですね。
今回初めて使用した OCR もかなりの精度で、発想次第でもっといろいろ面白いことできそう。
スクショの撮影・投稿も毎月○日に自動実行、できれば理想なんだけど、いい方法ないかなぁ。

# 参考にしたリンク

 - [
Introduction to Google Drive API](https://developers.google.com/drive/api/v3/about-sdk?hl=ja)
 - [Google Apps Scriptで画像の文字列を抜き出す](https://qiita.com/ttake/items/77f25a9c357ff2327868)
 - [【GAS】OCRを使った画像の文字取得を自動化する](http://www.initialsite.com/g04/14488)