分词工具(五):分词工具源码说明(二)

接上一篇,我们来看一下用Python实现的分词工具的源码。

这是上一篇文章的延续。

分词工具(四):分词工具源码说明(一)

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 程序实例中打开 .doc 或 .docx 文件。
  • 第 198 行:遍历文件内容中的段落。
  • 第 199 行:从段落的内容中提取文本。
  • 第 200 行:提取当前段落的页码。在 Range.Information(3) 中,“3”对应于 wdActiveEndPageNumber 常量值。更多细节 WdInformation 枚举 (Word) |微软文档
  • 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 程序实例中打开 .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 编码的只读文本模式打开文件。 (模式='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 程序的实例中打开 .xls 或 .xlsx 文件。
  • 第 305~317 行:从存储表格注释的 Excel 文件的第一张表中提取文本。
  • 第306行:获取表格注释表的最后一行行号。在 Range(“A1”).End(-4211).Row 中,“-4211”是“xlDown”常量。更多细节 XlDirection 枚举 (Excel) |微软文档 查看文档
  • 第 309 行:将 table comments sheet 的内容读入 table_comments 变量中。该方法是不使用循环,一次性将范围内的内容读入内存的方法。 VBA 编码模式:范围循环读取 查看内容这篇文章是用Excel VBA解释的,但是如果你在Python中使用OLE Automation,几乎一样可以应用。
  • 第 310-317 行:将 table_comments 转换为 DataFrame df_table 并添加来自“FileName”、“FileType”、“Page”、“Source”列的数据。
  • 第 322~334 行:从存储列注释的 Excel 文件的第二张表中提取文本。
  • 第 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 将输入字符串分解为词性单元,并返回每个单元都被标记的字符串。
    • 比如文本是'Users define functional and non-functional requirements',那么pos函数的执行结果就是'[('use', 'NNG'), ('character', 'XSN'), (' is', 'JX'), ('function', 'NNG'), ('enemy', 'XSN'), ('request', 'NNG'), ('spec', 'NNG' '), ( 'and', 'JC'), ('b', 'XPN'), ('feature', 'NNG'), ('enemies', 'XSN'), ('request', 'NNG'), ( '东西', 'NNG'), ('to', 'JKO'), ('定义', 'NNG'), ('应该', 'XSV+EF'), ('.', 'SF') ]'。
    • 上例标注的词性中,'NNG'是普通名词,'XSN'是名词派生后缀,'JX'是助动词,'JC'是连接词,'XPN'是前缀,'JKO'是宾语助词,'XSV + EF'是动词派生后缀+词尾,'SF'表示句号/问号/感叹号。
  • 第65行:从词性标注结果中选择词性最合适的普通名词(NNG)、专有名词(NNP)和外来词(SL)作为标准候选词。外语(SL)被指定提取由字母组成的缩写词作为标准候选词。
  • 第 70 行:使用正则表达式,'(NNP/|NNG/)+(XSN/)*|(XPN/)+(NNP/|NNG/)+(XSN/)*|(SL/) +' 找到图案。
    • 此模式找到以下三种情况之一:
      • (NNP/|NNG/)+(XSN/)*:1个或多个(专有名词或普通名词)+ 0个或多个名词派生后缀
      • (XPN/)+(NNP/|NNG/)+(XSN/)*:1个或多个前缀+1个或多个(专有名词或普通名词)+0个或多个名词派生后缀
      • (SL/)+:至少一门外语
  • 第 71~84 行:将找到的单词与上述正则表达式模式连接起来,并通过附加后缀 '[compound word]' 将它们添加到提取的单词列表中。后来在做标准词典细化的时候,我们特意加了后缀来标识提取出来的复合词。
  • 第 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 예시
词云示例
  • 第 258-263 行:创建 WordCloud 对象。
    • 使用了字体文件夹下的NanumBarunGothic.ttf(NanumBarunGothic)字体文件。要更改为另一种字体,将字体文件复制到字体文件夹并指定文件名。
    • Background_color、max_words、width 和 height 可以更改为所需的值。
  • 第266行:在DataFrame df_group中,创建以Index(词)为Key,'Freq'(频率)为Value的字典词。
  • 第 269 行:根据具有频率的单词创建一个 WordCloud 图像。
  • 第 270 行:保存生成的 WordCloud 图片。

源码讲解到此结束。接下来我们就来看看源码的补充说明和词性标注。


<< 相关文章列表 >>

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CN简体中文