资讯动态

Python解析Word期末试卷:从.doc到JSON的结构化转换与批量生成

发布时间:2026/9/19 21:37:46 来源:尧图企业网站定制
简介这是一份人教版七年级英语期末试题文档面向七年级学生、家长及英语教师适用于期末复习自测、模拟考试和命题参考。试卷内容完整涵盖听力、词语释义和单项填空等模块并附有听力稿方便自主听练与课堂讲评听力部分涉及时间问询、职业讨论、天气感受等真实场景知识运用部分则聚焦词汇辨析、冠词和时态等核心语法点。试题结构清晰难度循序渐进既适合学生考前系统梳理也方便教师组织专项训练。文档共1个doc文件体积约560KB轻量便捷可直接打印或编辑。目前已有485人学习使用题目注重基础知识与生活场景结合能有效检验学生对时态、冠词、职业词汇等要点的掌握情况。配合详细的试题解析还能帮助使用者快速定位薄弱环节、查漏补缺是一份兼顾实用与备考价值的复习资料。1. 一道 .doc 期末题为什么值得拆成结构化数据很多英语老师考前最忙的一件事不是出题而是把手里那份「人教版七年级英语期末试题含听力稿.doc」从头到尾过一遍改年份、换两篇阅读、把听力稿里的名字换掉再重新排版。这些操作看起来是在编辑 Word实际上每一次都是在做同一件事把一份半结构化的文档按题型、按分值、按听力原文和题目之间的对应关系重新组织一遍。人做这件事容易漏因为 .doc 里没有「这道题属于哪个题型」的字段只有加粗的标题、制表位和一堆看似整齐实则不统一的段落格式。这类文件的痛点不在「打开」而在「复用」。听力稿要单独提取给播音老师答案要单独拆给阅卷组题目要按题型统计分值而 .doc 本身把这些信息混在同一份文本流里。把它转成结构化数据比如 JSON就等于给这份卷子建立了索引之后无论是生成答案页、做试卷变体、还是按题型抽题都只是在索引上操作。作为 IT 从业者你不需要懂英语教学只要把 Word 的段落、表格、字体属性当作数据源来处理就能把这个场景做成一套可复用的工具链。这篇就从一份真实的 .doc 出发讲怎么读取、怎么拆解、怎么把听力稿和题目文本对应起来最后怎么再生成一份干净的 .doc。2. 用 Python 打开 .doc先转 .docx 再读段落与表格2.1 为什么不直接用 python-docx 读 .docpython-docx 是处理 Word 文档最常用的库但它只支持 .docx也就是 Office 2007 以后的 XML 格式。老式的 .doc 是 OLE 复合文档python-docx 打开会直接报错。很多从学校 FTP 或者U盘里拷出来的试卷文件名是 .doc实际内容可能是两种一种是真正的二进制 .doc另一种是把 .docx 改了后缀。所以第一步永远是探测真实格式而不是看扩展名。常见做法是先用 file 命令或者读文件头的 magic bytes 判断。.doc文件的头两个字节一般是D0 CF.docx是PK其实就是 zip 的头部。判断完再走不同的处理路径。如果是真 .doc我一般用 LibreOffice 的无头模式转成 .docx命令大概是soffice --headless --convert-to docx --outdir ./converted 期末试题.doc这行命令的工作方式是LibreOffice 启动无界面实例把指定目录下的 .doc 转换并输出到./converted目录文件名不变只是扩展名变为 .docx。做批量转换时可以直接遍历目录一次传多个文件。注意--outdir参数要放在--convert-to之后顺序写反会导致输出文件写到当前目录。转换完成后用 python-docx 读取就顺畅了。还有一个细节LibreOffice 转换出来的 .docx 在格式还原度上比 WPS 另存为要好尤其是听力稿部分经常带着下划线、批注和修订痕迹LibreOffice 会保留段落结构而有些在线转换工具会把这些信息扁平化。2.2 读取段落和表格并保留顺序教学楼里流传的旧版试卷听力稿和笔试题目往往不在同一个文档里或者是听力稿放在最后几页。而这份「含听力稿」的 .doc 通常是这样排的前面是听力材料原文中间是听力题后面是笔试部分最后附一份参考答案。要把这种混合结构拆开光读 paragraph 是不够的因为题目里的选项经常放在表格里表格在 python-docx 里是独立的 body 元素。遍历 body 元素时必须同时处理 paragraph 和 table并且保持它们在文档里出现的先后顺序。python-docx 提供了document.element.body直接遍历这个 XML 元素的子节点遇到w:p就解析段落遇到w:tbl就解析表格。from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph doc Document(converted/期末试题.docx) def iter_block_items(parent): from docx.oxml.ns import qn body parent.element.body for child in body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, parent) elif child.tag qn(w:tbl): yield Table(child, parent) blocks list(iter_block_items(doc)) for i, block in enumerate(blocks): if isinstance(block, Paragraph): text block.text.strip() if text: print(i, P, text[:40]) else: print(i, T, f{len(block.rows)}行x{len(block.columns)}列)这段代码的核心是iter_block_items这个生成器。它直接操作文档的 XML 树按文档顺序产出段落和表格对象而不是像doc.paragraphs那样只能拿到纯段落。判断节点类型时用qn(w:p)来解析带命名空间的标签名这是一个很容易踩坑的点直接比较child.tag会得到带命名空间前缀的完整字符串字符串匹配很容易写错。2.3 格式信息是拆题的隐藏线索读取文本内容只是第一步更关键的是读格式。老试卷里题型标题比如「一、听力部分」通常用黑体加粗题干正文用宋体五号听力稿里的对话人名字会加粗或者带括号。这些格式信息恰恰是划分题型边界的依据。python-docx 里读取段落格式和字体格式的常规姿势如下for p in doc.paragraphs: if not p.text.strip(): continue fmt p.paragraph_format runs p.runs if not runs: continue run0 runs[0] font_name run0.font.name size run0.font.size.pt if run0.font.size else None bold run0.font.bold print(p.text[:30], |, font_name, size, bold if bold else )通过字体名和字号可以把文档切分成三大区块字号 14 到 16 且加粗的是大标题例如「人教版七年级英语期末试题」字号 12 加粗的是题型标题例如「一、听力理解」正文则是 10.5 到 12 磅不加粗。把这个规则做成一个分类函数再结合前面按顺序遍历得到的块列表就能给每个块打上「标题 / 题干 / 选项 / 听力稿正文」的标签。这里要提醒一个容易误判的情况听力稿里的人名「Tom」「Lucy」会在 Word 里被设为西文字体 Times New Roman而中文字体是宋体。python-docx 的run.font.name只能拿到w:rFonts里的ascii属性如果文件里设置的是eastAsia字体读出来是 None。想读中文字体得手动访问 XMLrPr run0._element.rPr rFonts rPr.find(qn(w:rFonts)) if rPr is not None else None ea_font rFonts.get(qn(w:eastAsia)) if rFonts is not None else None这一行代码解决的问题是为什么同一份文档有的机器上显示黑体代码读出来却是 None。Word 把中英文字体分开存放取不到eastAsia属性时不要轻易用font.name的结果下结论。3. 听力稿与题目文本的结构化解析按题型切分并生成 JSON3.1 先切开段落流再识别题型边界拿到带标签的块列表后拆卷子的逻辑就变成了一个顺序扫描问题。我习惯先把所有块的文本和标签放进一个列表然后写一个状态机当前处于哪个题型、当前处理的是题干还是选项、是否已经进入听力稿区域。状态机的转移规则参考实际试卷的版式设计块内容特征判定题型状态动作以「听力」开头加粗字号 12 以上听力部分开始重置当前题型标记后续为听力以「一、」「二、」等序号开头加粗大题边界新建 current_section写入题目类型行首是「( ) 1.」或「1.」小题题干记录题号等待读取选项行首是「A.」「B.」「C.」选项追加到当前题号的 options 列表含「听力原文」「录音稿」「Text」字样听力稿区域单独标记为 listening_script不再当题目处理这个表对应的是一个可以被代码复用的启发式规则集。真实试卷里经常有不按套路出牌的情况比如听力稿里每一段前面写着「Text 1」「Text 2」而这些标记也会匹配「1.」这个规则所以判断顺序很重要先判断是否进入听力稿区域再判断是否是题目。import re blocks [...] # list of (tag, text) sections [] current_section None current_question None in_script False script_lines [] section_pattern re.compile(r^[一二三四五六七八九十]、) question_pattern re.compile(r^\(?\s*([0-9]{1,2})\s*\)?\.) option_pattern re.compile(r^([A-D])[\.、\s]) for tag, text in blocks: if 听力原文 in text or 录音稿 in text: in_script True continue if in_script: script_lines.append(text) continue if section_pattern.match(text.strip()): current_section {title: text.strip(), questions: []} sections.append(current_section) current_question None continue m question_pattern.match(text.strip()) if m and current_section is not None: current_question {no: int(m.group(1)), stem: text.strip(), options: []} current_section[questions].append(current_question) continue m option_pattern.match(text.strip()) if m and current_question is not None: current_question[options].append(text.strip())这段代码的关键在正则的边界处理。question_pattern用了^\(?\s*([0-9]{1,2})\s*\)?\.意思是可以匹配「1.」「(1) .」「( 1 ).」这类宽松写法但不会匹配「Text 1」这种没有点号结尾的句子。section_pattern限定必须以汉字序号开头避免把听力稿里的「一、」意外切成大题。运行完这段后sections就是干净的 JSON 前身script_lines是听力稿全文。3.2 听力稿需要单独的结构每段对应一组题目听力稿和听力题之间存在对应关系一段录音对应 1 到 5 道题。拆听力稿不能只存一个字符串列表因为后续如果要生成「只含听力稿的教师版」或者要做听力题和原文的对照表就必须知道「Text 2 对应第 3 到第 5 题」。在扫描script_lines时按Text N切分text_re re.compile(r^Text\s*(\d), re.IGNORECASE) script_list [] current_text None for line in script_lines: m text_re.match(line.strip()) if m: current_text {text_no: int(m.group(1)), content: []} script_list.append(current_text) elif current_text is not None: current_text[content].append(line.strip())对应关系可以后续通过题号区间来关联。听力部分通常会在题干里写明「听下面 5 段对话每段对话后有一个小题」第一段 Text 对应第 1 题第二段对应第 2 题。把这个规则外置为配置项mapping [] idx 1 for s in script_list: mapping.append({ text_no: s[text_no], question_start: idx, question_end: idx # 默认一对一 }) idx 1如果某一段录音对应多道题就把question_end的类型改成数组或者用 question 数组直接挂在script_list的元素上。从复用角度考虑我推荐把题目数组直接内嵌进听力块里这样生成对照时不用再算区间。3.3 导出 JSON 并检查完整性解析完所有题型和听力稿后统一导出为一个 JSON 文件。这个 JSON 就是后续所有自动化操作的中间件。{ title: 人教版七年级英语期末试题, sections: [ { title: 一、听力理解, questions: [ {no: 1, stem: Who is the boy talking to?, options: [A. His teacher, B. His mother, C. His friend], answer: B} ] } ], listening_script: [ {text_no: 1, questions: [1], content: [M: Hi, Mom. This is my new friend, Li Lei., W: Nice to meet you, Li Lei.]} ] }导出后立刻做一次完整性检查这一步可以帮你在后续生成环节省下大量排错时间。检查逻辑包括每个 section 下是否有题目、题号是否连续、听力稿里的题号是否都在听力题范围内、有没有选项缺失。写一个只输出问题行的校验脚本import json data json.load(open(paper.json, encodingutf-8)) for sec in data[sections]: nums [q[no] for q in sec[questions]] if nums ! list(range(nums[0], nums[-1] 1)): print(题号不连续:, sec[title], nums) for script in data[listening_script]: for qno in script[questions]: if qno len(data[sections][0][questions]): print(听力稿题号超出范围:, script[text_no], qno)题号不连续的问题特别常见因为原卷里可能有「根据对话判断正误」这种题型题干不带数字而是带 T/F正则一匹配就会漏题。遇到这种题要在 3.1 的状态机里加一条规则题干以T或F结尾也属于有效题干。4. 批量生成期末卷变体模板替换、抽题矩阵与完整性校验4.1 用一个 JSON 做模板而不是直接改 .doc很多老师要的其实不是同一份卷子的解析而是「把这套题的听力换掉」「把完形填空换成去年的」。直接改 Word 文档很痛苦因为替换文本会破坏格式。更好的做法是把第 3 步生成的 JSON 当作模板在数据层面做替换最后再一次性渲染成新的 Word。所谓模板替换核心是定位要替换的题号和文本。常见做法是给 JSON 里的每个 section 和 question 增加一个tag字段比如listening_01、cloze_02。替换时按 tag 操作而不是按全文匹配。这样既能精确替换也能在替换后检查是不是所有题目都还指向合法选项。def replace_question(paper, tag, new_stemNone, new_optionsNone): for sec in paper[sections]: for q in sec[questions]: if q.get(tag) tag: if new_stem: q[stem] new_stem if new_options: q[options] new_options return True return False这个函数只做一件事按 tag 找到题目修改题干或选项不改动其他内容。返回 False 表示 tag 不存在调用方可以立刻报错避免静默跳过。此时你已经把「编辑试卷」变成了「改 JSON 字段」不再依赖 Word 里的查找替换对话框。4.2 抽题矩阵按题型抽原题而不是随机全抽随机抽题不能抽到一半发现听力稿对不上。所以要先定义抽题矩阵。矩阵的每一行是一个题型列是「原题范围」「抽取数量」「是否依赖听力稿」这三个字段。构建方式就是查 JSON 的 sections 和 listening_scriptmatrix [] for sec in paper[sections]: count len(sec[questions]) matrix.append({ section: sec[title], range: [q[no] for q in sec[questions]], need_script: listening in sec[title], pick: count # 默认全抽变量替换时改这个值 })抽题时用切片模拟「从题库里选一个子集」子集的顺序可以打乱但必须保证题号在听力稿的questions引用范围里。import random def build_paper(paper, matrix): new_paper {title: paper[title], sections: [], listening_script: []} offset 0 for row in matrix: qs row[range] picked random.sample(qs, row[pick]) if row[pick] len(qs) else qs[:] new_section { title: row[section], questions: [q for q in paper_sections(row[section]) if q[no] in picked] } new_paper[sections].append(new_section) # 重排题号 for idx, q in enumerate(new_section[questions], startoffset 1): q[no] idx offset len(new_section[questions]) return new_paper重排题号这一步极其重要。听力稿里记录的还是原题号如果把题号改了听力稿和题目就断了。所以在重排题号的同时需要同步更新听力稿的questions字段for script in origin_script_list: script[questions] [mapping.get(x, x) for x in script[questions]]这里的mapping是原题号到新题号的字典在build_paper的循环里一边重排一边填充。4.3 生成后的完整性校验要校验三张表生成新卷子后不能只看题目数量对不对。要同时校验三张表的一致性题目清单、听力稿题号引用、答案表。每张表都有一个「合法性条件」写成一个函数返回 booldef validate(paper): errors [] all_questions [] for sec in paper[sections]: all_questions.extend(sec[questions]) nums [q[no] for q in all_questions] if nums ! list(range(1, len(nums) 1)): errors.append(题号未连续递增) script_refs [x for s in paper[listening_script] for x in s[questions]] for ref in script_refs: if ref not in nums: errors.append(f听力稿引用不存在的题号: {ref}) answer_keys [q[answer] for q in all_questions if q.get(answer)] if len(answer_keys) ! len(all_questions): errors.append(存在缺答案的题目) return errors这里的validate不输出成功信息只报告 errors。调用方在生成 .doc 之前先跑一次任何错误都直接终止生成。对于需要批量产出多个班级试卷的场景这个校验函数是整个流水线的安全阀没有它一份错题的卷子会同时发给两个班。5. 从 JSON 回到 .doc用逆向生成保住听力稿的段落边界5.1 用 python-docx 重建试卷听力稿逐段写入解析和生成是对称的。解析时按段落读生成时也按段落写。用 python-docx 创建新文档按 JSON 的 sections 顺序写入标题、题干、选项和听力稿。听力稿生成时要注意保留段落边界一段录音一个段落不要合并成一个长字符串。from docx import Document from docx.shared import Pt doc Document() style doc.styles[Normal] style.font.name Times New Roman style.font.size Pt(12) doc.add_heading(paper[title], level0) for sec in paper[sections]: doc.add_heading(sec[title], level1) for q in sec[questions]: p doc.add_paragraph() run p.add_run(f{q[no]}. {q[stem]}) run.bold False for opt in q[options]: doc.add_paragraph(opt) doc.add_page_break() doc.add_heading(听力原文, level1) for script in paper[listening_script]: doc.add_paragraph(fText {script[text_no]}) for line in script[content]: doc.add_paragraph(line) doc.save(new_paper.docx)这段代码里doc.add_heading(听力原文, level1)会生成蓝色标题如果不想保留 Word 默认的 Heading 样式颜色可以把 level 改成 0 并手动设置字号。另外style.font.name只对西文字体生效中文需要额外设置qn(w:eastAsia)否则中文字符会默认用宋体跟原文不一致。这里我保留默认宋体因为 12 磅的宋体在大多数场景下是符合试卷习惯的。5.2 快速校对把题干和听力稿放到同一屏验证对应关系生成之后经常要人工确认听力稿和题目的说话人是否对得上比如原文中的人物是 Lucy 还是 Lily和题目选项是否一致。用代码生成一个对照文本每一条听力稿后面跟着对应的题号和选项快速扫一遍即可发现问题。for script in paper[listening_script]: print(f--- Text {script[text_no]} ---) print( .join(script[content])) for qno in script[questions]: q find_question(paper, qno) print(f [{qno}] {q[stem]}) for opt in q[options]: print(f {opt})find_question按题号从 sections 里找到对应题目这个对照表的输出格式可以直接粘贴到发给老师的校对文档里。很多复现问题都出在这里听力稿里是「W: Hello, may I speak to Mrs. Green?」题干问的是「What is the woman doing?」人工扫一遍能立刻发现指代不一致但纯靠文本比对很难自动化识别因为涉及语义。所以这个环节保留人工但把人工需要的信息压缩到一屏以内。5.3 让 Word 帮你检查开启拼写与语法检查再输出最终版最后一个技巧是借 Word 自己的校对能力。python-docx 生成的文档默认不会自动开启拼写检查但你可以在 XML 里写入设置打开文档后 Word 会对英文内容做拼写检查波浪线会标出可疑单词。from docx.oxml.ns import qn settings doc.settings.element proof settings.find(qn(w:proofState)) if proof is None: proof settings.makeelement(qn(w:proofState), {}) settings.append(proof) proof.set(qn(w:spelling), true) proof.set(qn(w:grammar), true)这段代码在文档的 settings.xml 里添加proofState节点属性设置为开启状态。保存后打开 Word系统会对全文跑一遍拼写检查听力稿里的人名和时间表达错误会直接显示红波浪线。对于一份要发给全年级的期末卷多这一道保险比事后发现人名拼错再撤回要划算得多。生成.docx后如果学校教务处要求的是.doc用 LibreOffice 再转一次即可soffice --headless --convert-to doc --outdir ./final new_paper.docx。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价