単語抽出ツール(5):単語抽出ツールソースコード説明(2)

前の記事に続いて、Pythonで実装された単語抽出ツールのソースコードについて説明します。

前の記事で続く内容だ。

単語抽出ツール(4):単語抽出ツールソースコード説明(1)

4. 単語抽出ツールのソースコード

4.3。 get_file_text関数

def get_file_text(file_name) -> DataFrame:
    """
    MS Word, PowerPoint, Text, DB Comment(Excel) file에서 text를 추출하는 함수
    :param file_name: 파일명
    :return: file에서 추출한 text(DataFrame type)
    """
    df_text = DataFrame()
    if file_name.endswith(('.doc', '.docx')):
        df_text = get_doc_text(file_name)
    elif file_name.endswith(('.ppt', '.pptx')):
        df_text = get_ppt_text(file_name)
    elif file_name.endswith('.txt'):
        df_text = get_txt_text(file_name)
    elif file_name.endswith(('.xls', '.xlsx', '.xlsb')):
        df_text = get_db_comment_text(file_name)
    return df_text
  • 行357〜365:ファイル拡張子に従って適切な関数を実行し、その結果をdf_textに入れて返します。

ファイル拡張子に従って実行する各関数の説明は次のとおりです。

4.3.1。 get_doc_text関数

def get_doc_text(file_name) -> DataFrame:
    """
    doc 파일에서 text를 추출하여 DataFrame type으로 return
    :param file_name: 입력 파일명 (str type)
    :return: 입력 파일에서 추출한 text
    """
    # :return: 입력 파일에서 추출한 text에 형태소 분석기로 명사 추출한 DataFrame
    start_time = time.time()
    print('\r\nget_doc_text: %s' % file_name)
    word_app = win32com.client.Dispatch("Word.Application")
    word_file = word_app.Documents.Open(file_name, True)
    # result = []
    df_text = pd.DataFrame()
    page = 0
    for paragraph in word_file.Paragraphs:
        text = paragraph.Range.Text
        page = paragraph.Range.Information(3)  # 3: wdActiveEndPageNumber(Text의 페이지번호 확인)
        if text.strip() != '':
            sr_text = Series([file_name, 'doc', page, text, f'{file_name}:{page}:{text}'],
                             index=['FileName', 'FileType', 'Page', 'Text', 'Source'])
            df_text = df_text.append(sr_text, ignore_index=True)

    word_file.Close()
    print('text count: %s' % str(df_text.shape[0]))
    print('page count: %d' % page)
    end_time = time.time()
    # elapsed_time = end_time - start_time
    elapsed_time = str(datetime.timedelta(seconds=end_time - start_time))
    print('[pid:%d] get_doc_text elapsed time: %s' % (os.getpid(), elapsed_time))
    # return get_word_list(df_text)
    return df_text
  • 行193:win32comパッケージでMS Wordプログラムのインスタンスを作成します。 MS Wordが実行されていない場合は、このコードで実行されます。
  • 行194:上記で作成したMS Wordプログラムinstanceで.docまたは.docxファイルを開きます。
  • 行198:ファイルの内容の段落を巡回する。
  • 行199:段落の内容からテキストを抽出する。
  • 200行目:現在の段落のページ番号を抽出します。 Range.Information(3)では、「3」はwdActiveEndPageNumber定数値に対応します。詳しくは WdInformation enumeration(Word)| Microsoft Docsを参照してください。
  • 行202〜204:抽出したテキストをSeriesオブジェクトにし、df_text DataFrameの行に追加します。
  • 行214:ファイルから抽出したテキストを含むdf_textを返します。

4.3.2。 get_ppt_text関数

def get_ppt_text(file_name) -> DataFrame:
    """
    ppt 파일에서 text를 추출하여 DataFrame type으로 return
    :param file_name: 입력 파일명 (str type)
    :return: 입력 파일에서 추출한 text
    """
    # :return: 입력 파일에서 추출한 text에 형태소 분석기로 명사 추출한 DataFrame
    start_time = time.time()
    print('\r\nget_ppt_text: %s' % file_name)
    ppt_app = win32com.client.Dispatch('PowerPoint.Application')
    ppt_file = ppt_app.Presentations.Open(file_name, True)
    # result = []
    df_text = pd.DataFrame()
    page_count = 0
    for slide in ppt_file.Slides:
        slide_number = slide.SlideNumber
        page_count += 1
        for shape in slide.Shapes:
            shape_text = []
            text = ''
            if shape.HasTable:
                col_cnt = shape.Table.Columns.Count
                row_cnt = shape.Table.Rows.Count
                for row_idx in range(1, row_cnt + 1):
                    for col_idx in range(1, col_cnt + 1):
                        text = shape.Table.Cell(row_idx, col_idx).Shape.TextFrame.TextRange.Text
                        if text != '':
                            text = text.replace('\r', ' ')
                            shape_text.append(text)
            elif shape.HasTextFrame:
                for paragraph in shape.TextFrame.TextRange.Paragraphs():
                    text = paragraph.Text
                    if text != '':
                        shape_text.append(text)
            for text in shape_text:
                if text.strip() != '':
                    sr_text = Series([file_name, 'ppt', slide_number, text, f'{file_name}:{slide_number}:{text}'],
                                     index=['FileName', 'FileType', 'Page', 'Text', 'Source'])
                    df_text = df_text.append(sr_text, ignore_index=True)
    # print(result)
    ppt_file.Close()
    # print(df_result)
    print('text count: %s' % str(df_text.shape[0]))
    print('page count: %d' % page_count)
    # print(df_text.head(10))
    # print(df_result.Paragraph)
    # return df_result
    end_time = time.time()
    # elapsed_time = end_time - start_time
    elapsed_time = str(datetime.timedelta(seconds=end_time - start_time))
    print('[pid:%d] get_ppt_text elapsed time: %s' % (os.getpid(), elapsed_time))
    # return get_word_list(df_text)
    return df_text
  • 行138:win32comパッケージでMS Powerpointプログラムのインスタンスを作成します。 MS Powerpointが実行されていない場合は、このコードで実行されます。
  • 行139:上記で作成したMS Powerpointプログラムinstanceで.pptまたは.pptxファイルを開きます。
  • 行143:ファイルのスライドを巡回する。
  • 行146:各スライドのシェイプを巡回する。
  • 行149〜157:図形がテーブルの場合、テーブルの各セルからテキストを抽出します。
  • 行158〜162:図形がテーブルではなくテキストを持っている場合は、テキストを抽出します。
  • 行163〜167:抽出したテキストをSeriesオブジェクトにし、df_text DataFrameの行に追加します。
  • 行181:ファイルから抽出したテキストを含むdf_textを返します。

4.3.3。 get_txt_text関数

def get_txt_text(file_name) -> DataFrame:
    """
    txt 파일에서 text를 추출하여 DataFrame type으로 return
    :param file_name: 입력 파일명 (str type)
    :return: 입력 파일에서 추출한 text
    """
    # :return: 입력 파일에서 추출한 text에 형태소 분석기로 명사 추출한 DataFrame
    start_time = time.time()
    print('\r\nget_txt_text: ' + file_name)
    df_text = pd.DataFrame()
    line_number = 0
    with open(file_name, 'rt', encoding='UTF8') as file:
        for text in file:
            line_number += 1
            if text.strip() != '':
                sr_text = Series([file_name, 'txt', line_number, text, f'{file_name}:{line_number}:{text}'],
                                 index=['FileName', 'FileType', 'Page', 'Text', 'Source'])
                df_text = df_text.append(sr_text, ignore_index=True)
    print('text count: %d' % df_text.shape[0])
    print('line count: %d' % line_number)
    end_time = time.time()
    # elapsed_time = end_time - start_time
    elapsed_time = str(datetime.timedelta(seconds=end_time - start_time))
    print('[pid:%d] get_txt_text elapsed time: %s' % (os.getpid(), elapsed_time))
    # return get_word_list(df_text)
    return df_text
  • 行228:ファイルをUTF8エンコーディングの読み取り専用テキストモードで開きます。 (mode='rt')
  • 行229:ファイルの行を巡回する。
  • 行231〜234:行テキストをSeriesオブジェクトにし、df_text DataFrameの行に追加します。
  • 行242:ファイルから抽出したテキストを含むdf_textを返します。

4.3.4。 get_db_comment_text関数

def get_db_comment_text(file_name) -> DataFrame:
    """
    db_comment 파일에서 text를 추출하여 DataFrame type으로 return
    :param file_name:  입력 파일명 (str type)
    :return: 입력 파일에서 추출한 text
    """
    # :return: 입력 파일에서 추출한 text에 형태소 분석기로 명사 추출한 DataFrame
    start_time = time.time()
    print('\r\nget_db_comment_text: %s' % file_name)
    excel_app = win32com.client.Dispatch('Excel.Application')
    full_path_file_name = os.path.abspath(file_name)
    excel_file = excel_app.Workbooks.Open(full_path_file_name, True)

    # region Table comment
    table_comment_sheet = excel_file.Worksheets(1)
    last_row = table_comment_sheet.Range("A1").End(-4121).Row  # -4121: xlDown
    table_comment_range = 'A2:D%s' % (str(last_row))
    print('table_comment_range : %s (%d rows)' % (table_comment_range, last_row - 1))
    table_comments = table_comment_sheet.Range(table_comment_range).Value2
    df_table = pd.DataFrame(list(table_comments),
                            columns=['DB', 'Schema', 'Table', 'Text'])
    df_table['FileName'] = full_path_file_name
    df_table['FileType'] = 'table'
    df_table['Page'] = 0
    df_table = df_table[df_table.Text.notnull()]  # Text 값이 없는 행 제거
    df_table['Source'] = df_table['DB'] + '.' + df_table['Schema'] + '.' + df_table['Table'] \
                         + '(' + df_table['Text'].astype(str) + ')'
    # print(df_table)
    # endregion

    # region Column comment
    column_comment_sheet = excel_file.Worksheets(2)
    last_row = column_comment_sheet.Range("A1").End(-4121).Row  # -4121: xlDown
    column_comment_range = 'A2:E%s' % (str(last_row))
    print('column_comment_range : %s (%d rows)' % (column_comment_range, last_row - 1))
    column_comments = column_comment_sheet.Range(column_comment_range).Value2
    df_column = pd.DataFrame(list(column_comments),
                             columns=['DB', 'Schema', 'Table', 'Column', 'Text'])
    df_column['FileName'] = full_path_file_name
    df_column['FileType'] = 'column'
    df_column['Page'] = 0
    df_column = df_column[df_column.Text.notnull()]  # Text 값이 없는 행 제거
    df_column['Source'] = df_column['DB'] + '.' + df_column['Schema'] + '.' + df_column['Table'] \
                          + '.' + df_column['Column'] + '(' + df_column['Text'].astype(str) + ')'
    # print(df_column)
    # endregion

    excel_file.Close()
    df_text = df_column.append(df_table, ignore_index=True)
    # print(df_text)
    end_time = time.time()
    # elapsed_time = end_time - start_time
    elapsed_time = str(datetime.timedelta(seconds=end_time - start_time))
    print('[pid:%d] get_db_comment_text elapsed time: %s' % (os.getpid(), elapsed_time))
    print('text count: %s' % str(df_text.shape[0]))
    # return get_word_list(df_text)
    return df_text
  • 300行目:win32comパッケージでMS Excelプログラムのインスタンスを作成します。 MS Excelが実行されていない場合は、このコードで実行されます。
  • 行302:上記で作成したMS Excelプログラムinstanceで.xlsまたは.xlsxファイルを開きます。
  • 行305〜317:テーブルコメントが格納されているExcelファイルの最初のシートからTextを抽出します。
  • 行306:テーブルコメントシートの最後の行番号を取得します。 Range(「A1」)、End(-4211)。Rowでは、「-4211」は「xlDown」定数です。詳しくは XlDirection enumeration(Excel)| Microsoft Docs 文書を参照してください。
  • 行309:テーブルコメントシートの内容をtable_comments変数として読み込みます。この方法は、ループを使わずに Range の内容を一度に memory で読む方法です。 VBAコーディングパターン:Range Loop - 読み取り(Read) 内容を参照してください。この記事はExcel VBAとして説明されていますが、PythonでもOLE Automationを使用するとほぼ同じように適用できます。
  • 行310〜317:table_commentsをDataFrame df_tableに変換し、「FileName」、「FileType」、「Page」、「Source」の列のデータを追加します。
  • 行322〜334:列コメントが格納されているExcelファイルの2番目のシートからTextを抽出します。
  • 行323:列コメントシートの最後の行番号を取得します。行306と同じ方法を使用してください。
  • 行326:列コメントシートの内容をcolumn_comments変数として読みます。行309と同じ方法を使用してください。
  • 行327~334:column_commentsをDataFrame df_columnに変換し、「FileName」、「FileType」、「Page」、「Source」の列のデータを追加します。
  • 行339:df_columnとdf_tableを組み合わせてdf_textを作成します。
  • 行347:DBテーブル、列コメントが格納されているExcelファイルから抽出したテキストを含むdf_textを返します。

4.3.5。 get_hwp_text関数

def get_hwp_text(file_name) -> DataFrame:
    pass

現在実装されていません。今後必要に応じて実装予定だ。

4.3.6。 get_pdf_text関数

def get_pdf_text(file_name) -> DataFrame:
    pass

現在実装されていません。今後必要に応じて実装予定だ。

4.4。 get_word_list関数

単語抽出ツールで最も重要な関数です。

def get_word_list(df_text) -> DataFrame:
    """
    text 추출결과 DataFrame에서 명사를 추출하여 최종 output을 DataFrame type으로 return
    :param df_text: 파일에서 추출한 text(DataFrame type)
    :return: 명사, 복합어(1개 이상의 명사, 접두사+명사+접미사) 추출결과(Dataframe type)
    """
    start_time = time.time()
    df_result = DataFrame()

    tagger = Mecab()
    # tagger = Komoran()
    row_idx = 0
    for index, row in df_text.iterrows():
        row_idx += 1
        if row_idx % 100 == 0:  # 100건마다 현재 진행상태 출력
            print('[pid:%d] current: %d, total: %d, progress: %3.2f%%' %
                  (os.getpid(), row_idx, df_text.shape[0], round(row_idx / df_text.shape[0] * 100, 2)))
        file_name = row['FileName']
        file_type = row['FileType']
        page = row['Page']
        text = str(row['Text'])
        source = (row['Source'])
        is_db = True if row['FileType'] in ('table', 'column') else False
        is_db_table = True if row['FileType'] == 'table' else False
        is_db_column = True if row['FileType'] == 'column' else False
        if is_db:
            db = row['DB']
            schema = row['Schema']
            table = row['Table']
            if is_db_column:
                column = row['Column']

        if text is None or text.strip() == '':
            continue
        try:
            # nouns = mecab.nouns(text)
            # [O]ToDo: 연속된 체언접두사(XPN), 명사파생접미사(XSN) 까지 포함하여 추출
            # [O]ToDo: 명사(NNG, NNP)가 연속될 때 각각 명사와 연결된 복합명사 함께 추출
            text_pos = tagger.pos(text)
            words = [pos for pos, tag in text_pos if tag in ['NNG', 'NNP', 'SL']]  # NNG: 일반명사, NNP: 고유명사
            pos_list = [x for (x, y) in text_pos]
            tag_list = [y for (x, y) in text_pos]
            pos_str = '/'.join(pos_list) + '/'
            tag_str = '/'.join(tag_list) + '/'
            iterator = re.finditer('(NNP/|NNG/)+(XSN/)*|(XPN/)+(NNP/|NNG/)+(XSN/)*|(SL/)+', tag_str)
            for mo in iterator:
                x, y = mo.span()
                if x == 0:
                    start_idx = 0
                else:
                    start_idx = tag_str[:x].count('/')
                end_idx = tag_str[:y].count('/')
                sub_pos = ''
                # if end_idx - start_idx > 1 and not (start_idx == 0 and end_idx == len(tag_list)):
                if end_idx - start_idx > 1:
                    for i in range(start_idx, end_idx):
                        sub_pos += pos_list[i]
                    # print('%s[sub_pos]' % sub_pos)
                    words.append('%s[복합어]' % sub_pos)  # 추가 형태소 등록

            if len(words) >= 1:
                # print(nouns, text)
                for word in words:
                    # print(noun, '\t', text)
                    if not is_db:
                        # sr_text = Series([file_name, file_type, page, text, word],
                        #                  index=['FileName', 'FileType', 'Page', 'Text', 'Word'])
                        df_word = DataFrame(
                            {'FileName': [file_name], 'FileType': [file_type], 'Page': [page], 'Text': [text],
                             'Word': [word], 'Source': [source]})
                    elif is_db_table:
                        # sr_text = Series([file_name, file_type, page, text, word, db, schema, table],
                        #                  index=['FileName', 'FileType', 'Page', 'Text', 'Word', 'DB', 'Schema', 'Table'])
                        df_word = DataFrame(
                            {'FileName': [file_name], 'FileType': [file_type], 'Page': [page], 'Text': [text],
                             'Word': [word], 'DB': [db], 'Schema': [schema], 'Table': [table “” not found /]
, 'Source': [source]}) elif is_db_column: # sr_text = Series([file_name, file_type, page, text, word, db, schema, table, column], # index=['FileName', 'FileType', 'Page', 'Text', 'Word', 'DB', 'Schema', 'Table', 'Column']) df_word = DataFrame( {'FileName': [file_name], 'FileType': [file_type], 'Page': [page], 'Text': [text], 'Word': [word], 'DB': [db], 'Schema': [schema], 'Table': [table “” not found /]
, 'Column': [column], 'Source': [source]}) # df_result = df_result.append(sr_text, ignore_index=True) # Todo: append를 concat으로 바꾸기 df_result = pd.concat([df_result, df_word], ignore_index=True) except Exception as ex: print('[pid:%d] Exception has raised for text: %s' % (os.getpid(), text)) print(ex) print( '[pid:%d] input text count:%d, extracted word count: %d' % (os.getpid(), df_text.shape[0], df_result.shape[0])) end_time = time.time() # elapsed_time = end_time - start_time elapsed_time = str(datetime.timedelta(seconds=end_time - start_time)) print('[pid:%d] get_word_list finished. total: %d, elapsed time: %s' % (os.getpid(), df_text.shape[0], elapsed_time)) return df_result
  • 行35:自然言語形態素アナライザMecabオブジェクトを作成します。 Mecab以外のタグを使用するには、ここでパッケージ名を変更します。
  • 行38:DataFrame df_textの行を巡回する。
  • 行64:pos関数で形態素分析器の品詞タグ付けを実行します。品詞タギングに関する内容は別途整理する。
    • 品詞タグ付け関数posは、入力文字列を品詞単位に分解し、各単位がどの品詞であるかを表示した文字列を返します。
    • たとえば、textが「ユーザーは機能的要件と非機能的要件を定義します。」の場合、pos関数の実行結果は「[(「使用」、「NNG」)、 ')、('は'、'JX')、('機能'、'NNG')、('敵'、'XSN')、('要求'、'NNG')、('仕様'、'NNG ')、('と'、'JC')、('非'、'XPN')、('機能'、'NNG')、('敵'、'XSN')、('要求'、'NNG ')、('仕様'、'NNG')、('を'、'JKO')、('定義'、'NNG')、('する'、'XSV+EF')、('.', 'SF')]'だ。
    • 上記の例でタグ付けされた品詞のうち、「NNG」は一般名詞、「XSN」は名詞派生接尾辞、「JX」はアシスタント、「JC」は接続調査、「XPN」は体言接頭辞、「JKO」は目的格調査、 XSV+EF'は動詞派生接尾辞+終結語、'SF'はピリオド/疑問符/感嘆符を意味する。
  • 65行目:品詞タグ付けの結果から、標準単語候補として最も適した品詞である一般名詞(NNG)、固有名詞(NNP)、外国語(SL)を選抜する。外国語(SL)は、アルファベットからなる略語を標準単語候補に抽出するために指定した。
  • 行70:正規表現(regula expression)を利用して '(NNP/|NNG/)+(XSN/)*|(XPN/)+(NNP/|NNG/)+(XSN/)*|(SL/) + 'パターンを探します。
    • このパターンは次の3つのうちの1つを見つけます。
      • (NNP/|NNG/)+(XSN/)*:(固有名詞または一般名詞)1個以上+名詞派生サフィックス0個以上
      • (XPN/)+(NNP/|NNG/)+(XSN/)*: 体語接頭辞 1 個以上 + (固有名詞または一般名詞) 1 個以上 + 名詞派生サフィックス 0 個以上
      • (SL/)+: 外国語 1 つ以上
  • 行71〜84:上記の正規表現パターンで見つかった単語を連結し、suffix '[複合語]'を付けて抽出単語リストに追加する。後で標準語の事前精製作業をするときは、複合語で抽出された単語を識別するためにわざとsuffixを付けておく。
  • 86~110行目:抽出された単語をソースやファイル形式などの付加属性を加えてDataFrameに入れる。
  • 行122:抽出された単語のリストを含むdf_resultを返します。 

参考までに、他の品詞パターンをさらに抽出するには、65行と70行を修正すればよい。

4.5。 make_word_cloud関数

def make_word_cloud(df_group, now_dt, out_path):
    """
    명사의 빈도를 구한 DataFrame으로 word cloud 그리기
    :param df_group: 명사 빈도 DataFrame
    :param now_dt: 현재 날짜 시각
    :param out_path: 출력경로
    :return: None
    """
    start_time = time.time()
    print('\r\nstart make_word_cloud...')
    from wordcloud import WordCloud
    import matplotlib.pyplot as plt
    # malgun.ttf # NanumSquare.ttf # NanumSquareR.ttf NanumMyeongjo.ttf # NanumBarunpenR.ttf # NanumBarunGothic.ttf
    wc = WordCloud(font_path='.\\font\\NanumBarunGothic.ttf',
                   background_color='white',
                   max_words=500,
                   width=1800,
                   height=1000
                   )

    # print(df_group.head(10))
    words = df_group.to_dict()['Freq']
    # print(words)
    # words = df_group.T.to_dict('list')
    wc.generate_from_frequencies(words)
    wc.to_file('%s\\wordcloud_%s.png' % (out_path, now_dt))
    # plt.axis('off')
    end_time = time.time()
    # elapsed_time = end_time - start_time
    elapsed_time = str(datetime.timedelta(seconds=end_time - start_time))
    print('make_word_cloud elapsed time: %s' % elapsed_time)

この関数はWordCloudパッケージを使用します。

WordCloud 예시
WordCloudの例
  • 行258〜263:WordCloudオブジェクトを作成します。
    • fontフォルダの下にあるNanumBarunGothic.ttf(共有のゴシック)フォントファイルを使用しました。別のフォントに変更するには、フォントフォルダにフォントファイルをコピーしてファイル名を指定します。
    • background_color、max_words、width、heightは希望の値に変更して使用すればよい。
  • 行266:DataFrame df_groupでは、KeyはIndex(単語)、Valueは「Freq」(頻度)で構成される辞書単語を生成します。
  • 行269:頻度を含む単語からWordCloudイメージを作成します。
  • 行270:生成されたWordCloudイメージを保存します。

ここまでソースコードの説明は終わった。次に、ソースコードの追加説明と品詞タグ付けに関する内容を見てみましょう。


<< 関連記事のリスト >>

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ja日本語