● 研二在读学生,非工科非计算机专业,故代码简陋初级勿喷,本文仅为记录和快乐分享。
○ 感谢肯定,感谢点赞收藏分享,转载请注明本页出处即可。 ____Ⓙ即刻@王昭没有君
本文仅为笔者摸索总结-欢迎订正补充交流讨论-
❤python识别word文件格式 ——(专栏:基于python编写简单office阅卷程序①)
————————
一、整体思路:
🍒1. 使用python第三方库docx识别尽可能多的word格式;(更简单方便)
- 使用 dir() 查看当级存在的属性或下级对象(不含双下划线__的)
- 使用 (.属性)试图调用查看属性,或(.对象)进入下级对象
🍒2. 将.docx转为.xml格式文件,读取标签,补充识别docx库无法识别的格式;
- 解压word.docx文件为xml文件(不止一个,有好几个文件夹)
- 找到相应的属性在xml文件中的存储标签名和层级
- 使用(层级.tag)(层级.attrib)(层级.text) 试图取出该属性
🍒3. office有个懒惰且简洁的规则是,很多默认属性和格式,若该文档中作者未修改默认格式或属性,则在xml文件中该属性或格式的标签不存在 ,则在用python抽取该格式或属性时,返回值为None或不存在,有时还会报错。例如:
- 默认字体为宋体(有的版本是宋体(标题)或宋体(正文))
- 默认字号小三(也可能因版本不同而不同或.doc和.docx差异)
- 默认无首行缩进、默认行间距1.0倍等
而在修改了这些格式后,该属性标签会存储在.docx和.xml文件中。又不像是完全的日志文件。
————————
二、使用python库情况
此处均为编写阅卷程序用到的,若只识别word格式,并不需要以下全部:
import docx # 读取word文件
import xlrd # 读取excel文件,主要是获取名单和创建地址用
import openpyxl # 读取/写入 excel文件,主要是记录成绩用
import os # 使用文件路径等
import xml.etree.ElementTree as ET # 读取xml文件
除此之外,在解压转为xml文件时还用到以下库:
import os # 因笔者分开写的解压程序,解压也用到os库
import xlrd # 因笔者分开写的解压程序,解压也用到xlrd库,主要是获取名单和创建地址用
import shutil # 删除配置文件
import zipfile
# 解压word(.docx)、excel(.xlsx)、ppt(.pptx)文件成为.xml格式文件
————————
三、docx库识别文档结构:
-
document:
- sections:
- parapraphs:
- runs:
-
sections和paragraphs是同级关系
-
表格和图片游离于sections和paragraphs
例如 :
🍍1.读取文件:docx.Document ( ’ 文件地址 ’ )
file = docx.Document(r"F:\文件地址\word.docx")
🍍2.读取节们:文件.sections
sections = file.sections # 节们
for section in sections: # 遍历节
print(section.page_height) # 页高
(1)分节符是分割节与节的标志(未尝试过分页符,欢迎补充);
(2)有关页面的属性基本都在sections部分里;
(3)节属性包括但不仅限于: # 使用print(dir(section)) 、print(dir(sections))查看更多属性和下级对象
- 页高 :section.page_height
- 页宽 :section.page_width
- 页面横纵 :section.orientation
- 装订线 :section.gutter
- 左边距 :section.left_margin
- 右边距 :section.right_margin
- 上边距 :section.top_margin
- 下边距 :section.bottom_margin
- 页眉:section.header
- 页脚 :section.footer
✨——其中页眉: # 使用print(dir(section.header)) 查看更多属性和下级对象
- 页眉顶端距离 :section.header_distance
- 页脚底端距离 :section.footer_distance
- 页眉内容 :section.header.paragraphs[0].text
- 页眉对齐 :section.header.paragraphs[0].alignment
- 页眉字号:section.header.paragraphs[0].runs[0].font.size
- 页眉字体:section.header.paragraphs[0].runs[0].font.name
✨——页脚类似,但页码只能从xml文件识别。
🍍3.读取段落们:文件.paragraphs
paragraphs = file.paragraphs # 段落们
for i in range(len(paragraphs)): # 遍历段落 也可以写成上面节的遍历形式,此处须为后续保留段号i,故写成这种形式。
paragraph = paragraphs[i]
if paragraphs[i].text != "": # 筛选非空段
print(paragraph.text) # 段落内容
(1)有关段落的属性基本都在paragraphs部分里;
(2)节属性包括但不仅限于: # 使用print(dir(paragraph)) 、使用print(dir(paragraphs))查看更多属性和下级对象
- 整段内容 :paragraph.text
- 对齐方式 :paragraph.alignment
- 段前距 :paragraph.paragraph_format.space_before
- 段后距 :paragraph.paragraph_format.space_after
- 左侧缩进 :paragraph.paragraph_format.left_indent
- 右侧缩进 :paragraph.paragraph_format.right_indent
- 首行缩进 :paragraph.paragraph_format.first_line_indent
- 行间距 :paragraph.paragraph_format.line_spacing
(3)分栏、项目符号不在paragraphs属性里,只能从xml文件识别。
🍍4.读取字块们:文件.paragraphs
paragraphs = file.paragraphs # 段落们
for i in range(len(paragraphs)): # 遍历段落
paragraph = paragraphs[i]
if paragraph.text!="": # 筛选非空段
for run in paragraph.runs: # 遍历字块
print(run.text) # 字块内容
break
(1)有关字的属性基本都在runs部分里;
(2)runs字块在切割时,常以相同属性分割,遇到不同属性时分割,如:
-
全球超级计算机500强榜单20日公布,“神威太湖之光”登上榜首。
-
若设置中英文不同字体,则该段落字块被分成:全球超级计算机///500///强榜单///20///日公布,“神威太湖之光”登上榜首。
-
原理上笔者猜测各属性完全一致的一个段落会被划分为一个整字块。
而在笔者实际操作阅卷时,学生总有离奇的神操作,同样的段落常被分割为不同的字块,且同一文件运行几次每次结果都不一样,令人咬牙切齿。故在实际运用时,笔者只取首字块的首字调取其属性。
-
(3)字块属性包括但不仅限于: # 使用print(dir(run)) 、使用print(dir(runs))查看更多属性和下级对象
- 内容 :run.text
- 字体 :run.font.name # font
- 字号 :run.font.size
- 斜体 :run.font.italic
- 加粗 :run.font.bold
- 下划线 :run.font.underline
- 颜色 :run.font.color.rgb # 颜色RGB值
🍍5.读取表格们:文件.tables
tables = file.tables # 表格们
for table in tables: # 遍历表格
for row in table.rows: # 遍历表格行
r1 = r1 + 1
for cells in row.cells:
print(cell.text) # 逐行打印表格内容
for column in table.columns: # 遍历表格列
r2 = r2 + 1
print(r1,r2) # r1行数 r2列数
(1)表格属性 在tables中:
- 表格对齐方式 :table.alignment # 区别于单元格对齐方式
(2)行列属性 在rows和columns中:
- 表格行高 :row.height
- 表格列宽 :column.width
(3)单元格属性 在cells中:
- 单元格内容 :cell.text
- 单元格对齐方式 :cell.alignment
🍍6.读取图片们:文件.inline_shapes
pics = file.inline_shapes # 图片们
for pic in pics: # 遍历图片
- 图片长 :pic.width
- 图片宽 :pic.height
- (不知道是什么type) :pic.type
docx库里能识别到的格式并不完整,本文有提到可识别的大部分格式,其余多数只能通过读取xml文件调取。虽然xml文件可以识别到全部格式,但使用docx库读取还是更加简便,能不用xml就尽量不用。
然而库功能并不完全,此时需要读取.docx文件转成的.xml文件,识别其中格式。其中最常用的格式是页码。本文以识别页码为例。
————————
四、读取xml文件识别文档结构:
🍋1.文件转换
(1)最直接的方法:手工将word.docx文件重命名为word.zip文件,再解压缩。
①原文件
②是
③解压到当前文件夹,或你选择的地方
④我们需要使用到的xml文件都在解压后的word文件夹里
⑤根据文件的不同,里面的xml文件数量和内容均有差异。例如笔者这个文档有页眉和页脚,故该文件夹下才有footer123.xml和header123.xml。若无页眉页脚且无修改页眉页脚历史记录,则该文件夹下不含footer123.xml和header123.xml。用记事本可以简单地打开查看内容。
(2)使用代码批量转换。在CSDN上有很多代码分享,可自行查阅word转xml文件等关键字。此处贴笔者使用的代码,尴尬的是笔者找不到原出处了,若原作者看见本文请联系笔者填写出处或删除本部分,非常抱歉。
- 因笔者将试卷文件夹设置为学生学号,每个学号的文件夹里有三个需要读取的文件,故先读取名单中学号,以学号作为试卷地址路径的一部分,批量每个学号解压文件夹里的三个文件。
import os
import zipfile
import shutil
import xlrd
class Name_list():
def __init__(self, file_address):
self.file_address = file_address
pass
def read(self, sheet_name):
workbook = xlrd.open_workbook(self.file_address)
sheet = workbook.sheet_by_name(sheet_name)
data = []
for i in range(0, sheet.nrows):
data.append(sheet.row_values(i))
return data
pass
def unzip_file(path, filenames):
print(path)
#print(os.listdir(path))
for filename in filenames:
filepath = os.path.join(path,filename)
if os.path.exists(filepath):
zip_file = zipfile.ZipFile(filepath) # 获取压缩文件
#print(filename)
newfilepath = filename.split(".",1)[0] # 获取压缩文件的文件名
newfilepath = os.path.join(path,newfilepath)
#print(newfilepath)
if os.path.isdir(newfilepath): # 根据获取的压缩文件的文件名建立相应的文件夹
pass
else:
os.mkdir(newfilepath)
for name in zip_file.namelist(): # 解压文件
zip_file.extract(name,newfilepath)
zip_file.close()
Conf = os.path.join(newfilepath,'conf')
if os.path.exists(Conf): # 如存在配置文件,则删除(需要删则删,不要的话不删)
shutil.rmtree(Conf)
print("解压{0}成功".format(filename))
def main():
for j in range(int(len(student_list) - 2)):
stu_id = student_idlist[j]
address_stu_id = str(address_beforeid + str(stu_id)) # 试卷地址
if os.path.exists(address_stu_id):
filenames = ['excel操作题.xlsx', 'PPT操作题.pptx', 'word操作题.docx'] # 目录下需要解压的文件名
unzip_file(address_stu_id, filenames)
pass
if __name__ == '__main__':
address_idlist = r"F:\名单.xlsx" # 名单
address_beforeid = 'F:\\试卷\\' # 试卷路径学号文件夹之前的部分
student_list = Name_list(address_idlist).read('Sheet1')
student_idlist = [[] for r in range(int(len(student_list) - 2))]
for k in range(int(len(student_list) - 2)):
student_idlist[k] = int(student_list[k + 2][1])
pass
main()
(3)转换完成后的效果:
🍋2.读取xml识别word页码
————关于格式在哪个xml文件的哪层标签、如何找放在下一小节,先上结论。
(1)页码的标签一般在word文件夹中footer2.xml文件中,有页码但找不到考虑寻找footer1.xml和footer3.xml内容。
(2)根标签内的< ftr > < /ftr>标签内的2级< sdt > < /sdt >标签中存储页码相关属性,文件中有< sdt > < /sdt >标签在阅卷时至少证明该考生对页码进行过操作,会使用页码功能。
(3)< sdt > < /sdt >标签内的5级标签< instrText > < /instrText >内存储页码格式信息。页码样式不同在 instrText标签例如本题要求学生添加型如 “ - 1 - ” 形式的页码,则在instrText中的.text为 :PAGE * MERGEFORMAT,另有5级标签<jc></jc>中的.attrib存储页码对齐方式,此处默认对齐方式为居中,页码默认居中时, xml文件中无 jc标签。
- 贴上读xml文件的代码,此处代码参考:
https://blog.csdn.net/weixin_36279318/article/details/79176475
import xml.etree.ElementTree as ET
class Xml2DataFrame:
def __init__(self,xmlFileName):
self.xmlFileName = xmlFileName
pass
def read_xml(self):
tree = ET.parse(self.xmlFileName)
root = tree.getroot()# 第一层解析
#print('root.tag:', root.tag, ',root-attrib:', root.attrib, ',root-text:', root.text)
for sub1 in root:# 第二层解析
child = sub1
print('sub1.tag:', child.tag, ',sub1.attrib:', child.attrib, ',sub1.text:', child.text)
for sub2 in sub1:# 第三层解析
child = sub2
print('sub2.tag:', child.tag, ',sub2.attrib:', child.attrib, ',sub2.text:', child.text)
(此处继续嵌套for 略写)
if __name__ == '__main__':
file_path = r'F:\考试文件夹\学号\word操作题\word\footer2.xml'
xml_df = Xml2DataFrame(file_path)
xml_df.read_xml()
得以下输出结果 :
sub1.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}sdt ,sub1.attrib: {} ,sub1.text: None
sub2.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}sdtPr ,sub2.attrib: {} ,sub2.text: None
sub3.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}id ,sub3.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': '-482698706'} ,sub3.text: None
sub3.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}docPartObj ,sub3.attrib: {} ,sub3.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}docPartGallery ,sub4.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': 'Page Numbers (Bottom of Page)'} ,sub4.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}docPartUnique ,sub4.attrib: {} ,sub4.text: None
sub2.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}sdtContent ,sub2.attrib: {} ,sub2.text: None
sub3.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}p ,sub3.attrib: {'{http://schemas.microsoft.com/office/word/2010/wordml}paraId': '57F7B8B0', '{http://schemas.microsoft.com/office/word/2010/wordml}textId': '0880F220', '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rsidR': '001547AD', '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rsidRDefault': '001547AD'} ,sub3.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}pPr ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}pStyle ,sub5.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': 'ac'} ,sub5.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}jc ,sub5.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': 'right'} ,sub5.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}r ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldChar ,sub5.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType': 'begin'} ,sub5.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}r ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}instrText ,sub5.attrib: {} ,sub5.text: PAGE \* MERGEFORMAT
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}r ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldChar ,sub5.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType': 'separate'} ,sub5.text: None
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}r ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}rPr ,sub5.attrib: {} ,sub5.text: None
sub6.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}lang ,sub6.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': 'zh-CN'} ,sub6.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}t ,sub5.attrib: {} ,sub5.text: 2
sub4.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}r ,sub4.attrib: {} ,sub4.text: None
sub5.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldChar ,sub5.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType': 'end'} ,sub5.text: None
sub1.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}p ,sub1.attrib: {'{http://schemas.microsoft.com/office/word/2010/wordml}paraId': '337E501C', '{http://schemas.microsoft.com/office/word/2010/wordml}textId': '77777777', '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rsidR': '001547AD', '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rsidRDefault': '001547AD'} ,sub1.text: None
sub2.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}pPr ,sub2.attrib: {} ,sub2.text: None
sub3.tag: {http://schemas.openxmlformats.org/wordprocessingml/2006/main}pStyle ,sub3.attrib: {'{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val': 'ac'} ,sub3.text: None
(4)在读取xml每一层内容时,因笔者水平有限,使用for嵌套for嵌套for读取每一层。在读取较复杂文件时不得不嵌套到了20个for。经请教得知可使用递归写法。但递归算法占用内存较多,对电脑要求较高。考虑到学院办公室的电脑配置,也有笔者水平有限,运用递归实属痛苦,此优化暂停。
(5)阅卷抽取关键信息时定义一变量a用以存储内容,if ‘sdt’ in child.tag : a = str(child.tag + child.attrib + child.text),判断字符串内容,再定标判分即可。
(6)两个非常相似的.docx可能因为一个小差异,在xml文件存储时格式信息存储的层数有很大区别,笔者遇见最高相差5层。因此在寻找某标签的层数时、输出时若找不到,除了考虑是不是默认属性,还要考虑是否存储在相邻的上下几层中。
🍋3.查看xml寻找位置
(1)识别某格式,首先需要找到该格式的xml存储标签。笔者新建3个空白word,第一个空白保存,第二个添加普通页尾,第三个添加要求格式页码。
(2)上面提到我们可以使用记事本简单地打开xml文件查看内容,此处对这三个文件进行比较。使用记事本打开xml文件,复制全文粘贴到某新建excel表格中某单元格,使用excel分列功能以’ < '符号分列 , 就可以找到每个标签中的区别了。在经过筛查后最终确定该格式存储在某标签中。
反正是笨办法英文单词连蒙带猜加谷歌翻译四级水平绰绰有余
(3)下一步我们需要确定标签大致层数,使用代码使其xml输出显示逐层推进,此处代码参考:
https://blog.csdn.net/qq_41958123/article/details/105357692
import xml.dom.minidom
uglyxml = '需要输出的xml内容'
xml = xml.dom.minidom.parseString(uglyxml)
xml_pretty_str = xml.toprettyxml()
print(xml_pretty_str)
输出如图:
全选复制粘贴进excel:
再搜索标签名确定位置,数列数即可。
————————
五、总结
- 这是office三件套的第①部分——word部分,接下来要去准备网课考试和组会了。有空再梳理excel部分、ppt部分和面对学生们的奇葩操作,为了防止程序崩溃中断应注意的各种注意事项。
- 反正就是笨办法只考验耐心的办法,从一开始就处处暴露我的非专业性,我也找不到更好的办法,我就是闲的,不想人工改卷子想一劳永逸。我菜我认了别骂我,不爱看右上角八叉关闭谢谢您的善意。
我终于梳理完了怎么这么长
六、参考链接
https://blog.csdn.net/weixin_36279318/article/details/79176475
https://blog.csdn.net/qq_41958123/article/details/105357692
来源:oschina
链接:https://my.oschina.net/u/4330928/blog/4771946