ゆとりずむ

東京で働く意識低い系ITコンサル(見習)。金融、時事、節約、会計等々のネタを呟きます。

酔った勢いでMoneyForwardの残高をLine通知する仕組みを作ってみた

こんにちは、らくからちゃです。

夫婦のお金の管理をどうするのかは、結構ご家庭によってカラーが出るようです。我が家では毎月決まった日に、私の口座から妻の口座に生活費を振り込み、その金額の中でやりくりしてもらうという方法を採用しています。

www.yutorism.jp

基本的に、

  1. 青いカード(Kyash)・・・還元率高め
  2. 銀色のカード(楽天銀行)・・・必要に応じてATMでおろせる

の優先度で使ってもらい、どうしても足らない時や緊急の際には黒いカード(REXカード)を使ってもらうというルールにしています。

予算が残高の形で見えるので、節約にも良いかなあと思ったのですが、案外残高のチェックが億劫なんですよね。Kyashは使った瞬間に通知が来て、その際に残高も表示されますが、すぐに消えてしまう。Moneyforwardの残高も反映まで時間が掛かるし、自分から見に行かないと気が付きづらい。

なんか良い感じに出来ねえかなーと思ったところ「せや、LINEに今日の残高を通知したらええんとちゃうか?」と思いたち、酔った勢いでMoneyForwardの残高をLine通知する仕組みを作ってみましたヽ(=´▽`=)ノ

ド素人が何も考えず「とりあえず動けば良い」の崇高な精神のもと、総工数3時間ほどで作った雑な仕組みなので、プロから見ればツッコミどころ満載だと思いますが、どなたかのお役に立つかもしれませんので、共有いたします。

試してみて頂くのは良いかと思いますが、全て自己責任でオネシャス!

ざっくりとした構成

まずはざっくりとしたシステム構成図です。

f:id:lacucaracha:20190609230234p:plain

ざっくりとした流れとしては、

  1. MoneyForwardへの口座残高の更新通知を投げる
  2. MoneyForwardからGoogle Sheetsに残高を吐き出させる
  3. 残高をLINE notifyで通知する

ね、簡単でしょ?

やりたいことはその程度のことです。普段ウェブ系の開発に慣れ親しんでいる人ならば、息を吸って吐く間にできそうですね。

とはいえ、普段コンサルなんて肩書で紙芝居(パワポ)職人をしている人間からは全くの未知の領域です。あれこれ試行錯誤しながら試してみた内容を整理してみたいと思います。

Cloud Functions with Puppeteer でスクレイピングしてみる

まず口座残高の更新をマネーフォワードに命令する方法について考えてみましょう。

人間がこの操作をやるには、マネーフォワードのサイトにログインして「一括更新」というボタンをポチッとするだけです。人間の代わりにコンピュータにブラウザを操作させてアレコレやらせる行為を「スクレイピング」といい、そういう処理をさせたいわけですね。

そんなもんPythonでガリっとやれば何も難しいことありませんが、家のパソコンからずーっと指示出すのもイケてないよね、と思いましたので、今回は「Cloud Functions with Puppeteer」を使ってみることにしました👏

はい、この時点で、数時間前の私はついていけてません(笑)。

パソコンで定期的にプログラムを走らせる代わりに、サーバーをお借りして、そこでクラウド上で処理ししたら捗るんじゃね?って話です。ギョーカイ的には「サーバーレス環境でヘッドレスブラウザを使ってスクレイピングする」なんて言うんでしょうかね。

余談ですが、Zaimにはデータ連携用のAPIが用意されているらしいのですがマネーフォワードは一般では使えなさげ。スクレイピング自体、あまりお行儀が良い行為でもありませんが、散々よそのサービスから引っこ抜いてきているのですから、多めに見てもらいましょう。

話を戻しますと、実際に処理を実行するのは、Cloud FunctionsというGoogleの提供しているサービスです。そこでPuppeteer(パペッティア、操り人形使いって意味らしい)というプログラムでブラウザを仮想的に立ち上げコントロールをします。

というわけで、まずはCloud Functionsの初期登録からですねー。

cloud.google.com

利用料は掛かるみたいですが、365日間$300ドルぶん無料でついてくるみたいですし、勝手に課金されることは無さげなので、まずはどーんと登録しちゃいましょ!

f:id:lacucaracha:20190609232359p:plain

登録できれば、ハンバーガーメニューをクリックしたら、やたら大量の項目が出てくると思います。今回、活躍するのは下から2つ目の「Cloud functions」ですね!

f:id:lacucaracha:20190609232810p:plain

でまあ、ガチャガチャいじってたら「関数の作成」が出来るわけですね。名前は適当に「updateMoneyForward」とかにしておきましょう。あと合わせて実行する関数名も同じにしておきましょうか。

f:id:lacucaracha:20190610230705p:plain

まずpackage.jsonに今回利用するpuppetterのバージョン情報を記載しておきましょう。

{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "puppeteer": "^1.6.2"
  }
}

本体のindex.jsのほうはこんな感じです。適宜、mailとpwは自分のものに置き換えて試してみてくださいな。

const puppeteer = require('puppeteer');
let page;

async function getBrowserPage() {
  // Launch headless Chrome. Turn off sandbox so Chrome can run under root.
  const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  return browser.newPage();
}

exports.updateMoneyForward = async (req, res) => {
  
 const url = "https://moneyforward.com/users/sign_in";
 const mail = "★メールアドレス★";
 const pw = "★パスワード★";


  if (!page) {
    page = await getBrowserPage();
  }

  await page.goto(url);
  
  if((await page.title()) == 'ログイン|マネーフォワード ME'){
await page.focus('input[name="sign_in_session_service[email]"]'); await page.type('input[name="sign_in_session_service[email]"]',mail); await page.focus('input[name="sign_in_session_service[password]"]'); await page.type('input[name="sign_in_session_service[password]"]',pw); await page.click('#login-btn-sumit'); await page.waitFor(1000); }  await page.click('a[href="/aggregation_queue"]'); res.send('update終了'); }

 やってる内容はどうってことありません。まずはサインイン画面を表示して、タイトルが「ログイン」ならば、メールアドレス欄、パスワード欄に所定の値を指定し、ログインボタンをポチッと押すだけです。

でログインが出来れば「一括更新」のボタンが実行している「aggregation_queue」へのポストをクリックで実行してるだけ。まあそんなもんです。

出来上がったら「トリガー」というタブに表示されているURLをクリックしてみましょう。しばらくしたら「update終了」と表示され、それと同時にマネーフォワードへの更新指示も行われているはずです。

Google スプレッドシートとの連携

お次はマネーフォワードの残高をGoogleスプレッドシートに出力する処理です。 

LINE通知するだけならば、何もそんな手間の掛かることはしなくても良いのですが、どっかに出力しとけば何かと便利かな?なんて感覚で、ひとまず出しておくことにしてみました。

先ほどと全く同じ要領で、今度は「writeSpreadsheet」を作成しましょう。で、package.jsonにはgoogleapiも書いておきます。

{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "puppeteer": "^1.6.2",
  "googleapis": "^27.0.0"
}
}

さてここで先に、出力先となるGoogleスプレッドシートを作成しておきます。スプレッドシートの作り方は流石にいいよね?わかんなきゃググって。

シートは、

  • Data
  • Notify

の2つを作っておきましょう。で、Googleスプレッドシートって許可された人しかアクセスできないので、Cloud Functionsの実行ユーザを編集者として追加しておきます。

実行ユーザは、今回作成した関数の「サービス アカウント」という項目にcybernetic-arc-xxxx@appspot.gserviceaccount.com みたいな感じで表示されていると思います。こちらを、スプレッドシートの「共有」メニューから、編集者としてぶっこみましょう。

あとスプレッドシートのURLを見てください。/d/ から/editの間の部分がIDになっていますので、下記のソースコードをindex.jsに貼り付けると同時に「スプレッドシートのID」部分も置き換えてみましょう。

const puppeteer = require('puppeteer');
const {google} = require('googleapis');

let page;

async function getBrowserPage() {
  // Launch headless Chrome. Turn off sandbox so Chrome can run under root.
  const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  return browser.newPage();
}

exports.writeSpreadsheet= async (req, res) => {
  
 const url = "https://moneyforward.com/users/sign_in";
const mail = "★メールアドレス★";
const pw = "★パスワード★"; if (!page) { page = await getBrowserPage(); } await page.goto(url); if((await page.title()) == 'ログイン|マネーフォワード ME'){ await page.focus('input[name="sign_in_session_service[email]"]'); await page.type('input[name="sign_in_session_service[email]"]',mail); await page.focus('input[name="sign_in_session_service[password]"]'); await page.type('input[name="sign_in_session_service[password]"]',pw); await page.click('#login-btn-sumit'); await page.waitFor(1000); } console.log(await page.title()); const result = await page.evaluate(getMoneyForward); (async () => { const auth = await google.auth.getClient({ scopes: ['https://www.googleapis.com/auth/spreadsheets'] }); const sheets = google.sheets({version: 'v4', auth}); const req = { spreadsheetId: '★スプレッドシートのID★', range: 'Data', valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', resource: { values: result, }, }; await sheets.spreadsheets.values.append(req); })(); res.send('write終了'); } function getMoneyForward() { //return document.querySelector('.heading-radius-box').textContent; return [...document.querySelectorAll('.facilities-column')].map(account => { var now = new Date(); now.setTime(now.getTime() + 1000*60*60*9);// JSTに変換 var title = ''; var amount = ''; var date = ''; if(account.querySelector('a')) title = (account.querySelector('a').textContent); if(account.querySelector('.number')) amount = (account.querySelector('.number').textContent).replace('円','').replace(',','') ; if(account.querySelector('.date')) date = (account.querySelector('.date').textContent).replace('取得日時(','').replace(')',''); return [now.toLocaleString('ja-JP'),title, amount,date]; }); }

先ほどと同じようにトリガー実行することにより、うまくいけばSpreadsheet上に各口座別の残高が出力されます。

LINE notifyでLINEへ通知する

最後に、取得したデータをLINEへ通知しましょう!

LINEへの通知は、LINE notifyという仕組みを使います。LINE notifyというユーザから通知を受け取れるルームを作り、そこに結果が出力され通知されるイメージです。

具体的な登録方法はこちらが詳しいのでご参照の上、トークンをご準備ください。

そのまえに、実際に連携するデータはどれ?って話ですよね(笑)

Dataシートにダラダラ溜め込んでいるデータの中から、実際に連携する最新の値はNotifyシートで取得します。A列に任意の口座名を指定すればB列に最新残高を表示させたい。

f:id:lacucaracha:20190610235411p:plain

Google スプレッドシートにはフィルター関数という超絶便利な関数があるので、こんな感じに書いておけばB列に最新残高を表示させることが出来ます。

=text(filter(Data!C:C,Data!A:A=max(Data!A:A),Data!B:B=A2),"¥#,##0")

Data列のA列(更新日時)が最新で、B列(口座名)が指定の値と一致するもののC列の値(残高)を取得する。みたいなSQLのSELECT文チックな書き方が出来ます。INDEXとMATCHの組み合わせ技で色々無茶をやっていたのが馬鹿らしく思えますね(笑)。

さてこうして取得した値を、LINE notify経由でぶん投げる処理を実装しましょう。

作成したスプレッドシートから、ツール→スクリプトエディタで、Google Apps Scriptが書けます。エクセルで言うところのVBみたいなもんですわ。そこに下記をペタリと貼り付けます。トークンのところだけ、最初に取得したLINE notifyのトークンを指定してください。

function sendHttpPost(message){
  var token = '★トークン★';
  var options =
   {
     "method"  : "post",
     "payload" : "message=" + message,
     "headers" : {"Authorization" : "Bearer "+ token}

   };

   UrlFetchApp.fetch("https://notify-api.line.me/api/notify",options);
}


function sendAmount(){
  var message ='\n';
  var sheet = SpreadsheetApp.getActive().getSheetByName('Notify');
  var range = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn());
  
  for(var row=2; row <= range.getNumRows(); row++){

    var column = 1;
    //口座名
    message = message + range.getCell(row,column).getValue() + ' ';
      column++;
    //金額
    message = message + range.getCell(row,column).getValue() + ' ';
    column++;
    //取得日時
    //message = message + range.getCell(row,column).getValue() + ' ';
    //column++;
    
      message = message + '\n';
  }
  
  Logger.log(message);
  sendHttpPost(message);
}

これで、このファンクションを実行すればLINEに通知が飛びます。メニューの関数を実行から試してみましょう!

では仕上げとして、これらの一連の処理を定期的に実行できるようにしておきましょう。スクリプトに下記2つを追加します。それぞれ、update/writeを実行した際のトリガーURLに置き換えておいてくださいね。

function updateMoneyForward(){
  var updateURL = '★updateURLのトリガーURL★';
  var response = UrlFetchApp.fetch(updateURL).getContentText();
  return response;
}

function writeSpreadsheet(){
  var writeURL = '★writeURLのトリガーURL★';
  var response = UrlFetchApp.fetch(writeURL).getContentText();
  return response;
}

スクリプトのメニューから編集→現在のプロジェクトのトリガーを選ぶと、こんな感じの画面が表示されます。

f:id:lacucaracha:20190611001101p:plain

ここから、一定時間で処理が実行されるように指定することが出来ます。プラスボタンを押して、イベントのソースを「時間主導」にしてお好きな間隔を指定ください。

こんな感じで通知が飛ぶようになれば完成です(๑•̀ㅂ•́)و✧

f:id:lacucaracha:20190611001546p:plain

まとめ

Cloud Functions with Puppeteer を使えば、サクッとクラウド上でスクレイピングを実行することができました。パソコンつけっぱなしにしなくて済んでエコですね。あとLINE notifyも鼻血が出るくらい簡単に、一番参照頻度の高いLineへデータを飛ばせるので非常に便利です。

「動かしながら勉強する」の基本コンセプトのもと、行き当たりばったりでトライしてみましたが、エンジニアではない私でも、この程度は簡単に出来るだけの道具が揃ってきています。皆様も色々挑戦してみた結果をシェアし合いましょう!!

ではでは、今日はこのへんで。