【改訂版】日本語テキストから、SVO格を指定した検索単語を含む「係受け元単語 -> 係受け先単語」を全件出力するGUIツール

__[前回作成したGUIツール](https://qiita.com/Afo_guard_enthusiast/items/27d8801494e91c7f0c45)__の画面を見やすく変えました。

・ [前回の記事](https://qiita.com/Afo_guard_enthusiast/items/27d8801494e91c7f0c45)


##挙動確認

```bash:Terminal
% python3 tkinter_ner_svo_list_filter_by_target_word_and_case_file_dialog2.py
```

__1. テキストファイルを選択__

![スクリーンショット 2020-12-04 23.39.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/55c8bfdd-4a11-3542-4c0d-ace51de9e02e.png)

__2. テキストファイルを選択__
![スクリーンショット 2020-12-04 23.39.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/9bc37ce9-6fc3-dea7-277d-5a95fcce305b.png)

__3. 選択可能な固有表現ラベルの中から、「地名」のラジオボタンを選択__
__（ テキストファイル内に登場するすべての「地名」単語が、出現回数の多い順に表示される ）__
![スクリーンショット 2020-12-04 23.39.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/6495a725-05c7-f582-ca52-db4c007c4a2e.png)

__4. 出力された「地名」単語の中から、関心を寄せる単語の番号を入力__
__5. 「主語」を選択。選択した「地名」単語の文字列を含む単語が、「主語」の役割で登場する文脈箇所から、「係り受け元単語 -> 係り受け先単語」のペアを、全件出力する__
![スクリーンショット 2020-12-04 23.39.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/4944ca32-fe8c-9c44-b509-cf480a313a35.png)

__（ 他の「地名」も見てみる。４番目の地名「日本」を選択。さらに、「主語」を選択。）__
__（ 単語「日本」を含む文字列が、「主語」の役割で登場する文脈箇所から、「係り受け元単語 -> 係り受け先単語」のペアを全件出力する ）__
![スクリーンショット 2020-12-04 23.40.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/f4f4f2bb-6c38-8a47-49d4-953c4613614b.png)

__（ 単語「中国」を文字列に含む単語が、「主語」の役割で登場する文脈箇所からは、「係り受け元単語 -> 係り受け先単語」は、1件も見つからなかった。 ）__
![スクリーンショット 2020-12-04 23.40.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/17c4120d-8433-bbc6-ca5b-92514b590ee1.png)

__選択可能な固有表現ラベルの中から、「組織名」のラジオボタンを選択__
![スクリーンショット 2020-12-04 23.40.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/82698d2a-427b-cf80-a1fc-afe1d13d27d2.png)

__表示された１つ目の「組織名」（「ロシア軍」）という文字列を主語に持つ文脈から、「係り受け元単語 -> 係り受け先単語」のペアを全件、出力する。__

![スクリーンショット 2020-12-04 23.40.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/6339bef1-7b55-55bd-6eaf-2588daddb71f.png)



##改定後の実装コード

```Python:tkinter_ner_svo_list_filter_by_target_word_and_case_file_dialog2.py
# Tkinterのライブラリを取り込む
import tkinter, spacy, collections, CaboCha, os
import tkinter.filedialog, tkinter.messagebox
from typing import List, Dict, Tuple, TypeVar
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from spacy.matcher import Matcher

# グローバル変数の宣言
exracted_entity_word_list = ""
user_input_text = ""
named_entity_label = ""
T = TypeVar('T', str, None)
Vector = List[T]

# ターゲット単語の出現位置の格（主語（主格）、述語、目的語（目的格）
case_num_dict = {1 : "主語", 2 : "述語", 3: "目的語"}

# ファイルの参照処理
def click_refer_button():
    fTyp = [("","*")]
    iDir = os.path.abspath(os.path.dirname(__file__))
    filepath = filedialog.askopenfilename(filetypes = fTyp, initialdir = iDir)
    file_path.set(filepath)
    
# 固有表現抽出処理
def extract_words_by_entity_label(text, named_entity_label):
    nlp = spacy.load('ja_ginza')
    text = text.replace("\n", "")
    doc = nlp(text)
    words_list = [ent.text for ent in doc.ents if ent.label_ == named_entity_label]
    return words_list
    
# 出力処理
def click_export_button():
    # 選択された固有表現の種別名を取得
    named_entity_label = flg.get()
    
    global user_input_text
    f = open(file_path, encoding="utf-8")
    user_input_text = f.read()
    label_word_list = extract_words_by_entity_label(user_input_text, named_entity_label)
    # 指定された固有表現に該当する単語を取得した結果（単語リスト）を、{単語文字列 ： 出現回数}の辞書に変換する
    count_per_word = collections.Counter(label_word_list)
    # 出現回数の多い順番に並べる
    freq_per_word_dict = dict(count_per_word.most_common())
    #output_list =  ["{k}  : {v}".format(k=key, v=value) for (key, value) in freq_per_word_dict.items()]
    # 単語数を取得する
    unique_word_num = len(freq_per_word_dict)
    if unique_word_num > 0:
        message = "{num}件の{label}が見つかりました。\n\n出現回数順に並べた結果は以下です。\n\n".format(num=unique_word_num, label=named_entity_label)
        word_list = list(freq_per_word_dict.keys())
        word_freq_list = list(freq_per_word_dict.values())
        for i in range(unique_word_num):
            tmp = "{rank}番目の単語 : {word}\n出現回数 : {count}\n\n===================\n".format(rank=i+1, word=word_list[i], count=word_freq_list[i])
            message += tmp
    else:
        message = "{num}件の{label}が見つかりました。\n\n".format(num=unique_word_num, label=named_entity_label)
        
    textBox.insert(END, message)
    global exracted_entity_word_list
    exracted_entity_word_list = word_list
    return exracted_entity_word_list

def get_svo_info_(sentence:str, target_word:str, case_num_of_target_word:int) -> Tuple[Dict, List]:
    c = CaboCha.Parser()
    tree = c.parse(sentence)
    size = tree.size()
    myid = 0
    ku_list = []
    ku = ''
    ku_id = 0
    ku_link = 0
    kakari_joshi = 0
    kaku_joshi = 0

    for i in range(0, size):
        token = tree.token(i)
        if token.chunk:
            if (ku!=''):
                ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  #前 の句をリストに追加

            kakari_joshi = 0
            kaku_joshi = 0
            ku = token.normalized_surface
            ku_id = myid
            ku_link = token.chunk.link
            myid=myid+1
        else:
            ku = ku + token.normalized_surface

        m = (token.feature).split(',')
        if (m[1] == u'係助詞'):
            kakari_joshi = 1
        if (m[1] == u'格助詞'):
            kaku_joshi = 1

    ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  # 最後にも前の句をリストに追加
    for k in ku_list:
        if (k[2]==-1):  # link==-1?      # 述語である
            jutsugo_id = ku_id  # この時のidを覚えておく
    #述語句
    predicate_word = [k[0] for k in ku_list if (k[1]==jutsugo_id)]
    #for k in ku_list:
    #   if (k[1]==jutsugo_id):  # jutsugo_idと同じidを持つ句を探す
    #       print(k[1], k[0], k[2], k[3], k[4])
    #述語句に係る句
    # jutsugo_idと同じidをリンク先に持つ句を探す
    word_to_predicate_list = [k[0] for k in ku_list if k[2]==jutsugo_id]
    #　述語句に係る句 -> 述語句
    svo_arrow_text_list = [str(word_to_predicate) + "->" + str(predicate_word[0]) for word_to_predicate in word_to_predicate_list]
    #print(svo_arrow_text_list)

    desired_svo_arrow_text = [arrow_pair_str for arrow_pair_str in svo_arrow_text_list if target_word in arrow_pair_str]

    svo_dict = {}
    for num, k in enumerate(ku_list):
        if (k[2]==jutsugo_id):  # jutsugo_idと同じidをリンク先に持つ句を探す
            if (k[3] == 1):
                subject_word = k[0]
                if target_word in subject_word:
                    svo_dict["主語"] = subject_word
                    #print(subject_word)
            if (k[4] == 1):
                object_word = k[0]
                if target_word in object_word:
                    svo_dict["目的語"] = object_word
                    #print(object_word)
        if (k[1] == jutsugo_id):
                predicate_word = k[0]
                if target_word in predicate_word:
                    svo_dict["述語"] = predicate_word
                #print(predicate_word)

    case_type_of_search_word = case_num_dict[case_num_of_target_word]
    tmp_list_of_dict_list = [(svo_dict, desired_svo_arrow_text)]
    output_list_of_dict_list = [(dict_obj, list_obj) for (dict_obj, list_obj) in tmp_list_of_dict_list if list(dict_obj.keys())==[case_type_of_search_word]]
    # 空の要素を外す
    output = [elem for elem in output_list_of_dict_list if len(elem)>0]
    return output


# 受け取ったstr型のテキストデータに複数の文が含まれる場合を、「。」の出現回数で判定して検出。
# 複数の文を、１つの文を要素に持つlistに格納する。その後、リスト内包表記のなかで、文を一つずつ、一つの文を受け取るget_svo_info_に渡す。
# 受け取ったstr型のテキストデータに、１つの文しか含まれない場合は、上記の処理を行わない。
def get_svo_info(text:str, target_word:str, case_num_of_target_word:int) -> List[Tuple[Dict, List]]:
    sentence_num = text.count("。")
    if sentence_num > 1:
        sentence_list = text.split("。")
        sentence_list = [sentence for sentence in sentence_list if not(sentence == "")]
        output_list = result_list = [get_svo_info_(sentence, target_word, case_num_of_target_word) for sentence in sentence_list]
    else:
        output = get_svo_info_(text, target_word, case_num_of_target_word)
        output_list = [output]

    return output_list


def click_export_button2():
    # 選択された格位置の種別番号を取得
    case_label_int = flg2.get()
    case_name = case_num_dict[case_label_int]
    # 入力された単語を取得
    order_num = int(subject_num.get())-1 #ユーザが1を入力したとき、配列の0番地を指定する。
    target_word = exracted_entity_word_list[order_num]
    # 入力単語が、選択された格位置で出現する文脈箇所における「係受け単語関係」を抽出
    output_list = get_svo_info(user_input_text, target_word, case_label_int)
    # 空の要素を配列からとる
    tmp_list = [elem for elem in output_list if any(elem)]
    tmp = tmp_list
    length = len(tmp)
    output_message = ""
    for i, elm in enumerate(tmp):
        if elm[0] is None:
            output_message = "\n該当するものは見当たりませんでした。\n"
        else:
            output_message += "\n\n【 以下の" + case_name + "が見つかりました。 】\n\n" + str(elm[0][0]) + "\n\n【 以下の係り受け関係の単語ペアが見つかりました。 】\n\n" + str(elm[0][1]) + "\n\n"
            if length > i+1:
                output_message += "------------------------"
            
#    subject_string = exracted_entity_word_list[order_num]
#    output_list = get_subject_predicate_pair_list(user_input_text, subject_string)
    message = """
    
単語：「{search_word}」という文字列を{case}に含む「係り受け元単語 -> 係り受け先単語」のペアは、以下が見つかりました。
    
=========================================================================
    
{result}

=========================================================================

以上です。
    
""".format(search_word=target_word, case=case_name, result=output_message)
    textBox.insert(END, message)
    
if __name__ == '__main__':
    # ウィンドウを作成
    root = tkinter.Tk()
    root.title("文書内容_早見チェッカー") # アプリの名前
    root.geometry("730x800") # アプリの画面サイズ

    # ファイル選択ウインドウを作成
    # root.withdraw()
    fTyp = [("", "*.txt")]
    iDir = os.path.abspath(os.path.dirname(__file__))
    tkinter.messagebox.showinfo('ファイル選択ダイアログ','処理ファイルを選択してください！')
    file_path = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir)
    # 処理ファイル名の出力
    tkinter.messagebox.showinfo('以下のファイルを選択しました。',file_path)
    # Frame1の作成
    frame1 = ttk.Frame(root, padding=10)
    frame1.grid()
    
    # ラジオボタンの作成
    #共有変数
    flg= StringVar()
    
    #ラジオ1
    rb1 = ttk.Radiobutton(frame1, text='人名',value="PERSON", variable=flg)
    rb1.grid(row=2,column=0)
 
    #ラジオ2
    rb2 = ttk.Radiobutton(frame1, text='地名',value="LOC", variable=flg)
    rb2.grid(row=2,column=1)
    
    #ラジオ3
    rb3 = ttk.Radiobutton(frame1, text='組織名', value="ORG", variable=flg)
    rb3.grid(row=2,column=2)
    
    #ラジオ4
    rb4 = ttk.Radiobutton(frame1, text='日付',value="DATE", variable=flg)
    rb4.grid(row=3,column=0)
    
    #ラジオ5
    rb5 = ttk.Radiobutton(frame1, text='イベント名',value="EVENT", variable=flg)
    rb5.grid(row=3,column=1)
    
    #ラジオ6
    rb6 = ttk.Radiobutton(frame1, text='金額',value="MONEY", variable=flg)
    rb6.grid(row=3,column=2)
 
    # Frame2の作成
    frame2= ttk.Frame(root, padding=10)
    frame2.grid()
    
    # 固有表現単語を抽出した結果を表示させるボタンの作成
    export_button = ttk.Button(frame2, text='ファイルから指定した種類の単語を洗い出す', command=click_export_button, width=70)
    export_button.grid(row=0, column=0)
    
    # 「」ラベルの作成
    t = StringVar()
    t.set('出力された「単語」の中から、注目する単語の番号を入力してください。：')
    label1 = ttk.Label(frame2, textvariable=t)
    label1.grid(row=2, column=0)
    #テキストボックス2（「主語述語ペア」の「主語」入力欄）の作成
    subject_num = StringVar()
    subject_num_entry = ttk.Entry(frame2, textvariable=subject_num, width=50)
    subject_num_entry.grid(row=3, column=0)
    
    # Frame3の作成
    frame3 = ttk.Frame(root, padding=20)
    frame3.grid()
    
    # ターゲット単語がどの格で出現している文脈箇所の係り受け関係を抽出するのかを指定する。
    # 格を{1 : "主語", 2 : "述語", 3: "目的語"}でラジオボタンで選択可能にする。
    # 「」ラベルの作成
    u = StringVar()
    u.set('着目する「単語」が、どの格で登場する文中箇所を調べたいですか？')
    label2 = ttk.Label(frame3, textvariable=u)
    label2.grid(row=0, column=1)
    
    # Frame4の作成
    frame4 =ttk.Frame(root, padding=10)
    frame4.grid()
    
    #共有変数
    flg2= IntVar()
    #ラジオ1
    rb_a = ttk.Radiobutton(frame4, text='主語',value=1, variable=flg2)
    rb_a.grid(row=3, column=1)
 
    #ラジオ2
    rb_b = ttk.Radiobutton(frame4, text='述語',value=2, variable=flg2)
    rb_b.grid(row=3, column=5)
    
    #ラジオ3
    rb_c = ttk.Radiobutton(frame4, text='目的語', value=3, variable=flg2)
    rb_c.grid(row=3, column=10)
    
    # Frame5の作成
    frame5 =ttk.Frame(root, padding=10)
    frame5.grid()
    
    # 「係り受け関係にある単語ペア」を抽出した結果を表示させるボタンの作成
    export_button2 = ttk.Button(frame5,
    text='指定単語が指定した格で登場するぬ文脈から、「係り受け元単語 -> 係り受け先単語」のペアを抜き出して表示する', command=click_export_button2, width=80)
    export_button2.grid(row=1, column=0)
    
    # テキスト出力ボックスの作成
    textboxname = StringVar()
    textboxname.set('')
    label3 = ttk.Label(frame2, textvariable=textboxname)
    label3.grid(row=1, column=0)
    textBox = Text(frame2, width=100, height=35)
    textBox.grid(row=4, column=0)
    
    file_selected_message = """以下のファイルを選択しました。\n{filename}\n\n""".format(filename=file_path)
    textBox.insert(END, file_selected_message)
    # ウィンドウを動かす
    root.mainloop()

```