python #2.9 決まり字checker ver4

 改善点

・while文に、できるだけbreak以外の情報を入れないようにした(breakを書く前の作業がbreak後でも可能な場合、後で行う方がコードを読みやすい)

・list_into_dic関数の不備を修正; X = input('追加するキーの名前を入力してください:') の前にあったwhile Trueが不要だったため削除

・kmrj関数の不備を修正
   * strB + '重複' → strB + '(重複)' に訂正
   * strB が strA で始まる場合、strB の決まり字は  strB[:len(strA)+1]ではなくstrB[:len(strA)]

・for文の改善, 内包表記の使用など

・コメントアウトの補完

など。

 ちなみに、今回から Visual Studio Code を使用している。段を変える処理が格段に楽になったほか、デバッグがリアルタイムで行われるためタイプミスなどにも気付けて非常に良い。

import tkinter as tk
import tkinter.filedialog
from pykakasi import kakasi
import regex
import csv

# 文字種 (ひらがな, 漢字, 長音符以外の記号) を判別するメソッド

p = regex.compile(r'[\p{Script=Hiragana}\p{Script=Han}]+')
pAll = regex.compile(r'[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}ーa-zA-Z0-9]+')

# 漢字をひらがなに直すメソッド (ひらがなはひらがなのまま残る)

kakasi = kakasi()
kakasi.setMode('J', 'H')
conv = kakasi.getConverter()
               
# 文字列 s のうち、漢字・ひらがなの部分だけをひらがなに変換してできる文字列 conv_s を返す関数 converter(s) を定義
def converter(s):
   # (ファイル読み込み後なので基本的には不要だが、) 一応数値等である場合を考慮して s を文字列に変換する
   s = str(s)
   # まず、文字列 s のうち漢字かひらがなの箇所だけ、ひらがなに変換する         
   # 「漢字かひらがななのは、何番目の文字か」「それ以外なのは、何番目の文字か」を num_listA, B に保存する
   num_listA = []
   num_listB = []
   for i, ch in enumerate(s):
       if p.fullmatch(ch):
           num_listA.append(i)
       else:
           num_listB.append(i)
   # 空の文字列 conv_s を設定し、そこに文字列を足していく方針をとる
   conv_s = ''        
   # num_listA の要素数が 0 の場合、漢字もひらがなも含まれないので、変換は不要
   if len(num_listA) == 0:
       conv_s += s
   # num_listA の要素数が 0 でない場合、漢字かひらがなが含まれるので、変換が必要
   else:            
       # num_listB の要素数が 0 である場合、漢字かひらがなしか含まれないので、丸ごと変換すれば良い
       if len(num_listB) == 0:
           conv_s += conv.do(s)                
       # num_listB の要素数が 0 でない場合
       # 方針:漢字・ひらがなの連続部を一括変換し、漢字・ひらがな以外の連続部をセットで保存したものと組み合せたい
       ### 例:s = '2020年12月31日木曜日' のとき                
       ### '2020漢12漢31' と 'ねんaがつaにちもくようび' という文字列を作り、'漢' 'a' で split してリストに変換する
       ### zip 関数を使い 2 つのリストから交互に取り出して、もう一度一つの文字列 '2020ねん12がつ21にちもくようび' に戻す
       # 「連続かどうか」は、num_list における次の数字との <差> が 1 かどうかで判定
       else:
           # 先に、変換のない「漢字・ひらがな以外」の方 (num_listB の方) を処理する                
           ### 文字列 stringB の初期値に s のうち 1 文字目の漢字 or ひらがなを設定する
           ### これで、<差> を考えることのできない場合 (num_list の要素数が 1 の場合) もカバーできる                
           stringB = s[num_listB[0]]
           # num_listB の要素数が 2 以上の場合
           ### stringB に次の「漢字・ひらがな以外」を足していく
           ### ただし、num_listB の [i] に保存されている番号と、[i+1] に保存されている番号の差が 1 でない場合,
           ### '漢' を足してから 次の「漢字・ひらがな以外」 を足す
           if len(num_listB) >= 2:
               # 差をとれる回数は num_list の要素数より 1 少なくなる
               for i in range(len(num_listB)-1):
                   # 差が 1 でない場合のみ '漢' を足してから
                   if num_listB[i+1] - num_listB[i] != 1:
                       stringB += '漢'
                   # stringB に 次の「漢字・ひらがな以外」 を 追加する
                   stringB += s[num_listB[i+1]]
           # stringB を '漢' で区切ってリスト (listed_B) に変換
           listed_B = stringB.split('漢')                    
           # 次に、変換のある「漢字・ひらがな」の方 (num_listA の方) を処理する
           # listed_B と同じ要領で、listed_a を作る
           stringA = s[num_listA[0]]
           if len(num_listA) >= 2:
               for i in range(len(num_listA)-1):
                   if num_listA[i+1] - num_listA[i] != 1:
                       stringA += 'a'
                   # stringA に「漢字・ひらがな」の文字を一文字追加する
                   stringA += s[num_listA[i+1]]
           # stringA を 'a' で区切ってリストに変換
           listed_a = stringA.split('a')
           # listed_a の各要素をひらがなに変換した listed_A を作る
           listed_A = []
           for s_a in listed_a:
               listed_A.append(conv.do(s_a))
           # listed_A, B に対して zip 関数を使い、2 つのリストから交互に取り出した文字列を 1 つの文字列に統合
           ### ただし、2 つのリストは ['がもつ'], ['list', 'メソッド'] のように長さが違う場合がある
           ### zip 関数は短い方に長さを合わせるため、長くない方の最後に '' を足してから zip 関数を使用する
           ### 「長くない方」: 長さが等しいところに '' を足しても、短い方に揃えられて '' が消えるため、不都合はない
           # s の 1 文字目が漢字・ひらがなの場合、listed_A は B より長いか同じ長さなので、B に '' を足す
           if 0 in num_listA:
               listed_B.append('')
               for strA, strB in zip(listed_A, listed_B):
                   conv_s += strA + strB                            
           # s の 1 文字目が「漢字・ひらがな以外」の場合、listed_B は A より長いか同じ長さなので、A に '' を足す
           else:
               listed_A.append('')
               for strB, strA in zip(listed_B, listed_A):
                   conv_s += strB + strA
   # 戻り値として conv_s を返す
   return conv_s

# 文字列 s のうち、長音符以外の記号を削除してできる文字列 remov_s を返す関数 remover(s) を定義する
def remover(s):
   # (ファイル読み込み後なので基本的には不要だが、) 一応数値等である場合を考慮して s を文字列に変換する
   s = str(s)
   remov_s = ''
   # s から 1 文字ずつ取り出し、その文字が 「長音符以外の記号」以外 であれば remov_s に追加する。
   for ch in s:
       if pAll.fullmatch(ch):
           remov_s += ch
   return remov_s

# 複数のリスト a, b, c, .. を、辞書のリスト l_of_dics に組み込む関数 list_into_dic を定義する
### [属性Aの情報1,2,..,n],[B ],[C ],.. 内の情報を、
### 既存の 辞書のリスト [{X:--, Y:--, ..},{},..,{}] 内の n 個の辞書に追加する
### a に * をつけることで、2 個目以降の引数をまとめてタプルとして受け取らせて、複数リストを扱えるようにする
def list_into_dic(l_of_dics, *a):
   # タプルを for ループで展開する (else 以下で使用するので、番号も同時に取得しておく)
   for i, x in enumerate(a):
       # 組み込みたいリスト と 辞書のリスト の長さが同じである場合にのみ動作させる
       if len(x) == len(l_of_dics):
           # 追加するキーの名前を入力する
           X = input('追加するキーの名前を入力してください:')
           # 追加するキーの名前が既に存在するキーにはなかった場合、問題ないので目的の作業に進めばよいが、
           ### 追加するキーの名前が既に存在するキーと一致している場合、データが上書きされてしまうので対応が必要
           if X in l_of_dics[0]:
               print(X, 'キーは既に存在するため、すべての値が上書きされますがよろしいですか?')
               # 上書きしていいのか、別のキー名に変えたいかを尋ねる
               while True:
                   q = input('上書きする場合は[y]を、別のキー名に変える場合は[n]を入力してください:')
                   if q == 'y' or q == 'n':
                       break
                   else:
                       print('半角英字の[y]か[n]を入力してください')
               # 上書きする場合は、外の while ブロックから脱出して目的の作業に進む
               if q == 'y':
                   break
           # 組み込みたいリスト の内容を 辞書のリスト に追加する作業
           for j, s in enumerate(x):
               # l_of_dics の j 番目に保存されている辞書に、キー X, 値 s (= x[j]) の組を追加する
               l_of_dics[j][X] = s
       # リスト と 辞書のリスト の長さが異なる場合
       else:
           print(i + '番目のリストを組み込めません. 要素数を揃えてください.')
   return l_of_dics

# id を振る関数 id_maker を定義する
def id_maker(l_of_dics):
   keys = list(l_of_dics[0].keys())
   id_keys = []
   for key in keys:
       # id がすでにあれば、基本的には id を振る必要はないので、id らしきキーを列挙する
       if 'id' or 'ID' in key:
           id_keys.append(key)
   print('idらしきキーが', id_keys, 'である。')
   # id らしきキーを列挙した上で、それでも ID を振るのかどうか尋ねる
   while True:
       q = input('新規IDを振りますか? 振るなら[y],振らないなら[n]を入力してください:')
       if q == 'y' or q == 'n':
           break
       else:
           print('半角英字の[y]か[n]を入力してください')
   # ID を作る場合
   if q == 'y':
       id_list = [str(i) for i in range(len(l_of_dics))]
       id_l_of_dics = list_into_dic(l_of_dics, id_list)
       return id_l_of_dics
   # ID を作らない場合は元のまま返す
   else:
       return l_of_dics
   
# 辞書のリスト の一部を修正する関数 modifying_dic を定義する
def modifying_dic(l_of_dics):
   # キーを全て表示し、どのキーに対する値を print して確認するか決める
   ### どの辞書も同じキーを有しているとみなし、最初に保存されている辞書のキーを表示する
   key_list = list(l_of_dics[0].keys())
   print(key_list)
   # choices というリストに、修正が必要かチェックする上で表示しておきたいキーを保存していく
   ### 後で修正対象を指定しやすいように、'id' というキーがあれば表示することにする
   choices = []
   if 'id' in key_list:
       choices.append('id')
   while True:
       print('修正に使用するキーを一つずつ選んで入力し、[Enter]で確定してください。')
       K = input('全て選択し終えた場合は、[OK]と入力してください:')
       if K == 'OK':
           break
       elif K not in key_list:
           print('存在するキーを入力してください')
       else:
           choices.append(K)
   # 各辞書内の、choices に保存したキーに対応する値を ' | ' 区切りで表示する ... (*1)
   for dic in l_of_dics:
       s = ''
       for K in choices:
           s += K + ':' + dic[K] + ' | '
       print(s)
   # 修正箇所の有無を尋ねる
   while True:
       q = input('修正箇所がまだあれば[y]を, もうなければ[n]を入力してください:')
       if q == 'y' or q == 'n':
           break
       else:
           print('半角英字の[y]か[n]を入力してください')
   # 以下、修正箇所がある場合に修正を指示するコード
   if q == 'y':
       # id などを入力して、修正したい文字列 (の辞書) を指定する            
       while True:
           key = input('修正したい文字列を、どのキーに対する値で指定しますか? (例.[id]と入力):')
           val = input('そのキーに対する値を入力してください(例."id"が20なら[20]と入力):')
           # どの辞書も同じキーを有しているとし、0 番目の辞書にキー key があれば全ての辞書にキー key があるとみなす
           if key in l_of_dics[0]:
               break
           # key というキーがない場合、打ち込み直す
           else:
               print('その値はキーとして存在しません')
       # 辞書を1つずつチェックしていき、該当する文字列 (の辞書) を見つけたら修正する        
       for target in l_of_dics:
           # 辞書 target の key に対する値が val である場合だけ修正に入る
           if target[key] == val:
               # 辞書 target から、choices に保存したキーに対応する値を ' | ' 区切りで表示する ... (*2)
               ### *1 はすべての辞書から値を取り出すのに対し、*2 は target の辞書からのみ値を取り出す
               for K in choices:
                   print(K + ':' + target[K] + ' | ') 
               while True:                    
                   k = input('どのキーに属する値を修正しますか?:')
                   if k in target:
                       break
                   # k というキーがない場合、打ち込み直す
                   else:
                       print('入力された値はキーとして存在しません')
               # 辞書 target のキー k に対する値を新たなものに変更する
               target[k] = input('修正後の値を入力してください:')
   return l_of_dics

# 辞書のリストを、あるキーに対応する値に読み仮名を振り記号を削除したものを加え直した状態で返す関数 rubi を定義する
def rubi(l_of_dics):
   # キーの名前を確認するために 1 つめの辞書だけ表示する
   print(l_of_dics[0])
   # 特定のキーに対応する値のリストを作る
   while True:
       k = input('どのキーに属する値にルビを振りますか?:')
       # どの辞書も同じキーを有しているとし、0 番目の辞書にキー k があれば全ての辞書にキー k があるとみなす
       if k in l_of_dics[0]:
           break
       # k というキーがない場合、打ち込み直す
       else:
           print('そのようなキーは存在しません')
   # 辞書のリスト l_of_dics 内の各辞書から、キー k に対応する値を取り出し、リストに保存する
   target_vals = [dic[k] for dic in l_of_dics]
   # リストに保存されている各文字列にルビを振り、長音符以外の記号を削除し、別のリストに保存する
   new_vals = [remover(converter(value)) for value in target_vals]
   # リスト new_vals 内の情報を全て元の 辞書のリスト に登録する
   l_of_dics_2 = list_into_dic(l_of_dics, new_vals)
   # 最後に、修正したい場所を修正する
   l_of_dics_3 = modifying_dic(l_of_dics_2)
   return l_of_dics_3    

# 辞書のリストをソートして、ソートの基準としたキー名とともに返す関数 sorting_dic を定義する
def sorting_dic(l_of_dics):
   while True:
       k = input('どのキーでソートしますか?:')
       # どの辞書も同じキーを有しているとし、0 番目の辞書にキー k があれば全ての辞書にキー k があるとみなす
       if k in l_of_dics[0]:
           break
       # k というキーがない場合、打ち込み直す
       else:
           print('そのようなキーは存在しません')
   # 辞書のリスト l_of_dics を、キー k に対応する値の順番に並び替える
   s_l_of_dics = sorted(l_of_dics, key = lambda x:x[k])    
   # あとでキーを使いたいので 辞書のリスト と ソートしたキー をタプルで返す
   return (s_l_of_dics, k)

# 文字列 2 つを比較して、それらの決まり字をタプルで返す関数 kmrj を定義する
def kmrj(strA, strB):
   # strA と strB が同一である場合、いずれの決まり字も 'strA(重複)' と記述する
   if strA == strB:
       return ((strA + '(重複)'), (strB + '(重複)'))    
   # strB が strA で始まる場合、strA の決まり字は 'strA」' と記述する
   # strB の決まり字は、strB を (strA の文字数 + 1) 文字目までスライスした部分になる
   ### (strA の文字数 + 1) 文字目のインデックス値が (strA の文字数) であることに注意
   # 例:'py' と 'python' を比べるとき、決まり字はそれぞれ 'py」', 'pyt' とする
   elif strB.startswith(strA):
       return ((strA + '」'), strB[:len(strA)])
   # strA が strB で始まる場合、同様の処理を行う
   # 今回のメインプログラムではあり得ない状況だが、一般的な文字列 2 つを比較して決まり字を判定するためには必要な処理
   elif strA.startswith(strB):
       return (strA[:len(strB)], (strB + '」'))
   # それ以外の場合  
   else:
       # 同じ文字数だけスライスする
       # スライスする文字数を増やしていき、初めて異なったところが決まり字
       for i in range(len(strA)):
           if strA[:i+1] != strB[:i+1]:
               return (strA[:i+1], strB[:i+1])

# 辞書のリストを、特定のキーに対応する値の決まり字を加え直した状態で返す関数 kmrj_main を定義する
def kmrj_main(l_of_dics):
   # 辞書のリスト l_of_dics を 特定のキーでソートする (キーの名前も後で使うので k として保存しておく)
   l_of_dics, k = sorting_dic(l_of_dics)
   # 結果を保存しておく空リスト kmrjs を作る
   kmrjs = []
   # 要素が1つだけの場合、1字決まり
   if len(l_of_dics) == 1:
       kmrjs.append(l_of_dics[k][0])
   # 要素が2つの場合、kmrj 関数の戻り値がそのまま結果になる (タプル型をリスト型に変える必要あり)
   elif len(l_of_dics) == 2:
       kmrjs = list(kmrj(l_of_dics[0][k], l_of_dics[1][k]))
   # 要素が3つ以上の場合
   else:
       for i in range(len(l_of_dics)):
           # 最初は l_of_dics[0][k] と l_of_dics[1][k] で比べるだけで決まり字が決まる
           if i == 0:
               kmrjs.append(kmrj(l_of_dics[i][k], l_of_dics[i+1][k])[0])
           # 最初と最後以外は、2 回 kmrj 関数を使って長い方を決まり字とする
           # 前の要素との間で kmrj 関数を使用し、後ろの要素との間でも kmrj 関数を使用する
           elif 1 <= i <= len(l_of_dics) - 2:
               # kmrj 関数を 2 回使用し、2 つの決まり字候補を導く
               kmrj_f = kmrj(l_of_dics[i-1][k], l_of_dics[i][k])[1]
               kmrj_l = kmrj(l_of_dics[i][k], l_of_dics[i+1][k])[0]
               # kmrj_f が l で始まる or 一致する (= f の長さが l 以上である) 場合、決まり字は f
               if kmrj_f.startswith(kmrj_l):
                   kmrjs.append(kmrj_f)
               # kmrj_l の方が f より長い場合、決まり字は l
               else:
                   kmrjs.append(kmrj_l)
           # 最後は l_of_dics[i-1][k] と l_of_dics[i][k] で比べるだけで決まり字が決まる
           else:
               kmrjs.append(kmrj(l_of_dics[i-1][k], l_of_dics[i][k])[1])
   # 完成した決まり字のリスト kmrjs を、ソートした辞書のリスト l_of_dics に登録する
   l_of_dics = list_into_dic(l_of_dics, kmrjs)
   return l_of_dics

# ファイルダイアログで TSV ファイルを 辞書のリスト として読み込む関数
# キーは 1 行目に書かれているとする
def openfile():
   # まず、変換した辞書たちを保存するためのリストを作っておく
   filedata = []
   root = tk.Tk()
   root.withdraw()
   filename = tkinter.filedialog.askopenfilename()
   root.destroy()
   if filename:
       try:
           with open(filename, 'r') as f:
               # TSV ファイルとして 1 行ずつ読み込む
               rows = list(csv.reader(f, delimiter = '\t'))
               # 1 行だけしかデータがない TSV ファイルは受け付けない
               if len(rows) <= 1:
                   print('2 行以上の TSV ファイルを読み込んでください')
               else:
                   # 1 行目をキーとして扱う
                   keys = rows[0]
                   # 2 行目以降を値として各行を辞書に変換する
                   for row in rows[1:]:
                       # 空の辞書を作り、これに各値をキーとともに追加していく
                       dic_row = {}
                       for i, key in keys:
                           # リスト row の i 番目の値が、キー key (= keys[i]) に対応する値
                           dic_row[key] = row[i]
                       # 行 row を変換した辞書 dic_row を 辞書のリスト filedata に保存する
                       filedata.append(dic_row)
                   # 辞書のリストを返す
                   return filedata
       # ファイルが開けなかったときの例外処理
       except OSError:
           print('ファイルを開くことができません')
   # ファイルが指定されていないときの処理
   else:
       print('ファイルが指定されていません')
       exit()

# ファイルダイアログで 辞書のリスト を TSV ファイルとして書き出す関数
def saveas(l_of_dics):
   root = tk.Tk()
   root.withdraw()
   filename = tkinter.filedialog.asksaveasfilename()
   root.destroy()
   if filename:
       try:
           with open(filename, 'w') as f:
               # キーのリストを取得する
               keys = list(l_of_dics[0].keys())
               # 1 行目にはキーを書き出すので、初めに keys[0] を書き出す
               f.write(str(keys[0]))
               # キーが複数ある場合
               if len(keys) >= 2:
                   # [tab] keys[1] [tab] keys[3] ... [tab] keys[n]
                   for key in keys[1:]:
                       f.write('\t' + key)
               f.write('\n')
               # 2 行目以降はキーに対応した値を書いていく
               ### 先ほど使用した l_of_dics[0] も辞書で、書き出すべき内容なので忘れない
               for dic in l_of_dics:
                   # 初めに keys[0] に対する値を書き出す
                   f.write(str(dic[keys[0]]))
                   # キーが複数ある場合
                   if len(keys) >= 2:
                       # タブ区切りで 2 個目以降の値を書き出していく
                       for key in keys[1:]:
                           f.write('\t' + str(dic[key]))
                   f.write('\n')
       # ファイルが開けなかったときの例外処理
       except OSError:
           print('ファイルを開くことができません')
   # ファイルが指定されていないときの処理
   else:
       print('ファイルが指定されていません')
       exit()


# メインプログラム                            
if __name__ == '__main__':
   l_of_dics = openfile()
   l_of_dics = id_maker(l_of_dics)
   # 読みにしたがって決まり字を判定したい場合、読みを振る
   while True:
       Q = input('読みに沿って決まり字を判定する場合は[y]を,漢字を残して判定する場合は[n]を入力してください:')
       if Q == 'y' or Q == 'n':
           break
       else:
           print('半角英字の[y]か[n]を入力してください')
   if Q == 'y':
       l_of_dics = rubi(l_of_dics)
   l_of_dics = kmrj_main(l_of_dics)
   saveas(l_of_dics)

参考記事

・文字列にもfor文が適用できることを確認



この記事が気に入ったらサポートをしてみませんか?