Zaim用データ変換Pythonスクリプトを改良

前回、ZaimのエクスポートデータをLet's家計簿で読めるように変換するPythonスクリプトを作りました。
alasixosaka.hatenablog.com
しかし、少々使いづらいところがあったので改良を行いました。
具体的な改良点は3点。

  1. スクロールに対応
  2. 口座の情報をデフォルトで入れるように変更
  3. 日付のフォーマットを修正

スクロールに対応

前回のスクリプトでは多数のレシートを読み取った場合に画面からはみ出してしまって見えなくなるという不具合がありました。
エクスポートするデータの件数を絞れば対処できますが、そうすると読み取ったデータを一度に処理することができないのでこのままでは少々不便。なのでスクロールできるようにに改良しました。
PythonGUIであるTkinterにはスクロールバーという機能があり、これを使えばよいのですが、今回のスクリプトのようにたくさんのウィジェットをいっぺんにスクロールしようとすると少し工夫がいります。
前回は、Tkinterの親画面であるrootにframeを貼りつけ、ここにラベルとコンボボックスを直接貼り付けていました。こうすると実はスクロールバーが使えないのです。そこで、canvasという主にグラフィックなどを書くためのウィジェットを使います。まず、rootにcanvasを貼りつけ、その中にframeを貼りつけ、frameにラベルとコンボボックスを貼りつけます。スクロールバーはrootに貼り付け、canvasの横につけます。
文字で書くとややこしいですがこんなイメージです。

f:id:alasixOsaka:20210130184544j:plain
左が前回。右が改良後。

canvasをスクロールするのに色々試行錯誤していじくっているので前のスクリプトと見た目が結構変わってしまっていますがやっていることはほぼ一緒です。まず関係するところだけ抜き出します。

root = tk.Tk()
root.title(u"Zaimデータ変換")
root.geometry("500x500")


~ 省略 ~

#キャンバスエリア
canvas = tk.Canvas(root, width = 400, height = 500)#Canvasの作成

frame = tk.Frame(canvas)

ます、500×500のrootを作成し、それに400×500のcanvasを貼りつけます。そしてそのcanvasにさらにframeを貼りつけています。
ウィジットを貼りつける部分はframeにウィジットを貼りつけるので処理は同じです。ウィジットをframeに貼り付けた後、canvas.create_window(0, 0, anchor='nw', window=frame)として、canvas中の左上にframeを表示させ、
scroll = tk.Scrollbar(root, orient="vertical", command=canvas.yview)としてスクロールバーをrootに貼りつけます。scroll.grid(row=0, column=1, sticky='ns')でスクロールバーは、0行、1列目に配置。sticky='ns'は上下方向に貼りつくという意味です。canvascanvas.grid(row=0, column=0, ) として0行、0列に置いています。次に canvas.configure(yscrollcommand=scroll.set)としてスクロールバーを有効化し、最後にcanvas.configure(scrollregion=canvas.bbox("all"))として、スクロール範囲を指定しています。今回は全スクロールを指定してます。

    for i in range(m):

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

        k+=1
    canvas.create_window(0, 0, anchor='nw', window=frame)
    scroll = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
    scroll.grid(row=0, column=1, sticky='ns')
    canvas.grid(row=0, column=0, )  # Canvasの配置
    canvas.configure(yscrollcommand=scroll.set)

  ~ 省略 ~

    canvas.configure(scrollregion=canvas.bbox("all"))

これでラベルとコンボボックスをずらずらと貼り付けたcanvasがスクロールします。

口座情報のデフォルト設定

zaim を使っていて気がついたんですが、レシートの項目にメモという自由記述欄があります。メモに口座情報を入力しておけばエクスポートしたデータに反映されるので、こいつを読み込んでコンボボックスにデフォルトで表示させるようにしました。メモ欄の入力は自由記述なので何でも入力する事ができます。ですがプログラム上では、コンボボックスにプルダウンで表示するテキストと比較しているので両者が正確に一致する必要があります。一致するテキストがリストにない場合コンボボックスのデフォルトは空欄になります。
実はこの空欄は便利な使い方があって、既に一旦Let's家計簿に読み込んであるデータも一緒にエクスポートしてしまった場合、コンボボックスを空欄にして口座情報を入力しないでおくとLet's家計簿にインポートされません。ですので重複してインポートされないようにするために便利に使えます。zaim のデータエクスポートは日付指定でエクスポートされるので、後から古いレシートが出てきてスキャンした場合など、既にLet's家計簿にインポートしたデータも一緒にエクスポートされる事になります。こんな時にこの機能は便利に使えます。
まず、メモ欄を読み込むところですが、メモ欄はrow[7]に入りますので、それを一旦str=row[7]としてstrに代入し、nullでなければkozaxに代入。if shop!=oshop以下の部分(店の名前が更新されたところ)でkozal.appned(kozax)として口座情報をリストkozalに追記しています。

    for row in reader:

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

口座情報の表示は、ok_ckick関数のところで、if kozal[k] in valuelistとしてkozalのリストの中身とvaluelistを比較して一致するものがあれば、kozai=valuelist.index(kozal[k])でリストの何番目と一致するかをkozaiに代入し、list_item[k].current(kozai)としてデフォルトで表示するようにしています。

def ok_click(m):
    ~省略~
    valuelist = ['現金', ’カード', 'ICOCA']
    for i in range(m):

        list_Items[k] = ttk.Combobox(frame,values=valuelist,width=10)
        list_Items[k].grid(row=i+1, column=2)
        if kozal[k] in valuelist:
            kozai=valuelist.index(kozal[k])
            list_Items[k].current(kozai)
       

日付のフォーマットの修正

zaim からエクスポートしたデータの日付のフォーマットは“2021-01-20”というように年と月、月と日の間をハイフンでつないだ形式になっています。Let's家計簿では日付のフォーマットは“2021/01/20”というようにハイフンではなくスラッシュを使わないと正しく読んでくれません。
実はこれははじめのうちは気がついていませんでした。何故かと言うとはじめのうちは確認のため一旦エクセルで開いてからセーブしたデータをLet's家計簿にインポートしていたからです。一旦エクセルで開いてからセーブすると何故か日付のフォーマットが変わってしまってLet's家計簿に対応したフォーマットになっていました。この事自体知らなかったのですからpython で変換したデータを直接Let's家計簿にインポートする迄全く気が付きませんでした。
ハイフンをスラッシュに変換するのは簡単で、date = row[0].replace("-","/")とすればできます。

実行結果はこんな風になり、口座情報の欄はメモに記入した情報がデフォルトで表示されます。また、あふれた行はスクロールで表示させることができます。

f:id:alasixOsaka:20210205204100j:plain
メモに入れた現金がデフォルト表示、行があふれたらスクロールできるようになった。

全文です。

import csv,sys,os,tkinter.filedialog, tkinter.messagebox
import tkinter as tk
import tkinter.ttk as ttk

def ok_click(m):
    list_Items = [0]*m
    shop_Items = [0]*m
    date_Items = [0]*m
  
    k=0
    N=m
     valuelist = ['現金','カード', 'ICOCA']
    for i in range(m):

        list_Items[k] = ttk.Combobox(frame,values=valuelist,width=10)
        list_Items[k].grid(row=i+1, column=2)
        if kozal[k] in valuelist:
            kozai=valuelist.index(kozal[k])
            list_Items[k].current(kozai)
        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
    canvas.create_window(0, 0, anchor='nw', window=frame)
    scroll = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
    scroll.grid(row=0, column=1, sticky='ns')
    canvas.grid(row=0, column=0, )  # Canvasの配置
    canvas.configure(yscrollcommand=scroll.set)

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

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

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

    frame.update_idletasks()
    canvas.configure(scrollregion=canvas.bbox("all"))
#
# GUI設定
#
root = tk.Tk()
root.title(u"Zaimデータ変換")
root.geometry("500x500")
#frame_main = tk.Frame(root)
#frame_main.grid(sticky='news')

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

koza = ""
kozal =[]
kozai = []

#キャンバスエリア
canvas = tk.Canvas(root, width = 400, height = 500)#Canvasの作成

frame = tk.Frame(canvas)

#
# GUIの末端
#

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

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

    for row in reader:

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

        if n == 0:
            str = row[1]
            n = n + 1
m = len(shoplist)

ok_click(m)

root.mainloop()

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]
            if (shop!=shoplist[n-1])&(shop!="-"):
                n = n +1
            koza = kozal[n-1]
            date = row[0].replace("-","/")
            data.append(date)
            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 n == 0:
            data.append(str)
            writer.writerow(['日付','お店','内容','収入','支出','費目','口座'])
            n = 1
            data.clear()