应用python的docx模块解析word文件内容

工作问题

某协2023的工作报告调研,联合各单位要合作写行业调查白皮书,期间构造了一套完整的word问卷,并在会后邀请各单位填写结果。最后要对问卷结果进行整理和数据分析,以ppt报告,数据分析报告等多种形式展现。因为是word的问卷文件,其中还包含了各类常见的题型和格式,还需要参考不同人的数据填写习惯尽可能通用的筛选出问卷填写的结果。将大量的word格式文字重新整理,从中提取关键信息,最后存储为结构化的数据,就是我们的目标。简言之,这是次有难度的挑战。

涉及知识点

  • 使用python语言完成目标
  • docx模块的基础应用
  • 应用正则表达式解构内容

实现过程

目标

我们初始拿到的是各单位发回的问卷合计约70份,并由不同小组归类整理成不同的分组文件夹。我们要读取全部的文件内容,并将其中各个题目下填写的结果提取出来,最后把数据以结构化的形式整理出来。

调用对应的库和模块

# 系统模块
import os
import re
from collections import Counter
# pip方法下载模块
import docx
import pandas as pd
import jieba

文件分散在各个文件夹内,如果不通过代码快速筛选出每个文件的引用路径,逐个打开的过程也有较大的工作量。

定义通用方法

1.【遍历全部的同格式文件】返回一个文件夹内,限定某类格式文件,返回全部这类文件的绝对路径

def traverse_folder(folder_path, filename_suffix, blacklist = ['~'],_ = []):
    '''
    遍历文件夹中逐层下钻内容,返回全体规定后缀名的文件路径
    para:
    folder_path:根文件夹路径,层级直接用除号分割
    filename_suffix:定义后缀名,参考格式[.docx|.xlsx|.zip|.7z|.exe]
    blacklist:定义文件黑名单,当文件名中包含对应内容时自动跳过
    _:不需要重定义,仅作列表格式空容器。当递归时用于存储前一轮中的结果
    '''

    for filename in os.listdir(folder_path):
        full_path = os.path.join(folder_path, filename)
        if all(blackname not in filename for blackname in blacklist) :
            if os.path.isdir(full_path):
                # 如果是一个子目录,递归调用自己
                traverse_folder(full_path,filename_suffix, blacklist = blacklist, _ = _)
            elif filename.endswith(filename_suffix):
                # 如果是一个文件,执行你要执行的操作
                _.append(full_path)
        else:
            pass
    return _
2.【提取段落&表格中的文本】word文件中常见三种类型,段落,表格,图片,这里提供前两种类型的文本内容
# ------------------------------------------------------------------------------------
def get_table_text(document):
    '''
    提取表格文本

    para:
    document:提供一个docx库下的document类

    return:全部表格内文本,合并为一整段长文本输出
    '''
    table_text = ''
    # 遍历文档中的表格
    for table in document.tables:
        # 遍历表格中的行
        for row in table.rows:
            # 遍历行中的单元格
            for cell in row.cells:
                # 提取单元格中的文本
                for paragraph in cell.paragraphs:
                    table_text += paragraph.text
    return table_text
# ------------------------------------------------------------------------------------
def get_paragraph_text(document):
    '''
    提取段落文本

    para:
    document:提供一个docx库下的document类

    return:全部文本,合并为一整段长文本输出
    '''
    paragraph_text = ''
    for para in document.paragraphs:
        paragraph_text += para.text
    return paragraph_text
    
3.【正则匹配】根据正则规则提取目标文本
# ------------------------------------------------------------------------------------

def get_choice_text(document):
    '''
    根据正则条件匹配全部中文括号里的内容,并清除特殊符号仅保留内容,多选时将选项结果合并展现
    para:
    document:获取docx库中的document类
    '''
    result = re.findall(r'(\s*([a-zA-Z0-9]*?)\s*)',document.text)
        # 匹配括号中的内容,前后出现空格时消除空格
    pattern =r'(\s*([A-Za-z\s]+)\s*)'
    document = re.sub(r"\s+", "", document.text)
    match = re.search(pattern,document)
    if match:
        result =  re.compile(r'[^\s]+').findall(match.group(1))
        
    return result

# ------------------------------------------------------------------------------------

def get_blank_text(text, start_string, end_string):
    '''
    根据正则条件匹配全部前后文之间空白处的文本内容,并清除特殊符号仅返回要提取的内容
    text:提供一段文本,str格式
    start_string:限定text中目标待提取文本的内容之前的部分
    end_string:限定text中目标待提取文本的内容之后的部分
    '''
    pattern = re.escape(start_string) + '(.*?)' + re.escape(end_string)
    match = re.search(pattern, text)
    if match:
        result =  re.compile(r'[^_|\s]+').findall(match.group(1))
        return result
    else:
        return ''

4.仅供参考:【获取内容出现的频次确定XX名称内容】

(因为问卷本身设计缺陷,导致没有固定位置可以获取名称,考虑使用词汇频次提取目标内容)


def get_bank_name(text):
    '''
    根据问卷中提及次数最多的银行名称确定来源
    para:
    text:提供一段文本,str格式
    return:
    '''
    # 使用jieba分词
    words = list(jieba.cut(text))

    # 合并银行词汇
    new_list = []
    for i, s in enumerate(words):
        # 如果当前元素中包含“银行”一词,将其与前面的元素组合起来,但要分情况讨论
        if re.match(r'.+?银行(?<=银行)', s):
            if s not in ['远程银行','网上银行','电子银行','电话银行','商业银行','手机银行']:
                # 当出现高频易混淆词汇时,直接建立手工码表剔除
                new_list.append(s)
            else:
                pass
            # 银行是非独立词汇,则jieba分词成功,直接输出
            new_list.append(s)
        elif re.match(r'银行', s):
            #银行是独立词汇,则匹配前一个字符并且合并
            new_s = ''.join(words[i-1:i+1])
            if new_s not in ['远程银行','网上银行','电子银行','电话银行','商业银行','手机银行']:
                # 当出现高频易混淆词汇时,直接建立手工码表剔除
                new_list.append(new_s)
            else:
                pass
        else:
            # 如果当前元素中不包含“银行”一词
            pass


    most_common = Counter(new_list).most_common(1)
    return [most_common[0][0],most_common[0][1]]

提取过程

遍历文件夹提取绝对路径

# 调用函数来遍历文件夹

# blacklist是测试后发现的提交格式不规范的文件,~为临时文件
folder_path = 'C:/Users/xxx/Desktop/新建文件夹'  # 更改为你要遍历的文件夹路径
files_path = traverse_folder(folder_path,'.docx',blacklist=['~','CS15','NH4','CS26'],_=[])

文本内容提取逻辑

# 实例化一个dataframe
df = pd.DataFrame()

for i,file in enumerate(files_path):
    # print(i,".",file)     # 注释项:输出文件路径
    document = docx.Document(file)

    paragraph_text= get_paragraph_text(document)
    
    # 如果在预定填写位置填入了信息,则直接获取
    if '参与调研单位名称:' in paragraph_text and '问卷填写人:' in paragraph_text:
        pattern = "参与调研单位名称:(.*?)问卷填写人:"
        match = re.search(pattern, paragraph_text)
        # print('choose',match.group(1))
        bank_name = match.group(1)
    
    # 如果没有填入,则依靠关键词频次获取
    else :
        table_text= get_table_text(document)
        table_boolean_result = get_bank_name(table_text)
        paragraph_text= get_paragraph_text(document)
        paragraph_boolean_result = get_bank_name(paragraph_text)
        
        if 7*table_boolean_result[1] < paragraph_boolean_result[1] :            # 人为配置权重,只有当正文中出现银行命名关键词的次数比表格出现次数7倍还多,则输出正文的判断结果,否则输出表格的【权重尺度可以在合理范围内尽可能放大】
            # print('choose',paragraph_boolean_result[0] ,'rather than',table_boolean_result[0])
            bank_name = paragraph_boolean_result[0]
        else :
            # print('choose',table_boolean_result[0] ,'rather than',paragraph_boolean_result[0])
            bank_name = table_boolean_result[0]


    # --------------------------------------------------------

    #获取所有段落
    all_paragraphs = document.paragraphs
    paragraph_length =  len(all_paragraphs)
    i=0
    answers = []
    

    while i <paragraph_length:
        if '1.(单选)问题1' in all_paragraphs[i].text:
            answer = get_choice_text(all_paragraphs[i])
            # print(answer)
            answers.append(answer)
        elif '2.(不定项)问题2' in  all_paragraphs[i].text:
            answer = get_choice_text(all_paragraphs[i])
            # print(answer)
            answers.append(answer)
        elif '3.(多选)问题3' in  all_paragraphs[i].text:
            answer = get_choice_text(all_paragraphs[i])
            # print(answer)
            answers.append(answer)
        elif '6.问题6' in  all_paragraphs[i].text:
            start_index = i
            string_list = ['文本1','文本2','文本3','文本4','文本5']
            for j in range(i,paragraph_length,1):
                if '7.(填空题)问题7' in  all_paragraphs[j].text:         # 截取到下一题开始的位置
                    end_index = j
                    break
                else :
                    pass
            # 将各段内容合成一个列表
            target_paragraph = [all_paragraphs[k].text for k in range(start_index,end_index,1)]
            # 把列表结果拼成一段话
            answer = ''.join(target_paragraph)
            for m in range(len(string_list)-1):
                answers.append(get_blank_text(answer,string_list[m], string_list[m+1]))

        elif '8.问题8' in  all_paragraphs[i].text:
            start_index = i
            for j in range(i,paragraph_length,1):
                if '9.问题9' in  all_paragraphs[j].text:         # 截取到下一题开始的位置
                    end_index = j
                    break
                else :
                    pass
            # 将各段内容合成一个列表
            target_paragraph = [all_paragraphs[k].text for k in range(start_index+1,end_index,1)]
            # 把列表结果拼成一段话
            answer = ''.join(target_paragraph)
            # answer = re.findall(re.compile(r'\n{0,}(.+?)(?=\n{1,}|$)', re.DOTALL), all_paragraphs[start_index:end_index].text)        # 备用筛选格式,正则表达式也可以实现
            # 以列表形式存储
            answer = answer.split(" ")    
            # print(answer)
            answers.append(answer)

        i += 1
    
    # 将二维列表转换为一维,看情况输出结果
    one_dim_list = [element for row in answers for element in row]
    one_dim_list = [file]+[bank_name]+one_dim_list
    # print(one_dim_list)
    
    consule = pd.Series(one_dim_list)
    df = df.append(consule, ignore_index=True)


df.to_excel('output.xlsx', index=False)
# df就是我们需要的结果,其中以dataframe的形式存储每个空白的答案(一题中出现多个空白时,一个空白占据一个dataframe单元格)

在这段代码中,

  1. 当提取选择题/填空题时,因为原始问卷格式中,预留填空位置用中文括号圈定,且一题中可能存在一个或多个括号。(详见问题1,2,3)
  2. 当提取长文本填空题时,需要在一段文本中提取下划线结构的填空题时,可以参考第6题结构,将原题按照填空空格分割为多段文本[‘文本1’,‘文本2’,‘文本3’,‘文本4’,‘文本5’](详见问题6)
  3. 当提取主观题时,需要提取大段的内容时,可以先限定前后两题的大致范围,然后在范围内加工目标文本。(详见问题8)

表格内容提取逻辑

这里仅用第一份问卷中的内容做测试,如果想全部输出,请参考文本提取逻辑,加入外层循环

# 这里仅用第一份问卷中的内容做测试,如果想全部输出,请参考文本提取逻辑,加入外层循环
document = docx.Document(files_path[1])
# 获取文档中的所有表格
all_tables = document.tables

# 创建一个字典,分别存储表格所在段落位置和表格内容
tables_dict = {}

# 遍历所有表格
for table in all_tables:
    # 获取表格中的所有行和列
    rows = table.rows
    cols = table.columns

    # 创建一个空的列表,用于存储表格中的所有数据
    data = []
    
    # 遍历所有行和列,将表格中的数据存储到列表中
    for i, row in enumerate(rows):
        row_data = []
        for j, cell in enumerate(row.cells):
            row_data.append(cell.text)
        data.append(row_data)
    
    # 将列表转换为DataFrame,并添加到已有的数据中
    table_df = pd.DataFrame(data[1:], columns=data[0])
    table_key = table_df.columns[0]
    tables_dict[table_key] = table_df

# 打印问卷中所有表格索引结果,索引为首行首列表格内容
print(tables_dict.keys())
tables_dict['渠道']

获取了一份问卷中全部有价值的文本内容,并将内容以结构化的数据形式存储在excel中。大浪淘沙,矿床淘金,在信息中人为熵减提取价值的过程,正是数据加工处理中耗时最长,过程最枯燥,但却是一切的基石。而分析师的魅力正在于此。

总结

  1. 在问卷调查越来越方便的今天,运用问卷星,金数据,亦或是国外surveymonkey这样的工具,执行一轮调查并在后台汇总问卷结果,显然从操作上更加方便。但以如此复杂形式实现,既是为了数据本身的绝对安全和保密,也是因为市场里现有的各类问卷工具,虽然在调查题上涉猎广泛,格式全面,但距离真正的面面俱到和量身定制仍有一定距离。
  2. 数据分析过程中,往往人们专注于加工和分析的过程,而忽略了数据本身来源就是多样化的,也是杂乱无章的。数据产生于各个角落,而问卷这种古老的形式,当收集问题复杂,且电子化分卷存储时,频繁的打开关闭操作极度浪费人力。
  3. 统一回复一下为什么不用XXX解决,而要用上述的方式解决问题:
    。【OCR识别】发送给每个单位的问卷是word的格式,在主观题部分,允许各团队自由发挥,且允许插入配图等内容。OCR因为格式不固定,所以没法向识别发票一样解决问题。
    。【问卷星等产品】数据涉密,无法使用在线问卷调查工具。
    。【待补充…】
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值