列ごとにデータが出力されるので各項目をリスト化してから整形します。
shellでもできそうですが、リストの扱いはプログラミング言語の方が簡単なので今回もpythonを使います。
コード
各データを整形してDB(sqlite3)に突っ込むコードです。
import re import sqlite3 from pdfminer.high_level import extract_text text = extract_text('20210825_20210925110904.pdf') DATES = re.findall('\n\d\d', text) # shaping # Delete file output date DATES.pop(102) DATES.pop(51) # Delete carry data DATES.pop(0) DATES.pop(-1) mon_p1 = DATES[:50] day_p1 = DATES[50:100] mon_p2 = DATES[100:150] day_p2 = DATES[150:] category = re.findall('入|物販|カード', text) balance = re.findall(r'\\\d,?\d{1,3}', text) amount = re.findall('(\+\d,?\d{1,3}|\-\d,?\d{1,3})', text) # shaping balance.pop(0) page1 = [x for x in zip(mon_p1, day_p1, category[:50], balance[:50], amount[:50])] page2 = [x for x in zip(mon_p2, day_p2, category[50:], balance[50:], amount[50:])] con = sqlite3.connect('suica_expense.db') cur = con.cursor() cur.execute('CREATE TABLE IF NOT EXISTS expenses (date text, description text, balance int, amount int)') def Importfile(target): for i in target: MM = i[0].replace('\n','') DD = i[1].replace('\n','') BL = i[3].replace('\\','') AM = re.sub('\W','', i[4]) cur.execute('INSERT INTO expenses VALUES (\'{}\', \'{}\', \'{}\', \'{}\')'\ .format(MM+'/'+DD, i[2], BL, AM)) Importfile(page1) Importfile(page2) con.commit() con.close()
解説
まずは各項目のデータをリストに入れます。
月,日,種別,利用駅,残額,差額が明細として出力されますが、
月,日,種別,残額,差額(利用額)のみを格納します。
SUICAの履歴は100件分出力されますが、繰越の行が入って101件になっています。
この行は邪魔なので削除します。明細はまだ1回しかダウンロードしてないのですが、毎回出力されそうな気がします。
日付
繰越の日付を削除します。
100件分が2ページで出力されますが、50件毎に区切ると0番目と51番目が繰越の日付なのでこの位置のデータを削除します。
また数字二桁のものを集めると出力日の西暦が引っかかってしまうのでこれも削除します。(102番目と最後)
試しに5件出します。
>>> DATES[:5] ['\n08', '\n08', '\n08', '\n08', '\n08']
種別
支払いの種別です。
電車賃は入・出が1行で出力されますが、入の方だけ取れば電車賃と判別できます。
その他の支払いはすべて物販、チャージはカードで出力されます。
なので入、物販、カード(実データは半角)の文字列を取得します。
>>> category[:5] ['カード', '入', '物販', '入', '入']
残額
SUICAの残高は取っても仕方ない気がしますが、とりあえず入れておきます。
あとでグラフにでもして遊びましょう。
>>> balance[:5] ['\\3,320', '\\3,142', '\\2,300', '\\2,122', '\\1,649']
差額
実際の利用額です。
チャージしたら+、使ったら-で表記されるのでこの文字列をトリガーに取ります。
>>> amount[:5] ['+3,000', '-178', '-842', '-178', '-473']
各項目はちゃんと100件分です。(DATESは月と日が格納)
>>> len(DATES) 200 >>> len(category) 100 >>> len(balance) 100 >>> len(amount) 100
各リストのzip
各値を取得し終わったらzip化します。 各ページ50件なので、一応データも2ページ分作っておきます。
page1 = [x for x in zip(mon_p1, day_p1, category[:50], balance[:50], amount[:50])] page2 = [x for x in zip(mon_p2, day_p2, category[50:], balance[50:], amount[50:])]
格納
sqlite3へのインポートは割愛します。 リストの値を回してSQL文に入れるだけです。
実行
これを打つだけで格納されます。
(pdf) masashi@PC-ubuntu:~/expenses$ python suica_import.py
中身確認
sqlの出力はまだ作ってないのでsqlite3に入って確認します。
(pdf) masashi@PC-ubuntu:~/expenses$ sqlite3 suica_expense.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .tables expenses sqlite> SELECT count(*) FROM expenses; 100
ちゃんと100件分格納されてます。
データはこんな感じ。
sqlite> select * from expenses; 08/25|カード|3,320|3000 08/25|入|3,142|178 08/25|物販|2,300|842 08/25|入|2,122|178 08/25|入|1,649|473 (略)
続いて各件数。
半分は電車の運賃ですね。 仕事で結構出社してました。
sqlite> SELECT description, count(*) FROM expenses GROUP BY description; 入|51 物販|35 カード|14
各項目の合計金額を出してみます。
SUICA払いはコンビニでお酒、おつまみがほとんどなので額は増えがちですね。
ここを抑えるのが節約の肝になります。
sqlite> SELECT description, sum(amount) FROM expenses GROUP BY description; 入|16762 物販|23559 カード|42000
カードはチャージ分なので除外して、純粋な使用額はこちらです。
交通費もありますが意外とかかってます。
sqlite> SELECT sum(amount) FROM expenses WHERE description IN ('入', '物販'); 40321
まとめ
SUICAの明細をDBに取り込むことに成功しました。
支払いのトラッキング機能としてはすでに別のツールで賄えているのであまり有効ではないかもしれませんが、
こちらもウォッチして無駄遣いをしないようにしたいと思います。
SUICAの明細は過去100件分なので都度インポートしてると重複レコードが出てくると思いますが、
DBに格納しているのでうまいこと重複を避けて計算できるはずです。
(確認は今後)
久しぶりにSQL触りましたがいろんな集計ができて楽しいですね。
おまけ
当初差額データは生データのまま検証していましたが、集計がうまくできなかったので記号を取り除きました。
+-はついたままでも計算できたのですが、カンマが入ったまま集計しているナレッジがなく、数値は数値として計算して出力の過程で編集するものなのかもしれませんね。
DBはまだまだ勉強不足です。
sqlite> SELECT * FROM expenses; 08/25|カード|3,320|+3,000 08/25|入|3,142|-178 08/25|物販|2,300|-842 08/25|入|2,122|-178 08/25|入|1,649|-473 08/26|物販|1,016|-633 sqlite> SELECT description, sum(amount) FROM expenses GROUP BY description; 入|-16762 物販|-20767.0 カード|42.0 # 明らかにおかしい sqlite> SELECT sum(amount) FROM expenses; -37487.0 # 集計もうまくできていない