4
2

More than 3 years have passed since last update.

【 Word, Excel, PowerPoint, PDFファイル選択可能 】日本語テキストから、SVO格を指定した検索単語を含む「係受け元単語 -> 係受け先単語」を全件出力するGUIツール

Last updated at Posted at 2020-12-05

作ったもの

前回の記事で作成したコードを機能拡張して、テキストファイル(拡張子.txt)以外に、Word, Excel, PowerPoint, PDFファイルを、ファイル選択ダイアログから選択できるようにしました。


( 挙動確認 )

Wordファイル読込み

tkinterword.gif

今回は、インターネットから取得した政府(財務省)のWordファイルを使ってみます。

スクリーンショット 2020-12-05 12.25.02.png

Terminal
% python3 file_content_checker_ner_svo.py

( 上記のファイル「honbun.docs」を選択 )
スクリーンショット 2020-12-05 12.27.02.png

( 「組織名」のラジオボタンをクリック )
honbun.txtに含まれる「組織名」の単語が全件出力される。
スクリーンショット 2020-12-05 12.27.22.png

( 2番目に出力された単語「財務省」を選択し、「主語」を選択 )

「財務省」という文字列を文字列に含む単語が、「主語」の役割で登場する文中箇所から、「係り受け元単語 -> 係り受け先単語」のペアを全件出力する。

スクリーンショット 2020-12-05 12.32.24.png

( 3番目に出力された単語「総務省」を選択し、「主語」を選択 )

「総務省」という文字列を文字列に含む単語が、「目的語」の役割で登場する文中箇所から、「係り受け元単語 -> 係り受け先単語」のペアを全件出力する。

スクリーンショット 2020-12-05 12.34.08.png

PowerPointファイル読込み

google検索文字列
filetype:pptx inurl:go.jp  "宇宙開発"

スクリーンショット 2020-12-05 12.36.23.png

( ダウンロードさせて頂いたファイルの中身 )

スクリーンショット 2020-12-05 12.38.26.png

( ファイルを選択 )
スクリーンショット 2020-12-05 12.39.15.png

スクリーンショット 2020-12-05 12.39.22.png

( 「人名」を選択後、「主語」を選択 )

スクリーンショット 2020-12-05 12.41.24.png

( 「組織名」を選択 )

スクリーンショット 2020-12-05 12.42.31.png

( 「日付」を選択 )

スクリーンショット 2020-12-05 12.45.29.png

PDFファイル読込み

google検索文字列
filetype:pdf inurl:go.jp "新型コロナ" "発生源"

スクリーンショット 2020-12-05 16.11.24.png

( ダウンロードさせて頂いたファイルの中身 )

スクリーンショット 2020-12-05 16.12.00.png

( ファイルを選択 )
スクリーンショット 2020-12-05 16.13.55.png

スクリーンショット 2020-12-05 16.14.02.png

( 「人名」のラジオボタンをクリックした結果 )
スクリーンショット 2020-12-05 16.14.25.png

( 「地名」のラジオボタンをクリックした結果 )
スクリーンショット 2020-12-05 16.15.16.png

( 「組織名」のラジオボタンをクリックした結果 )
スクリーンショット 2020-12-05 16.16.34.png

( 「日付」のラジオボタンをクリックした結果 )
スクリーンショット 2020-12-05 16.17.28.png


実装コード

file_content_checker_ner_svo.py
# Tkinterのライブラリを取り込む
import tkinter, tkinter.filedialog, tkinter.messagebox, spacy, collections, CaboCha, os, os.path,docx, openpyxl, pptx
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LAParams, LTContainer, LTTextBox
from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager
from pdfminer.pdfpage import PDFPage
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

# グローバル変数の宣言
filepath = ""
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 get_text_matching_file_names(ext_pattern_list, search_directory_path) -> list:
    file_names_list = os.listdir(search_directory_path)
    # 上記のいずれかの拡張子を持つファイルのファイル名を格納する配列を用意
    available_filename_list = []
    # 上記のいずれかの拡張子を持つファイルのファイル名を配列に格納する
    for file_name in file_names_list:
        ext_check_result = []
        for ext in ext_pattern_list:
            boolean = ext in file_name
            ext_check_result.append(boolean)
        if True in ext_check_result:
            available_filename_list.append(file_name)
    return  available_filename_list

####PDFファイル(拡張子:.pdf)からテキスト抽出するメソッド
def get_text_list_recursively(layout) -> list:
    if isinstance(layout, LTTextBox):
        return [layout.get_text()]
    if isinstance(layout, LTContainer):
        text_list = []
        for child in layout:
            text_list.extend(get_text_list_recursively(child))
            return text_list

def get_text_from_pdf(filepath: str) -> str:
    laparams = LAParams(detect_vertical=True)
    resource_manager = PDFResourceManager()
    device = PDFPageAggregator(resource_manager, laparams=laparams)
    interpreter = PDFPageInterpreter(resource_manager, device)
    results = []
    with open(filepath, "rb") as file:
        for page in PDFPage.get_pages(file):
            interpreter.process_page(page)
            layout = device.get_result()
            text = str(get_text_list_recursively(layout))
            text = text.replace("\\n", "").replace("\n", "").replace(" ", "").replace(" ", "")
            results.append(text)

    output = "".join(results)
    return output


####PowerPointファイル(拡張子:.pptx)からテキスト抽出するメソッド
def get_text_from_powerpoint(filepath: str) -> str:
    prs = pptx.Presentation(filepath)
    output = ""
    for i, sld in enumerate(prs.slides, start=1):
        for shp in sld.shapes:
            if shp.has_text_frame:
                text = shp.text
                output += text.replace("\n", "").replace("\n", "").replace(" ", "").replace(" ", "")

    return output


####Wordファイル(拡張子:.pptx)からテキスト抽出するメソッド
def get_text_from_word(filepath: str) -> str:
    document = docx.Document(filepath)
    text_list = list(map(lambda par: par.text, document.paragraphs))
    text = "".join(text_list)
    output = text.replace(" ", "").replace(" ", "")
    return output

####Excelファイル(拡張子:.xlsx)からテキスト抽出するメソッド
def get_text_from_excel(filepath: str) -> str:
    output_text = ""
    book = openpyxl.load_workbook(filepath)
    sheet_list = book.sheetnames
    for sheet_name in sheet_list:
        sheet = book[sheet_name]
        for cells in tuple(sheet.rows):
            for cell in cells:
                data = cell.value
                if data is None:
                    continue
                else:
                    output_text += str(data).replace(" ", "").replace(" ", "")
    return output_text

def get_text_from_textfile(filepath: str) -> str:
    output_text = ""
    f = open(filepath, 'r', encoding='UTF-8')
    output_text = f.read()
    f.close()

    return output_text
####Key: ファイルの拡張子に応じて、Value: 適切なメソッドを選択する際に用いる辞書
func_dict = {
    '.pdf' : get_text_from_pdf,
    '.docx'  : get_text_from_word,
    '.xlsx'  : get_text_from_excel,
    '.pptx' : get_text_from_powerpoint,
    '.txt'  : get_text_from_textfile
         }

####mainメソッド
def get_text_from_file(file_path):
    # パスからファイルの「ファイル名.拡張子」を取得
    # https://www.gesource.jp/programming/python/code/0010.html
    root, ext = os.path.splitext(file_path)
    available_ext_list = list(func_dict.keys())
    # ファイルの拡張子に応じた適切なテキストデータ抽出メソッドを実行
    for ok_ext in available_ext_list:
        if ext == ok_ext:
            text = func_dict[ext](file_path)

    if not(ext in available_ext_list):
        text = ""

    return text

# ファイルの参照処理
def click_refer_button():
    # https://water2litter.net/rum/post/python_tkinter_filedialog_open/
    #fTyp = [('', '*.pdf'), ('', '*.pptx') ]
    fTyp = [("", "*.txt"), ("", "*.pdf"), ("", "*.docx"), ("", "*.xlsx"), ("", "*.pptx")]
    # os.path.dirname(__file__) は、pyスクリプトファイルを実行中のディレクトリのパス
    iDir = os.path.abspath(os.path.dirname(__file__))
    filepath_ = filedialog.askopenfilename(filetypes = fTyp, initialdir = iDir)
    file_path.set(filepath_)
    global filepath
    filepath = 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()
    # filepathは絶対パスなので、"os.path.relpath(filepath, './')"で、カレントディレクトリから見た相対パスに変える
    #rel_path = os.path.relpath(filepath, './')
    #user_input_text = get_file_content_text(rel_path)
    root, ext = os.path.splitext(file_path)
    # https://note.nkmk.me/python-os-basename-dirname-split-splitext/
    base_dir_pair = os.path.split(filepath)
    user_input_text = get_text_from_file(file_path)
    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型のテキストデータに複数の文が含まれる場合を、「。」の出現回数で判定して検出。
# 複数の文を、1つの文を要素に持つlistに格納する。その後、リスト内包表記のなかで、文を一つずつ、一つの文を受け取るget_svo_info_に渡す。
# 受け取ったstr型のテキストデータに、1つの文しか含まれない場合は、上記の処理を行わない。
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"), ("", "*.pdf"), ("", "*.docx"), ("", "*.xlsx"), ("", "*.pptx")]
    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()

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2