某协2023的工作报告调研,联合各单位要合作写行业调查白皮书,期间构造了一套完整的word问卷,并在会后邀请各单位填写结果。最后要对问卷结果进行整理和数据分析,以ppt报告,数据分析报告等多种形式展现。因为是word的问卷文件,其中还包含了各类常见的题型和格式,还需要参考不同人的数据填写习惯尽可能通用的筛选出问卷填写的结果。将大量的word格式文字重新整理,从中提取关键信息,最后存储为结构化的数据,就是我们的目标。简言之,这是次有难度的挑战。
我们初始拿到的是各单位发回的问卷合计约70份,并由不同小组归类整理成不同的分组文件夹。我们要读取全部的文件内容,并将其中各个题目下填写的结果提取出来,最后把数据以结构化的形式整理出来。
# 系统模块
import os
import re
from collections import Counter
# pip方法下载模块
import docx
import pandas as pd
import jieba
文件分散在各个文件夹内,如果不通过代码快速筛选出每个文件的引用路径,逐个打开的过程也有较大的工作量。
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 _
# ------------------------------------------------------------------------------------
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
# ------------------------------------------------------------------------------------
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 ''
(因为问卷本身设计缺陷,导致没有固定位置可以获取名称,考虑使用词汇频次提取目标内容)
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单元格)
在这段代码中,
这里仅用第一份问卷中的内容做测试,如果想全部输出,请参考文本提取逻辑,加入外层循环
# 这里仅用第一份问卷中的内容做测试,如果想全部输出,请参考文本提取逻辑,加入外层循环
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中。大浪淘沙,矿床淘金,在信息中人为熵减提取价值的过程,正是数据加工处理中耗时最长,过程最枯燥,但却是一切的基石。而分析师的魅力正在于此。