ZaimのデータをLet's家計簿用に変換する

長年愛用していた家計簿ソフトのやさしく家計簿が使えなくなったので、ScanSnapとZaimの組み合わせに変えたところ、かなり使い勝手良くなった。
alasixosaka.hatenablog.com
しかし、Zaimのデータを最終的にLet's家計簿に読み込む必要があり、データ変換が必要だった。といっても、そんなに大した手間でもないのだが、pythonでこの変換を一発で行うスクリプトを書いてみた。
まず、元のデータはこんな感じになっている。

f:id:alasixOsaka:20210124084320j:plain
ZaimのWebサイトからエクスポートした元のCSVファイル。これをLet's家計簿用に変換する

Let's家計簿で読み込むためには、一行目がタイトル行で”日付,お店,内容,収入,支出,費目,口座”となっていて、二行目から各タイトルに応じたデータが格納されている必要がある。また、上の図を見てわかるように、口座に対応する情報がない、更に、お店に関しては、同じお店で買った物品については二行目以降は”ー”となっていてお店の名前が無い状態になっているので二行目以降もお店の名前を入れる必要がある。この辺りも一度にやってしまいたい。

PythonCSVファイルを読み込む

まず、元のデータのCSVファイルを読む必要がある。PythonではCSVモジュールというのが標準で実装されているのでそれを使えば良い。なお、使ったPythonは3.x系で、windows10でPycharmを使って開発した。
例えばFドライブにあるZaimというフォルダにあるZaimTest.CSVというファイルを読み込む場合。

import csv

with open('f:\zaim\zaimtest.csv') as f:
reader = csv.reader(f)
for row in reader:

としてやればよい。
書き出すときも同様で、同じフォルダにZaimTest_Lets.CSVというファイルを作ってそこに書き込む場合は、

with open('f\zaim\zaimtest_lets.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow([0, 1, 2])

列を入れ替えて出力する

単純にZaimのデータからLet's家計簿ように列のデータを入れ替える場合を考えると、日付が1列目、お店が9列目、内容が7列目、収入が11列目、支出が12列目、費目が4列目(ないし3列目)、口座はなしとなっているのでこれを順番に読みだして書いていけばよい。

import csv

data = []
n = 0

with open('f:\zaim\zaimtest.csv') as f, open('f\zaim\zaimtest_lets.csv', 'w') as g:
reader = csv.reader(f)
writer = csv.writer(g, lineterminator="\n")
for row in reader:
     if n = 0
          writer.writerow['日付','お店','内容','収入','支出','費目','口座']
     if n != 0
          data.append(row[0])
          data.append(row[8])
          data.append(row[6])
          data.append(row[10])
          data.append(row[11])
          data.append(row[3])
          data.append(" ")
          writer.writerow(data)
          data.clear()
     n=n+1

読み込みようのファイルはfとしてオープンし、書き込み用のファイルはgとしてオープンして同時に読み書きしている。
また、元ファイルの1行目はタイトル行なので、空読みをして、Let's家計簿のタイトル行を書き込んでいる。一行書いた後にデータをクリアして、一行ずつデータを書き込んでいる。全部のデータをアペンドしてから書き込むと改行がうまくできないのでこういう書き方にしてみた。もっとスマートに書く方法があるように思うがとりあえず動けば良いということでこうしてみた。

ファイル名を指定して読み込む

一応列の入れ替えはできたが、このままでは読み込むファイルが変わるといちいちプログラムを修正する必要があるので実際には使い物にはならないので、ファイルを指定し読み込めるようにする。

import csv, os, tkinter, tkinter.filedialog, tkinter.messagebox
import tkinter.ttk as ttk
from tkinter import messagebox as mbox

root = tkinter.Tk()
root.geometry("300x300")
fTyp = [("", "")]
iDir = os.path.abspath(os.path.dirname(__file__))
tkinter.messagebox.showinfo('oxプログラム', '処理ファイルを選択してください')
file = tkinter.filedialog.askopenfilename(filetypes=fTyp, initialdir=iDir)
ffile = file.replace(".csv", "_lets.csv", 1)

with open (file) as f, open(ffile , 'w') as g:

なんかおまじないみたいだが、参考サイトのプログラムをほぼそのまま使わさせてもらった。Pythonでメッセージや何かの入力画面などを表示するのにTkinterというモジュールを使う。一行目でTkinterとファイルダイアログ、メッセージボックスを読み込んでこれらを使ってメッセージとファイル入力用の画面を出力している。選んだファイル名はfileに格納される。出力用のファイル名をファイル名の末尾に”_lets"とつけてffileとしている。

お店と口座を修正

一応、列を入れ替えて基本形はできたので、これをエクセルで開いてお店や口座の情報を整えても使えるのだが、それでは面白くないのでお店と口座もPythonで一気に修正してしまう。
まずは、お店の部分だが、Zaimでデータを出力した場合、同じお店で複数の品を購入したときに、二行目以降のデータが”ー”になる場合と、お店の名前になっている場合がある。後者の場合は修正は必要ないが、”ー”となっているとLet's家計簿で読み込んだときにお店の名前が”ー”となってしまうので修正しておく必要がある。
shoplistという配列を用意し、お店の名前が変わるたびに順番に入れておくようにする。お店の名前が変わったかどうかの判断は一つ前の行のデータをoshopに残しておいて、次の行のデータshopと比較している。ただ、shopが”ー”の場合は同じお店であると判断している。

    oshop = "-"
    shop =""
    shoplist = []

    for row in reader:

        if n != 0:
            shop = row[8]
            date = row[0]
            if shop == "-":
                shop = oshop
            if shop != oshop:
                shoplist.append(shop)
            oshop = shop
        n = n + 1

書き込むところは、nを変数にして、お店が変わるたびにnを更新して、shoplistからお店の名前を書き込んでいる。

for row in reader:

        if n != 0:
            shop = row[8]
            data.append(row[0])
            data.append(shoplist[n-1])
            data.append(row[6])
            data.append(row[10])
            data.append(row[11])
            data.append(row[3])
            data.append(" ")
            writer.writerow(data)
            data.clear()
            if (shop!=shoplist[n-1])&(shop!="-"):
                n = n +1


次は口座になるが、これが一番苦労した。口座に関してはいつも使う口座が決まっているのでプルダウンメニューから選択する形にした。例えば現金で買った場合には現金、カードで買った場合にはカードを選択できるようにしたかった。プルダウンメニューを表示するには、Tkinterのコンボボックスを使う。始めは、お店を1軒ごとに現金かカードなのかというのを選択してクリックする形にしようと考えた。しかし、Tkinterでは何かを入力してその値を使って処理をしようとするとroot.mainloop以降に処理を書かないといけないので、値はいっぺんに入れてしまった方が処理が楽なので、表形式で、何月何日のどのお店、例えば1月1日にセブンイレブンで買ったのは現金払いだったというような感じで入力するようにした。一覧にした時にお店の名前だけでは判断がつかないので日付も表示させるようにした。日付の取得は上のプログラムには書いてないが、お店の名前を取得するときにdatelistという配列にいれてある。
一覧表示には、フレームとグリッドというのを使った。まず、frame = ttk.Frame(root)でフレームを作成し、グリッドを使って日付、お店の名前、コンボボックスを表示している。コンボボックスに表示する選択肢はここでは現金、カード、ICOCAから選択できるようにして、valuelistという配列に入れている。コンボボックスをlist_Items[k]に入れて、row=i+1として一行ずつ並べていく、列は3列目にしている。kとiはループ関数。同じでよいと思うが参考サイトに倣って別々にした。

list_Items[k] = ttk.Combobox(frame,values=valuelist,width=10)
list_Items[k].grid(row=i+1, column=2)

日付、お店の名前も同様にdate_Itemsとshop_Itemsに入れて、1列目、2列目に並べる。そして、一番下にボタンを配置してボタンをクリックすることで入力値が取得できるようにした。def ButtonClicked_Run():以下の部分。得られた値はkozalという配列に格納される。呼び出し関数の最後にroot.destroy()を書いて、ボタンをクリックするとウィンドウが閉じるようにしている。
そして、全体をok_clikという関数にして呼び出している。呼び出しの引数はmでこれはお店のリストの要素数を入れている。

def ok_click(m):
    list_Items = [0]*m
    shop_Items = [0]*m
    date_Items = [0]*m
    frame = ttk.Frame(root)
    frame.grid(row=0, column=0)

    k=0
    N=m

    for i in range(m):

        valuelist = ['現金','カード','ICOCA']
        list_Items[k] = ttk.Combobox(frame,values=valuelist,width=10)
        list_Items[k].grid(row=i+1, column=2)
        shop_Items[k] = ttk.Label(frame,text=shoplist[k])
        shop_Items[k].grid(row=i + 1, column=1)
        date_Items[k] = ttk.Label(frame,text=datelist[k])
        date_Items[k].grid(row=i+1,column=0)

        k+=1

    def ButtonClicked_Run():
        B = [0]*(N)

        for i in range(N):
            B[i] = list_Items[i].get()
            kozal.append(B[i])
        root.destroy()

    button_Run = ttk.Button(root, text='実行', padding=5, command=ButtonClicked_Run)
    button_Run.grid(row=1, column=0)

    root.mainloop()

実行するとこんな感じになる。ハックルベリーはまだ選択していない状態なので空欄になっている。このまま実行を押すと、空欄のままヌルストリングがリストに入る。

f:id:alasixOsaka:20210124100313j:plain
口座選択のプルダウンメニュー

全体のプログラムは次のようになった。

import csv, os, tkinter, tkinter.filedialog, tkinter.messagebox
import tkinter.ttk as ttk
from tkinter import messagebox as mbox

root = tkinter.Tk()
root.geometry("300x300")
fTyp = [("", "")]
iDir = os.path.abspath(os.path.dirname(__file__))
tkinter.messagebox.showinfo('oxプログラム', '処理ファイルを選択してください')
file = tkinter.filedialog.askopenfilename(filetypes=fTyp, initialdir=iDir)
ffile = file.replace(".csv", "_lets.csv", 1)

koza = ""
kozal = []
m=0

def ok_click(m):
    list_Items = [0]*m
    shop_Items = [0]*m
    date_Items = [0]*m
    frame = ttk.Frame(root)
    frame.grid(row=0, column=0)

    k=0
    N=m

    for i in range(m):

        valuelist = ['現金','カード','ICOCA']
        list_Items[k] = ttk.Combobox(frame,values=valuelist,width=10)
        list_Items[k].grid(row=i+1, column=2)
        shop_Items[k] = ttk.Label(frame,text=shoplist[k])
        shop_Items[k].grid(row=i + 1, column=1)
        date_Items[k] = ttk.Label(frame,text=datelist[k])
        date_Items[k].grid(row=i+1,column=0)

        k+=1

    def ButtonClicked_Run():
        B = [0]*(N)

        for i in range(N):
            B[i] = list_Items[i].get()
            kozal.append(B[i])
        root.destroy()

    button_Run = ttk.Button(root, text='実行', padding=5, command=ButtonClicked_Run)
    button_Run.grid(row=1, column=0)

    root.mainloop()

with open (file) as f:
    reader = csv.reader(f)

    n = 0
    data = []
    str =""
    oshop = "-"
    shop =""
    date =""
    shoplist = []
    datelist =[]

    for row in reader:

        if n != 0:
            shop = row[8]
            date = row[0]
            if shop == "-":
                shop = oshop
            if shop != oshop:
                shoplist.append(shop)
                datelist.append(date)
            oshop = shop
        n = n + 1
m = len(shoplist)

ok_click(m)

n=0
with open (file) as f, open(ffile , 'w') as g:
    data.clear()
    reader = csv.reader(f)
    writer = csv.writer(g, lineterminator="\n")
    for row in reader:

        if n != 0:
            shop = row[8]
            koza = kozal[n-1]
            data.append(row[0])
            data.append(shoplist[n-1])
            data.append(row[6])
            data.append(row[10])
            data.append(row[11])
            data.append(row[3])
            data.append(koza)
            writer.writerow(data)
            data.clear()
            if (shop!=shoplist[n-1])&(shop!="-"):
                n = n +1

        if n == 0:
            data.append(str)
            writer.writerow(['日付','お店','内容','収入','支出','費目','口座'])
            n = 1
            data.clear()


参考にしたサイト
PythonのTkinterを使ってみる - Qiita
PythonでCSVファイルを読み込み・書き込み(入力・出力) | note.nkmk.me
Python:処理ファイルをGUIから選択する方法 - Qiita
PythonGUI(tkinterコンボボックス)で行列を作成 - Qiita
ボタンを使って Tkinter ウィンドウを閉じる | Delft スタック