分词工具(五):分词工具源码说明(二)
接上一篇,我们来看一下用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 包。
- 第 258-263 行:创建 WordCloud 对象。
- 使用了字体文件夹下的NanumBarunGothic.ttf(NanumBarunGothic)字体文件。要更改为另一种字体,将字体文件复制到字体文件夹并指定文件名。
- Background_color、max_words、width 和 height 可以更改为所需的值。
- 第266行:在DataFrame df_group中,创建以Index(词)为Key,'Freq'(频率)为Value的字典词。
- 第 269 行:根据具有频率的单词创建一个 WordCloud 图像。
- 第 270 行:保存生成的 WordCloud 图片。
源码讲解到此结束。接下来我们就来看看源码的补充说明和词性标注。
<< 相关文章列表 >>