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

###（ 後記 ）

画面の下部にある__ラベル「指定単語が指定した格で登場するぬ文脈から、」__の部分で、__「するぬ」は誤記__です。__正しくは、「する」__です。

___

##作ったもの

サンプルの和文テキストは、NHK NewsWebの国際面に、2020/12/1に掲載されていた__[記事](https://www3.nhk.or.jp/news/html/20201201/k10012741251000.html?utm_int=news_contents_news-main_001)__から拝借しました。

__0. Teminalで、Pythonスクリプトファイルを実行__

```bash:Terminal（Python3.9.0）
% python3 tkinter_ner_svo_list_filter_by_target_word_and_case_file_dialog2.py
```

__1. ファイル選択ダイアログから、解析したいテキストファイルを選択__

![スクリーンショット 2020-12-03 3.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/a8883321-1e3d-f1a3-c18e-b1ea693293a8.png)



![スクリーンショット 2020-12-03 3.04.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/a64c1d76-b892-6cf0-b5af-4d8f696c7209.png)


__2. 固有表現ラベルの選択肢（ラジオボタン）から、「地名」を選択__

__（ 対象ファイルに含まれる「地名」単語が、一括出力される。並び順は出現頻度の降順 ）__
![スクリーンショット 2020-12-03 2.28.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/ec00dae5-e84d-4641-a15c-2c5926d2479d.png)

__3.出力された「地名」単語のうち、関心を寄せた単語を、番号で指定__
（ 以下では、__「２」番__の単語である__「ロシア」__を選択 ）
__4. 選択した単語「ロシア」が、どの格で現れる文脈に関心を寄せるのかを、ラジオボタンで選択。__
（ 以下では、__「主語」__を選択 ）
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/ab31a2cf-bf5c-5896-1db4-9bfd1d0df2b4.png)

（ 以下では、__「述語」__を選択 ）
![スクリーンショット 2020-12-03 2.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/bce10d09-5003-fda5-cc35-28be9550fc61.png)

（ 以下では、__「目的語」__を選択 ）
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/e987b344-641a-7bb8-028b-0668f540e342.png)

__・ 出力された「地名」単語のうち、関心を寄せた単語を、番号で指定__
（ 以下では、__「４」番__の単語である__「日本」__を選択 ）

__・ 選択した単語「日本」が、どの格で現れる文脈に関心を寄せるのかを、ラジオボタンで選択。__
（ 以下では、__「主語」__を選択 ）
![スクリーンショット 2020-12-03 2.29.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/74a85058-d4d1-eb25-216e-58f0bfd351b1.png)

__・ 固有表現ラベルの選択肢（ラジオボタン）から、「組織名」を選択__
__・ 出力された「地名」単語のうち、関心を寄せた単語を、番号で指定__
（ 以下では、__「1」番__の単語である__「ロシア軍」__を選択 ）
__・ 選択した単語「ロシア軍」が、どの格で現れる文脈に関心を寄せるのかを、ラジオボタンで選択。__
（ 以下では、__「主語」__を選択 ）
![スクリーンショット 2020-12-03 2.31.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/862466/d709b280-2a40-841b-2c75-92995c5cc68a.png)


##実装コード

__（ 参考にさせて頂いたWebページ ）__

テキストデータから、SVO（SPO)のTriple set（[主語, 動詞（述語）, 目的語]）を返す処理を行う実装コードを探してみたところ、なかなか見当たりませんでした。方々を探してみたところ、見つけた以下のコードがうまく動きました。

・__[東邦大学 「2014-01-29 CaboChaによる係り受け解析の利用 ～ 文の主節の骨組を取り出す」](https://pepper.is.sci.toho-u.ac.jp/pepper/index.php?%A5%CE%A1%BC%A5%C8%2F%A5%C6%A5%AD%A5%B9%A5%C8%A5%DE%A5%A4%A5%CB%A5%F3%A5%B0%2FCaboCha%A4%C7%BC%E7%C0%E1%A4%CE%B9%FC%C1%C8%A4%F2%BC%E8%A4%EA%BD%D0%A4%B9)__

この記事では、東邦大学の研究室が公開中の上記のコードをもとに、上記の入出力関係を持つ関数（メソッド）に書き換えてみました。

```Python3: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_subject_predicate_pair_list(sentence: str, subject_word: str) -> Vector:
    T = TypeVar('T', str, None)
    c = CaboCha.Parser()
    tree =  c.parse(sentence)

    # 形態素を結合しつつ[{c:文節, to:係り先id}]の形に変換する
    chunks = []
    text = ""
    toChunkId = -1
    for i in range(0, tree.size()):
        token = tree.token(i)
        text = token.surface if token.chunk else (text + token.surface)
        toChunkId = token.chunk.link if token.chunk else toChunkId
        # 文末かchunk内の最後の要素のタイミングで出力
        if i == tree.size() - 1 or tree.token(i+1).chunk:
            chunks.append({'c': text, 'to': toChunkId})

    # ループの中で出力される「係り元→係り先」文字列をlistに格納
    pair_list = []
    #
    for chunk in chunks:
        if chunk['to'] >= 0:
            pair_list.append(chunk['c'] + "  --→  " + chunks[chunk['to']]['c'])
    
    output_list = [pair for pair in pair_list if subject_word in pair]
    return output_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()
    # 入力された単語を取得
    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)
    # 空の要素を配列からとる
    output_list = [elem for elem in output_list if any(elem)]
#    subject_string = exracted_entity_word_list[order_num]
#    output_list = get_subject_predicate_pair_list(user_input_text, subject_string)
    #result = "\n".join(output_list)
    message = """
    
単語：「{search_word}」を{case}に持つ「係り受け元単語 -> 係り受け先単語」のペアは、以下が見つかりました。
    
=========================================================================
    
{result}

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

以上です。
    
""".format(search_word=target_word, case=case_num_dict[case_label_int], result=output_list)
    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()

```