Python解析带答案.doc练习题:从非结构化文档到结构化JSON

发布时间:2026/10/21 8:21:49
Python解析带答案.doc练习题:从非结构化文档到结构化JSON
简介这份《计算机应用基础》练习题带参考答案文档面向正在备考计算机等级考试、修读计算机基础课程或需要系统梳理办公软件知识点的学生与自学者帮助其通过判断题、单项选择、多项选择等题型巩固Excel、Word、PowerPoint、Internet及HTML语言等核心内容。资源包内含1个doc文件整体约100KB轻量易存可直接打印或在线查阅适合课堂练习、考前自测与知识点查漏补缺。文档共收录40道题目并附参考答案覆盖Excel工具栏与函数输入、Windows快捷方式与画图操作、Word替换与分页符、PowerPoint视图与文本框、TCP/IP协议与HTML代码结构等高频考点题目表述贴近真实考试风格。目前已有273人学习下载可作为计算机应用基础复习阶段的配套练习材料帮助读者快速定位薄弱环节并对照答案理解易错概念。1. 一份 .doc 练习题为什么值得当成一个技术项目来做《计算机应用基础》练习题带参考答案.doc 这个标题乍看像是一份普通的课程资料但如果你把它当成一个「文档工程」问题来看它其实是一个典型的非结构化文档处理场景一份 Word 文档里混排了题干、选项、答案、解析格式不统一章节层级靠人工排版维持答案和题目之间没有稳定的分隔标记。很多老师、教辅编辑、企业培训岗的从业者每年都要重复处理几十份这样的文档手工拆分、校对、重新排版耗时且容易出错。这个标题真正指向的需求不是「下载一份题」而是「如何把一份带答案的 .doc 练习题变成可维护、可检索、可批量生成的结构化数据」。适合三类人需要批量整理题库的教辅从业者、想把纸质/Word 资料数字化的培训岗、以及想练手文档解析的开发者。下面按「先看清文档结构 → 再拆解字段 → 再落地脚本 → 再避坑」的顺序讲透。2. 先拆文档结构题干、选项、答案到底怎么分2.1 用 python-docx 读出段落先看清「脏数据」长什么样不要一上来就写正则。第一步是把 .doc 转成 .docx老 .doc 格式 python-docx 读不了常见做法是先用 LibreOffice 无头模式转一遍然后用 python-docx 把每个段落的文本和样式打印出来人工扫一遍。这一步的目的是确认三件事题干编号是「1.」还是「1、」还是「第1题」选项是「A.」还是「A、」还是「A」答案区是集中放在文末还是紧跟在每题后面。# 先用 LibreOffice 把 .doc 转成 .docx命令行不依赖 Office # soffice --headless --convert-to docx 练习题.doc --outdir ./converted from docx import Document doc Document(./converted/练习题.docx) for i, para in enumerate(doc.paragraphs): text para.text.strip() if not text: continue # 打印段落索引、样式名、前 60 个字符方便人工判断结构 print(i, |, para.style.name, |, text[:60])逻辑说明para.style.name是关键线索。很多文档的题干用了「标题 2」或自定义样式选项用了「正文」答案用了「正文缩进」。参数上text[:60]截断是为了在终端里一屏能看多行实际排查时可以改成完整输出。如果发现大量段落样式都是「Normal」说明排版是纯手工的只能靠文本模式识别。2.2 三种常见排版模式决定你后面用哪种解析策略扫完段落之后你大概率会落到下面三种模式之一。选错策略后面全是返工。模式特征推荐策略风险答案集中式题目连续排列文末统一「参考答案」先按答案区切分再回填答案编号与题号错位答案紧跟式每题下方直接跟「答案X」按题号切块块内提取解析文字里混入「答案」二字表格混排式题干在段落选项在表格段落表格双通道读取表格跨页导致顺序错乱我一般会先统计「答案」这个词出现的段落位置如果全部集中在后 20%就是集中式如果均匀分布就是紧跟式。这个判断用几行代码就能做比拍脑袋可靠。# 统计「答案」出现的位置分布判断排版模式 answer_positions [] total len(doc.paragraphs) for i, para in enumerate(doc.paragraphs): if 答案 in para.text: answer_positions.append(i) if answer_positions: ratio sum(answer_positions) / len(answer_positions) / total print(f答案平均位置比例: {ratio:.2f}) # ratio 0.7 偏集中式 0.5 偏紧跟式参数说明ratio只是辅助判断不是绝对标准。有些文档前半部分是判断题、后半部分是选择题答案位置会偏中间这时候要结合章节标题再切一刀。2.3 把「题号」当成锚点而不是把「答案」当成锚点血泪经验很多人第一反应是用「答案」做切分锚点结果遇到「以下答案正确的是」这种题干里的干扰词直接翻车。更稳的做法是用题号做锚点。题号的正则要写得宽一点覆盖1.1、1(1)第1题这几种写法同时要求题号出现在段落开头避免匹配到正文里的数字。import re # 题号锚点行首数字 1-3 位后面跟 . 、 ) 或「题」 QUESTION_PATTERN re.compile(r^\s*(\d{1,3})\s*[\.、\)]|^\s*第\s*(\d{1,3})\s*题) def is_question_start(text): return bool(QUESTION_PATTERN.match(text)) # 验证打印所有被判定为题号开头的段落 for i, para in enumerate(doc.paragraphs): if is_question_start(para.text.strip()): print(i, para.text.strip()[:50])逻辑说明正则里用了两个分支第一个匹配「数字分隔符」第二个匹配「第X题」。^\s*允许行首有空格因为 Word 里经常有首行缩进产生的空白。参数上\d{1,3}限制题号最多三位防止把「2024年」这种年份误判成题号。如果文档题号超过 999改成\d{1,4}但误判率会上升需要再加一层「后面必须跟选项或问号」的校验。3. 字段拆解把一道题拆成题干、选项、答案、解析四个字段3.1 选项识别的边界A 到 F别把「A 选项」当成选项选项识别的坑在于题干里可能出现「A 选项的说法正确」这种句子。判断标准是选项标记必须出现在段落开头且后面紧跟选项内容通常长度较短。我一般用「行首 单个大写字母 分隔符」来匹配同时限制选项字母范围在 A 到 F。OPTION_PATTERN re.compile(r^\s*([A-F])\s*[\.、\)]\s*(.)) def parse_options(paragraphs): options {} for para in paragraphs: m OPTION_PATTERN.match(para.strip()) if m: letter, content m.group(1), m.group(2).strip() options[letter] content return options逻辑说明[A-F]覆盖大多数选择题的选项范围如果遇到多选或判断可以扩展到[A-H]。(.)要求选项内容非空避免把孤立的「A.」当成选项。参数上如果文档选项用的是「A」这种全角括号正则里的[\.、\)]已经覆盖了全角右括号但左括号在行首的情况需要额外处理可以在匹配前先strip掉左括号。3.2 答案提取集中式和紧跟式要写两套逻辑紧跟式的答案提取相对简单在题目块内搜「答案」关键词即可。集中式则要先定位答案区再按题号回填。这里的关键是答案区的题号格式可能和题目区不一致比如题目区是「1.」答案区是「1、」所以回填时要用题号数字做 key而不是用原始字符串。def extract_answer_inline(block_text): # 紧跟式在题目块内找「答案X」或「答案 X」 m re.search(r答案[:]\s*([A-F]), block_text) if m: return m.group(1) return None def build_answer_map(answer_section_paragraphs): # 集中式答案区每段形如「1. A 2. B 3. C」 answer_map {} for para in answer_section_paragraphs: # 找出所有「数字答案字母」的组合 pairs re.findall(r(\d{1,3})\s*[\.、\)]?\s*([A-F]), para) for num, ans in pairs: answer_map[int(num)] ans return answer_map逻辑说明extract_answer_inline里的[A-F]支持多选答案如「ABD」。build_answer_map用re.findall一次性提取一段里的多组答案适合答案区把多题答案挤在一行的情况。参数上\d{1,3}和题号正则保持一致避免题号位数不一致导致匹配失败。如果答案区用的是「1-5 ABCDE」这种区间写法需要额外写一个区间展开函数这是集中式里最容易漏的分支。3.3 解析字段有就留没有就空着别硬凑不是所有练习题都带解析。如果文档里解析紧跟在答案后面通常以「解析」「【解析】」开头。提取时要注意解析可能跨多段不能只取一行。我一般用「从解析标记开始直到下一个题号锚点或答案标记为止」来截取。def extract_explanation(block_paragraphs): explanation [] started False for para in block_paragraphs: text para.strip() if re.match(r^[【\[]?解析[】\]]?[:]?, text): started True explanation.append(re.sub(r^[【\[]?解析[】\]]?[:]?, , text)) elif started: # 遇到下一个题号或答案标记就停止 if is_question_start(text) or re.match(r^答案[:], text): break explanation.append(text) return \n.join(explanation).strip()逻辑说明started标志位控制解析的起止避免把解析后面的下一题内容吞进来。参数上re.sub把解析标记本身去掉只留正文。如果解析里本身包含「解析」二字比如「解析解析法」这个正则不会误伤因为它要求「解析」出现在行首且后面跟冒号或括号。4. 落地脚本从 .doc 到结构化 JSON 的完整流程4.1 主流程转换、读取、切块、提取、导出把前面几节的逻辑串起来主流程分五步。每一步都要有中间产物落盘方便出错时回看而不是一口气跑完再 debug。import json from docx import Document def parse_practice_doc(docx_path, output_json): doc Document(docx_path) paragraphs [p.text.strip() for p in doc.paragraphs if p.text.strip()] # 第一步找到答案区起点集中式 answer_start None for i, text in enumerate(paragraphs): if re.match(r^参考答案|^答案部分|^标准答案, text): answer_start i break if answer_start is not None: question_paras paragraphs[:answer_start] answer_paras paragraphs[answer_start:] answer_map build_answer_map(answer_paras) else: question_paras paragraphs answer_map {} # 第二步按题号切块 blocks [] current [] for text in question_paras: if is_question_start(text) and current: blocks.append(current) current [text] else: current.append(text) if current: blocks.append(current) # 第三步逐块提取字段 questions [] for block in blocks: q_text block[0] q_num_match QUESTION_PATTERN.match(q_text) q_num int(q_num_match.group(1) or q_num_match.group(2)) options parse_options(block[1:]) inline_ans extract_answer_inline(\n.join(block)) final_ans inline_ans or answer_map.get(q_num, ) explanation extract_explanation(block) questions.append({ number: q_num, stem: q_text, options: options, answer: final_ans, explanation: explanation }) # 第四步导出 JSON with open(output_json, w, encodingutf-8) as f: json.dump(questions, f, ensure_asciiFalse, indent2) return questions # 调用 result parse_practice_doc(./converted/练习题.docx, ./output/questions.json) print(f共解析 {len(result)} 题)逻辑说明answer_start用三个常见答案区标题做匹配覆盖大多数文档。blocks的切块逻辑保证每块以题号开头。final_ans优先用紧跟式答案没有才回填集中式答案这样兼容两种模式。参数上ensure_asciiFalse保证中文正常写入indent2方便人工检查。如果文档题量超过 500建议把 JSON 换成 JSONL每行一题方便流式处理。4.2 校验环节题号连续性、答案覆盖率、选项完整性解析完不校验等于没解析。三个校验指标题号是否从 1 连续到 N有答案的题目占比是否合理低于 80% 说明答案提取有问题选择题的选项数是否都在 2 到 6 之间。def validate_questions(questions): issues [] numbers [q[number] for q in questions] # 校验题号连续性 expected set(range(1, max(numbers) 1)) missing expected - set(numbers) if missing: issues.append(f缺失题号: {sorted(missing)}) # 校验答案覆盖率 answered sum(1 for q in questions if q[answer]) if answered / len(questions) 0.8: issues.append(f答案覆盖率偏低: {answered}/{len(questions)}) # 校验选项完整性 for q in questions: if q[options] and not (2 len(q[options]) 6): issues.append(f第{q[number]}题选项数异常: {len(q[options])}) return issues issues validate_questions(result) for issue in issues: print(⚠, issue)逻辑说明missing用集合差集找缺失题号比逐个遍历快。答案覆盖率阈值 0.8 是经验值判断题可能没有选项但有答案所以选项校验只对options非空的题生效。参数上如果文档本身题号就不连续比如按章节重新编号这个校验会误报需要先按章节分组再校验。4.3 导出可读格式Markdown 和 CSV 各有用处JSON 适合程序消费但人工校对时 Markdown 更直观导入题库系统时 CSV 更通用。我一般三个格式都出JSON 做主存储Markdown 做校对稿CSV 做导入中间件。def export_markdown(questions, md_path): lines [] for q in questions: lines.append(f**{q[number]}. {q[stem]}**\n) for letter, content in sorted(q[options].items()): lines.append(f- {letter}. {content}) lines.append(f\n答案{q[answer]}\n) if q[explanation]: lines.append(f解析{q[explanation]}\n) lines.append(---\n) with open(md_path, w, encodingutf-8) as f: f.write(\n.join(lines)) export_markdown(result, ./output/questions.md)逻辑说明sorted(q[options].items())保证选项按字母顺序输出避免字典无序导致校对困难。参数上---分隔线方便在 Markdown 预览里快速定位题目边界。如果题目里本身包含---需要换成更长的分隔符或加转义。5. 避坑与排查解析 .doc 练习题时最容易翻车的 5 个点5.1 现象题号匹配到了年份或分数题目被切碎原因正则^\s*\d{1,3}太宽把「2024 年考试重点」这种句子也当成题号。解决在题号正则后面加一个「必须跟选项或问号或括号」的约束或者先排除包含「年」「分」「%」的段落。def is_question_start_strict(text): if not QUESTION_PATTERN.match(text): return False # 排除年份、分数等干扰 if re.search(r\d{4}\s*年|\d\s*分|%, text[:10]): return False return True5.2 现象答案回填错位第 5 题的答案跑到第 6 题原因集中式答案区里答案编号和题目编号的起始值不一致比如题目从 0 开始答案从 1 开始。解决回填前先比对题目最小题号和答案最小题号如果差 1整体偏移。5.3 现象选项被截断长选项只取到第一行原因Word 里长选项可能跨多个段落parse_options只取了匹配到的第一段。解决在切块阶段就把属于同一选项的后续段落合并判断依据是「下一段不以选项字母开头且不以题号开头」。5.4 现象解析内容混入了下一题的题干原因extract_explanation的停止条件只检查了题号和答案标记没检查选项标记。解决在停止条件里加上OPTION_PATTERN.match(text)遇到选项就停。5.5 现象.doc 转 .docx 后表格内容丢失原因LibreOffice 转换时某些文本框或嵌入对象里的表格不会被 python-docx 的paragraphs读到。解决改用doc.tables单独读表格按表格在文档中的顺序和段落做交叉比对。这一步没有银弹只能人工确认表格位置。6. 进阶把解析结果接进自动组卷和错题本解析出结构化 JSON 只是第一步。真正省时间的用法是用题号、题型、知识点三个维度建索引然后按规则抽题生成新卷子。我一般会在 JSON 里补一个tags字段解析时留空后续人工或半自动打标。组卷脚本按tags过滤按题号随机抽样输出一份新的 Markdown 或 docx。import random def generate_paper(questions, tag_filterNone, count20): pool questions if tag_filter: pool [q for q in questions if tag_filter in q.get(tags, [])] if len(pool) count: count len(pool) selected random.sample(pool, count) # 按题号排序保证卷面顺序 selected.sort(keylambda q: q[number]) return selected逻辑说明random.sample保证不重复抽题sort保证卷面题号递增。参数上count超过题库容量时自动降级为全选避免报错。如果要做错题本把selected换成「答案错误的题号列表」即可逻辑一样。验证解析质量的一个具体技巧随机抽 10 道题把原始 .doc 里的对应段落和解析出的 JSON 并排打印逐字比对题干和选项。这个动作我每次都会做因为正则的边界情况永远比想象的多。宁可多花十分钟人工抽检也不要等题库导入系统后才发现答案错位——那时候返工成本是现在的十倍。希望帮到你。本文还有配套的精品资源点击获取