私は元freeeの社員なんで、マネーフォワードの使い方を解説するのも変な話なんですが、freeeがAPIで様々な会計処理・レポート作成を自動化できるのに比べて、マネーフォワードにはAPIが無いので、csvファイルのダウンロード →エクセルやスプレッドシートに反映、を繰り返している人も多いのではないでしょうか。

というわけで、今回はマネーフォワードからのデータ取得の例として、当期の全仕訳データをスプレッドシートへ反映する処理を自動化してみました。

作業は、Jupyter Labsを使って組み上げ、最後に自動化の確認として全コードをPythonファイルとして実行しました。<XXX>の部分は、アカウントごとに異なりますので、独自のパラメーターを当て嵌めてください。

非エンジニアの私がローカルで動かせることを前提に書いているので、全体的にコードが冗長だったりする部分があるとおもいますが、ご了承ください。

1. Libraryのimport

使うのはこのあたりです。主に、seleniumを使い、テーブルからデータを読み取るのにBeautifulSoupを使います。必要に応じて、pip installしてください。


from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
import time
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime as dt

import plotly.express as px

2. ログインして仕訳帳へ

Seleniumでブラウザを立ち上げてログインします。 fy_endは会計年度末でデータの取引日を整理する用、dt_nowは現在の時刻で、いつのデータを連携したのか管理する用です。 ログイン後に自動的に仕訳帳にリダイレクトするために、最初から仕訳帳のURL(https://accounting.moneyforward.com/books?cti=<YOUR_ACCOUNT_CTI>)へ行きます。 (実際にブラウザで仕訳帳へ行き、URLをコピーして用意してください。)

実行すると、ブラウザが立ち上がり、コード内のemailとpasswordを使ってログインし、仕訳帳のページへ飛びます。

DOMの読み込みを待つ意味とサーバーへの負担を考慮して、time.sleep()で動作タイミングをコントロールします。


fy_end = dt(2021,11,30)
dt_now = dt.now()

driver = webdriver.Firefox()
driver.get('https://accounting.moneyforward.com/books?cti=
<YOUR_ACCOUNT_CTI>')
driver.find_element_by_xpath("//input[@type='email']").send_keys
('<YOUR_EMAIL>')
driver.find_element_by_xpath("//input[@type='submit']").click()

time.sleep(2)

driver.find_element_by_xpath("//input[@type='password']").send_keys
('<YOUR_PASSWORD>')
driver.find_element_by_xpath("//input[@type='submit']").click()

time.sleep(5)

ログイン後の画面:

3. ページあたり件数を調整

デフォルトでは、1ページあたり50件表示になっているのをMaxの200件に変更します。「50件/ページ」となっているプルダウンをクリック→キーボードで下を2回→ENTERの順で動くように指示します。次のページネーションの回数を減らすことが出来ます。


driver.find_element_by_css_selector('#s2id_page_limit').click()
actions = ActionChains(driver)
actions.key_down(Keys.DOWN)
actions.key_up(Keys.DOWN)
actions.key_down(Keys.DOWN)
actions.key_up(Keys.DOWN)
actions.key_down(Keys.ENTER)
actions.key_up(Keys.ENTER)
actions.perform()

time.sleep(5)

4. ページネーションとデータ取得

各ページのデータをデータフレームに格納していきます。一部、Table内にhidden要素があったり、勘定科目名と勘定コードを分ける必要があったりするので、対応しています。

なお、単純に、csvファイルの仕訳データをダウンロードしエクセルなどへ反映する、という操作を自動化する方法もありますが、1回あたりのマネーフォワード側での処理時間が遅かったり、色々不都合があるので、スクレイピングによる方法を取ってます。


n_page = int(driver.find_elements_by_css_selector("#js-top-paging
   li")[-2].text)

df_list = []
def get_je_table():
   tables = driver.find_elements_by_xpath("//table")
   body_table = BeautifulSoup(tables[1].get_attribute('outerHTML'),
    'html.parser')
   body_data = [
   [ cell.get_text(separator='_') for cell in
   row.select("th:not(.hidden), td:not(.hidden)")[0:9] ]
     + [  None if row.select_one("td.js-td-tag > .js-done") == None\
            else row.select_one("td.js-td-tag > .js-done")
            ['data-original-title']
         ,None if row.select_one("td.js-td-memo > .js-done") == None\
            else row.select_one("td.js-td-memo > .js-done")
            ['data-original-title']
          ]
    for row in body_table.find_all("tr")
    ]
    return pd.DataFrame(body_data)

df_list = df_list + [get_je_table()]

for p in range(n_page-1):
    driver.find_elements_by_css_selector("#js-top-paging a")
    [-1].click()time.sleep(3)
     df_list.append(get_je_table())

df = pd.concat(df_list)

取得したデータ:

5. データの整理

データが、そのままだと不便なので、データフレームを整理していきます。

  1. 列名を整理。
  1. 取引No.と取引日について、空白は1つ上の行のデータで埋める。
  1. スクレイピングしてきた日付は、yy/mmの文字列かつ「経費・債務支払い」みたいなラベルも付いているので、日付を整理(一律、2021年とした後、会計年度末日よりも後の日付になってしまうデータは1年引く、など)。
  1. 勘定科目とコードについて、とってきたデータは、_(アンダースコア)で繋がっているので、分けて別の列とする。
  1. 列を並び替える。
  1. 余計な行を削除。

#カラムの整理
  df.columns = ['取引No','取引日','借方勘定科目','借方補助科目','借方金額',
  '貸方勘定科目','貸方補助科目','貸方金額','摘要','タグ','メモ']
  df = df.reset_index(drop=True)

#空白セルへの対処
df['取引No'] = df['取引No'].apply(lambda x: None if x == '' else x)
df['取引No'] = df['取引No'].fillna(method='ffill')

df['取引日'] = df['取引日'].apply(lambda x: None if x == '' else x)
df['取引日'] = df['取引日'].fillna(method='ffill')

#取引日に「年」を反映
df['取引日'] = df['取引日'].apply(lambda x: x.split('_')[0])
df['取引日'] = df['取引日'].apply(lambda x:
dt(fy_end.year,int(x[0:2]),int(x[-2:])))
df['取引日'] = df['取引日'].apply(lambda x: x if x  fy_end else
 dt(x.year-1,x.month,x.day))

#勘定科目の名称とコードを分ける
def split_account(account_str,l_or_r):
  if l_or_r == 'l':
  return account_str.split('_')[0]
  if l_or_r == 'r':
  res = account_str.split('_')[1] if len(account_str.split('_'))
  == 2  else None
  return res
df['借方勘定科目コード'] = df['借方勘定科目'].apply(lambda x:
 split_account(x,'r'))
df['借方補助科目コード'] = df['借方補助科目'].apply(lambda x:
 split_account(x,'r'))
df['貸方勘定科目コード'] = df['貸方勘定科目'].apply(lambda x:
 split_account(x,'r'))
df['貸方補助科目コード'] = df['貸方補助科目'].apply(lambda x:
 split_account(x,'r'))

df['借方勘定科目'] = df['借方勘定科目'].apply(lambda x: split_account
(x,'l'))
df['借方補助科目'] = df['借方補助科目'].apply(lambda x: split_account
(x,'l'))
df['貸方勘定科目'] = df['貸方勘定科目'].apply(lambda x: split_account
(x,'l'))
df['貸方補助科目'] = df['貸方補助科目'].apply(lambda x: split_account
(x,'l'))

#列を並べ替え
cols = ['取引No','取引日','借方勘定科目コード','借方勘定科目','借方補助科目
コード','借方補助科目','借方金額','貸方勘定科目コード','貸方勘定科目','貸方
補助科目コード','貸方補助科目','貸方金額','摘要','タグ','メモ']
df = df[cols]


#余計なデータを削除
df = df[df['借方補助科目'] != '合計金額']

整理後のデータ:

6. 貸借の整理

ここまで来ると、このまま出力してエクセルなどに反映して使える状態です。しかし、もう一工夫したいところです。借方・貸方の概念は会計の正確さのチェックという意味では役に立ちますが、データ分析や自動化という意味では不便なので、シンプルなデータ構造にします。

整理するにあたり、各勘定科目が借方・貸方のどちらのタイプか(資産・費用のように借方がプラスか、負債・収益のように貸方がプラスか)、の情報が必要になるので、事前にGoogle sheetsに持っておいたアカウント体系をAPIで持ってきて連携しています。(マネーフォワードの各種設定→勘定科目→エクスポート、で落としてきたものにaccount_type情報を足したものです。)APIのコードは別ファイルなので、記載を省略しています。

また、Jupyter Labでのテスト段階では、いくつかPLのグラフを作ってデータが正しく取れているか確認しました。


from tools import gsheet

def clean_number(x):
   return x.replace(',', '')

def trunc_month(x):
   return dt(int(x.year),int(x.month),1)

accounts = gsheet.get_data("<SPREAD_SHEET_ID>",
"Accounts!A1:J")
accounts = accounts[['帳票','分類','決算書科目','勘定科目','account_type'
]].drop_duplicates()
data1 = df[['取引No', '取引日', '借方勘定科目コード', '借方勘定科目',
 '借方補助科目コード', '借方補助科目', '借方金額','摘要', 'タグ', 'メモ']]
data1['source'] = 1
data2 = df[['取引No', '取引日', '貸方勘定科目コード', '貸方勘定科目',
'貸方補助科目コード', '貸方補助科目', '貸方金額','摘要', 'タグ', 'メモ']]
data2['source'] = -1

data_cols = ['取引No', '取引日','勘定科目コード', '勘定科目',
'補助科目コード', '補助科目', '金額','摘要', 'タグ', 'メモ','source']
data1.columns = data_cols
data2.columns = data_cols
data = pd.concat([data1,data2])
data = data[data['金額'] != '']
data = pd.merge(data,accounts,how='left',on='勘定科目')
data['取引日'] = pd.to_datetime(data['取引日'])
data['月'] = data['取引日'].map(trunc_month)
data.account_type = data.account_type.astype(int)
data['金額'] = data['金額'].map(clean_number).astype(int)
data['金額'] = data['金額'] * data['source'] * data['account_type']

貸借を整理した後のデータ:

PLを出力してデータをチェック:

7. アウトプット

データの整理も出来たので、アウトプットして完了です。以下では、APIでGoogle Sheetsに自動反映しています。


import gspread
from df2gspread import df2gspread as d2g
from oauth2client.service_account import ServiceAccountCredentials

spreadsheet_key = '<SPREAD_SHEET_ID>'
wks_name = 'JE'

scope = ['https://spreadsheets.google.com/feeds',
         'https://www.googleapis.com/auth/drive']

credentials = ServiceAccountCredentials.from_json_keyfile_name(\
                        'jsonFileFromGoogle.json',scope)
gc = gspread.authorize(credentials)
spreadsheet = gc.open_by_key(spreadsheet_key)
values = [data[output_cols].columns.values.tolist()]
values.extend(data[output_cols].values.tolist())
spreadsheet.values_update(wks_name, params={'valueInputOption': 'USER_ENTERED'}, body={'values': values})

自動反映されたスプレッドシート:

Posted by: Ryoichi Fujita(藤田亮一)