ファイルの差分を抽出する

先月から今月の間の新規顧客を調べたい、なんてことがあったとしてください。
それぞれの顧客には、独自の顧客コードが割り振られていて、いろんな情報が蓄積されていると思ってください。
その中に、家族に割り振られているコード(当然複数人に割り振られている)、主に支払う人(家族コードと1対1)、住んでいる場所(複数対複数ですな)、というようなデータがあるとします。

顧客データは、月末に全データをタブ区切りで保管してありますので、それぞれの差分を調べると、新規に顧客になって頂いた方がわかるということです。

頭の部分が以下のようなデータが、居酒屋分と食堂分あると思っていただけるとありがたい。

member_id  member_name  family_code	osaifu_meigi	tiiki_code    
00000663  かおる  000AZ45RR    たけさん    022
00000664  あかりさん  000025WEF    ごろーちゃん    054
00000665  けんちゃん  000032GTY    下田さん    054
00000666  しんじ  000025WEF    ごろーちゃん    054

今回は、どの家族が増えてるか、というのが知りたくて、食堂の場合だけは、どの地域が増えたのか、というのも知りたいという場合です。
その上に、地域の順番は、単純なソートでは無いという状況です。
(東京駅の近くだけは、まとめて見たい、みたいな感じですかね)
ちなみに、今回は、月末に保管するときに、SQLを使うことで、希望の地域の順番で出力することはできています。
つまり、データの順番どおりに新しい家族を拾っていけば良い、ということです。

会社で使ってるのをそのまま公開するのは、ちと難しいので、いろいろと考えてるんですが、しっくりいって無い感はありますな。
まぁ、その辺りは、大人な対応ということで...

ということで、プログラム自体は以下になります。

# -*- coding: utf-8 -*-
# Created on Thu Nov 11 15:08:05 2021
# 新規に登録された家族情報を抽出する
# 家族コード、財布な人、地域コード、で新規かどうかを判断する
# SQLで抽出したデータは、書いてある順番通りに出力することを想定している
# 
# GUIで行うこと
#  居酒屋か食堂かを選択
#  新しいファイルと古いファイルを選択
#  ファイル作成ボタン
#  作成終了時にポップアップ
# 具体的な作業内容
#  設定ファイルを読み込む(入力フォルダと出力フォルダを定義する)
#  古いファイルで「家族コード、財布な人、地域コード」の集合を作る
#  新しいファイルを1行づつ読む
#  古いファイルに無くて、新家族データに無ければ、新家族データに追加する
#  新地域データの数を増やす
#  
import configparser
import PySimpleGUI as sg
import sys
from datetime import datetime
# config.iniがあるかどうか確かめて無かったりしたら終了
try:
   fin = open('config.ini','r')
except FileNotFoundError:
   sg.popup('プログラムと同じフォルダにconfig.iniが必要',title='注意!!')
   sys.exit()
except Exception() as other:
   sg.popup('なんか知らんがエラーです ',other,title='注意!!')
   sys.exit()
fin.close()
   
#  設定ファイルを読み込む
cfg = configparser.ConfigParser()
cfg.read('config.ini')

# Windows Layout
layout = [  [sg.T('店の種類選択'),sg.Radio('居酒屋', 'radio', key='izaka',default=True,enable_events = True),sg.Radio('食堂','radio',key='syoku',enable_events = True)],
           [sg.In(key='old_file'),sg.FileBrowse('旧ファイル',target='old_file',initial_folder=(cfg['folder']['in']),key='input1')],
           [sg.In(key='new_file'),sg.FileBrowse('新ファイル',target='new_file',initial_folder=(cfg['folder']['in']),key='input2')],
           [sg.Button('作成',key='go')]  ]
# Make Windows Title=新規登録抽出
window = sg.Window('新規登録抽出', layout) 
# ファイル選択窓内を消すためにオブジェクトを設定する
ip1 = window['old_file']
ip2 = window['new_file']

# Main loop
while True:
   event, values = window.read()
   
   print(event, values)
   
   # これがないと無限ループになります
   # プログラムの最後にwindow.close()を書いて終了にする
   if event == sg.WIN_CLOSED: #ウィンドウのXボタンを押したときの処理 
       break
       
   # ラジオボタンが押された場合
   if (event == 'izaka') or (event == 'syoku'):
       ip1.Update('')
       ip2.Update('')
       
   # 作成ボタンが押された場合
   if event == 'go':
       # 選択したのとは違うファイルを選んでいたら注意を促す
       if not((values['izaka'] and values['old_file'].find('居酒屋') > 0 and values['new_file'].find('居酒屋') > 0) or (values['syoku'] and values['old_file'].find('食堂') > 0 and values['new_file'].find('食堂') > 0)):
           sg.popup('店種とファイルの関係が間違ってます',title='注意!!')
           continue
       # 古いファイルで、タブ区切りの「家族コード、財布な人、地域コード」の集合を作る
       fin = open(values['old_file'],'r',encoding='shift_jis')
       # 1行ずつ読んで集合にする
       duml0 = fin.readlines()
       duml1 = []
       for dd in duml0:
           dlist = dd.split('\t')
           dlist1 = [dlist[2], dlist[3], dlist[4].zfill(4)]
           #print (dlist1)
           duml1.append(('\t'.join(dlist1)))
       old_data = set(duml1)
       fin.close()
       # 新しいファイルを1行づつ読む
       fin = open(values['new_file'],'r',encoding='shift_jis')
       # 1行ずつ読んでリストを作る
       duml0 = fin.readlines()
       # 新規追加データリストを用意する
       new_data = ['family_code	osaifu_meigi	tiiki_code']
       # 食堂の時には、地域用のデータリストと家族数用の辞書も用意する
       if values['syoku']:
           tiiki_list = []
           tiiki_data = {}
           
       for dd in duml0:
           dlist = dd.split('\t')
           dlist1 = [dlist[2], dlist[3], dlist[4].zfill(4)]
           # 古いファイルに無くて、新メンバーデータに無ければ、新メンバーデータに追加する
           if ('\t'.join(dlist1) not in old_data) and ('\t'.join(dlist1) not in new_data):
               new_data.append(('\t'.join(dlist1)))
               # 食堂だったら新地域データの数を増やす
               if values['syoku']:
                   # リストに入っている場合は、辞書の会員数を増やす
                   if dlist[4].zfill(4) in tiiki_list:
                       tiiki_data[dlist[4].zfill(4)] += 1
                   # 入っていない場合は、新しいリストと辞書を作る
                   else:
                       tiiki_list.append(dlist[4].zfill(4))
                       tiiki_data[dlist[4].zfill(4)] = 1
       
       # それぞれのファイルを作成する
       # 今の時間を求める
       now = datetime.now()
       # 居酒屋と食堂で用意するファイルが違う
       if values['izaka']:
           fout = open(cfg['folder']['out'] + '居酒屋new_member' + now.strftime('%Y%m%d%H%M%S') + '.tsv','w',encoding='shift_jis')
       else:
           fout = open(cfg['folder']['out'] + '食堂new_member' + now.strftime('%Y%m%d%H%M%S') + '.tsv','w',encoding='shift_jis')
           fout1 = open(cfg['folder']['out'] + '食堂tiiki_list' + now.strftime('%Y%m%d%H%M%S') + '.tsv','w',encoding='shift_jis')
           
       # new_dataを書き込む
       for dd in new_data:
           fout.write(dd + '\n')
       fout.close()
       # 食堂の場合は、地域データも作成
       if values['syoku']:
           for dd in tiiki_list:
               fout1.write(dd + '\t' + str(tiiki_data[dd]) + '\n')
           fout1.close()

       sg.popup('作成しました',title='終了')
       #  
window.close()

このプログラムの勘所

・設定ファイルを使ってみた

毎回好き勝手な設定ファイルを作るよりも、ちゃんとしたフォーマットのファイルを使った方が良い、という、どなたかの教えを守ってみようかと思った次第でございます。

・ラジオボタンを押したらファイル選択欄の文字を消す

そもそも、ラジオボタン押してもイベントが発生しない、と思っていたので、どうしたもんかと困っておりました。
ところが、マニュアルを見ていたら、オプションに「enable_events = True」ってのを設定すると、イベントとして拾ってくれることが分かりました。
ということで、無事に目的の動作をしてくれるようになりましたな。

・古いデータは、存在有無を調べるだけなので、集合にする

ファイルから読む時はリストに入れますが、知りたいのは、そこにあるかどうかだけなので、集合にしてみました。
もしかしたら、早くなってるのかもしれません。

・辞書データを順番どおりに出力するならリストも使う

他にちゃんとした方法があるのかもしれませんが、簡単な方法にしてみました。
順番を覚えておくために、辞書データを作るときに一緒にリストも作ってます。