☰
Python解析.doc练习题:从Word文档到结构化题库的完整方案
2026/10/21 8:21:47 网站建设 项目流程

简介:这份《计算机应用基础》练习题带参考答案文档,面向正在备考计算机等级考试、修读计算机基础课程或需要系统梳理办公软件知识点的学生与自学者,帮助其通过判断题、单项选择、多项选择等题型巩固Excel、Word、PowerPoint、Internet及HTML语言等核心内容。资源包内含1个doc文件,整体约100KB,轻量易存,可直接打印或在线查阅,适合课堂练习、考前自测与知识点查漏补缺。文档共收录40道题目并附参考答案,覆盖Excel工具栏与函数输入、Windows快捷方式与画图操作、Word自动更正与替换、PowerPoint视图与文本框、Internet中TCP/IP协议与HTML代码结构等高频考点,题目表述贴近真实考试风格。目前已有273人学习下载,可作为计算机应用基础复习阶段的配套练习材料,帮助读者快速定位薄弱环节并对照答案理解原理。

1. 一份 .doc 练习题,为什么值得当成一个技术项目来做

《计算机应用基础》练习题带参考答案.doc 这个标题,乍看像是一份普通的课程资料,但在实际工作中,它代表了一类非常典型的文档处理需求:把一份带答案的 Word 练习题,变成可检索、可批改、可批量分发的结构化数据。很多做教务系统、在线题库、培训平台的朋友都遇到过这个场景——手里拿到一份几十页的 .doc 文件,里面是选择题、填空题、判断题混排,答案有的在题后括号里,有的统一放在文末。手工整理?一百道题能对到眼花。直接扔给程序解析?格式稍微一变就翻车。

这个方向适合三类人:一是需要把纸质或 Word 题库数字化的教务/培训从业者;二是想练手文档解析的 Python 初学者,因为 .doc 格式比 .docx 更“原始”,能逼着你理解二进制文档结构;三是需要批量生成练习卷的一线讲师,把答案抽出来之后,可以随机组卷、生成不带答案的版本。核心要解决的问题就一个:怎么把 .doc 里的题目和答案稳定地拆出来,并且保留可校验的对应关系。下面我从格式选型开始,一步步拆到能跑通的代码和参数。

2. 先搞清楚 .doc 和 .docx 的解析路线差异

2.1 为什么不能直接用 python-docx 读 .doc

python-docx 这个库名字很直白,但它只认 .docx,也就是 Office 2007 之后基于 XML 和 ZIP 的格式。你拿一个真正的 .doc 文件(OLE 复合文档,二进制结构)丢给它,它会直接报PackageNotFoundError,因为它找不到 ZIP 的魔数PK。这是最常见的第一个坑:很多人看到“Word 文档”就默认用 python-docx,结果在 .doc 上卡住。

那怎么判断手里的是哪种?看文件头就行。用 Python 读前 8 个字节:

# 判断 Word 文档的真实格式 def detect_word_format(filepath): with open(filepath, 'rb') as f: header = f.read(8) # .docx 是 ZIP 格式,开头为 PK\x03\x04 if header[:4] == b'PK\x03\x04': return 'docx' # .doc 是 OLE 复合文档,开头为 D0 CF 11 E0 if header[:4] == b'\xd0\xcf\x11\xe0': return 'doc' return 'unknown' print(detect_word_format('练习题.doc')) # 输出 doc print(detect_word_format('练习题.docx')) # 输出 docx

这段逻辑说明很简单:文件头是格式的“指纹”,比扩展名可靠。参数上没什么可调的,读 8 个字节足够。如果你拿到的是 .doc,有两条路:一是用 LibreOffice 的无头模式转成 .docx 再解析,二是用 antiword 或 textract 这类工具直接抽文本。我一般会先转格式,因为后续处理统一在 .docx 上做,工具链更成熟。

2.2 转格式的两种落地方式与参数

第一种,命令行调用 LibreOffice。Linux 上装好 libreoffice 之后:

# 无头模式把 .doc 转成 .docx,输出到 out 目录 libreoffice --headless --convert-to docx --outdir ./out ./练习题.doc

关键参数是--headless,不加会尝试弹 GUI 导致卡住;--convert-to docx指定目标格式;--outdir指定输出目录。转换完成后,out 目录下会出现同名的 .docx。这个方式稳定,但依赖本机装了 LibreOffice,批量处理时建议加个超时控制。

第二种,纯 Python 方案,用subprocess包一层,方便在脚本里循环处理多个文件:

import subprocess import os def convert_doc_to_docx(doc_path, out_dir): os.makedirs(out_dir, exist_ok=True) cmd = [ 'libreoffice', '--headless', '--convert-to', 'docx', '--outdir', out_dir, doc_path ] # 超时 60 秒,避免个别文件卡死整个批处理 result = subprocess.run(cmd, capture_output=True, timeout=60) if result.returncode != 0: raise RuntimeError(f"转换失败: {result.stderr.decode()}") # 返回生成的 docx 路径 base = os.path.splitext(os.path.basename(doc_path))[0] return os.path.join(out_dir, base + '.docx')

逻辑上就是拼命令、跑子进程、检查返回码。参数timeout=60是血泪经验,遇到过损坏的 .doc 让 LibreOffice 挂起,没有超时整个队列就堵死了。转换完之后,统一用 python-docx 读段落,进入下一步。

3. 从段落里拆题目和答案:正则与状态机

3.1 先摸清题目的排版规律

拿到 .docx 之后,第一件事不是写代码,是打开文件看几十行。常见的《计算机应用基础》练习题排版有这么几种:选择题题干以“1.”“2.”或“1、”开头,选项是 A/B/C/D 各占一行或挤在一行;答案有的紧跟在题干后的括号里,比如“( )”,有的在题末写“答案:A”,还有的整份答案统一放在文档最后,形如“1. A 2. B 3. C”。

我一般会先写个探针脚本,把前 30 个非空段落打印出来,人工确认模式:

from docx import Document doc = Document('练习题.docx') count = 0 for para in doc.paragraphs: text = para.text.strip() if text: print(repr(text)) count += 1 if count >= 30: break

用repr是为了看清有没有不可见字符,比如全角空格、制表符。这一步花五分钟,能省后面两小时的调试。确认模式之后,再决定用正则还是状态机。

3.2 用正则提取题干、选项和行内答案

假设排版是“1. 题干( )”后面跟“A. xxx”“B. xxx”这种。可以按行处理,用正则匹配题号开头:

import re # 匹配以 数字+点/顿号 开头的题干行 question_pattern = re.compile(r'^(\d+)[\.、]\s*(.+)') # 匹配选项行,如 A. xxx 或 A、xxx option_pattern = re.compile(r'^([A-D])[\.、]\s*(.+)') # 匹配行内答案,如 (A) 或 (A) inline_answer_pattern = re.compile(r'[((]\s*([A-D])\s*[))]') questions = [] current = None for para in doc.paragraphs: text = para.text.strip() if not text: continue q_match = question_pattern.match(text) if q_match: # 遇到新题号,先把上一题存起来 if current: questions.append(current) current = { 'num': int(q_match.group(1)), 'stem': q_match.group(2), 'options': [], 'answer': None } # 检查题干里有没有行内答案 ans = inline_answer_pattern.search(text) if ans: current['answer'] = ans.group(1) continue o_match = option_pattern.match(text) if o_match and current: current['options'].append({ 'label': o_match.group(1), 'text': o_match.group(2) }) continue if current: questions.append(current) print(f"共提取 {len(questions)} 题")

这段代码的逻辑是逐段扫描,遇到题号就开一个新字典,遇到选项就追加,遇到行内答案就填 answer 字段。参数上,question_pattern里的[\.、]覆盖了英文点和中文顿号两种写法,\s*容忍题号和文字之间的空格。注意inline_answer_pattern用了全角和半角两套括号,因为 Word 里经常混用。跑完之后,questions就是一个结构化的列表,可以直接转 JSON 或入库。

3.3 答案在文末时,用状态机切分答案区

如果答案统一放在文档最后,前面的正则只能拿到题干和选项,answer 全是 None。这时候需要先定位答案区的起始位置。常见标志是“参考答案”“答案”单独成段。做法是先把所有段落读成列表,找到标志段的下标,然后对答案区单独解析:

def split_answers(paragraphs): # 找到答案区起始下标 answer_start = None for i, p in enumerate(paragraphs): if re.match(r'^参考答案|^答案', p.strip()): answer_start = i break if answer_start is None: return paragraphs, [] body = paragraphs[:answer_start] answer_block = paragraphs[answer_start:] # 答案区常见格式:1. A 2. B 或 1、A answers = {} for line in answer_block: for m in re.finditer(r'(\d+)[\.、]\s*([A-D])', line): answers[int(m.group(1))] = m.group(2) return body, answers

逻辑说明:先找标志段,把文档切成正文和答案两块;答案区用finditer而不是match,因为一行里可能挤了多个“题号+答案”。参数上,[A-D]只覆盖选择题,如果还有判断题,把范围改成[A-D对错√×]即可。拿到 answers 字典后,回填到前面提取的 questions 里,按 num 对应。

4. 避坑与排查:五个真实翻车点

4.1 现象:转换后段落全挤在一起,题号识别不到

原因:原 .doc 里用软回车(Shift+Enter)换行,转成 .docx 后这些换行不是独立段落,python-docx 的paragraphs读不到。解决:改用doc.element.body.iter()遍历所有文本节点,或者先把软回车替换成硬回车再转。我一般是在 LibreOffice 转换前,用宏或手动替换,但批量场景下更稳的是解析时同时读paragraph.text和run.text,把 run 拼起来再按换行符切。

4.2 现象:正则匹配到了页码或页眉里的数字

原因:Word 的页眉页脚也会被 python-docx 读到,里面的“第 1 页”会被^(\d+)[\.、]误判成题号。解决:只遍历doc.paragraphs不够,要过滤掉页眉页脚。python-docx 里页眉页脚不在paragraphs里,但如果你用了doc.element.body就会带进来。稳妥做法是加一个题号连续性校验:如果新题号不是上一个题号加一,就跳过。

4.3 现象:选项里的“A.”被当成新题号

原因:有些题干里本身包含“A.”,比如“以下关于 A. 的说法”。解决:选项正则只在当前题目上下文里生效,且要求选项字母按 A、B、C、D 顺序出现。如果遇到不按顺序的,记录警告人工复核,不要硬塞。

4.4 现象:答案区里“1. A”和“1、A”混用导致漏提

原因:正则只写了一种分隔符。解决:分隔符用字符集[\.、.],把全角句点也加进去。另外答案之间可能用空格或换行分隔,finditer比按行切更鲁棒。

4.5 现象:提取出的题目数量对不上,少了十几道

原因:有些题号是“(1)”或“一、”这种非阿拉伯数字开头。解决:先统计文档里所有可能的题号模式,再决定正则。如果确实有中文数字题号,加一个映射表把“一”到“十”转成数字,或者直接放弃题号、按题干特征切分。

5. 把结果用起来:导出 JSON、生成无答案卷与校验

5.1 导出结构化 JSON 并做完整性校验

提取完只是第一步,要能验证结果对不对。我一般会导出 JSON,然后写个校验函数:检查题号是否连续、每题是否有至少两个选项、选择题是否都有答案。

import json def validate_questions(questions): errors = [] nums = [q['num'] for q in questions] # 检查题号连续性 for i in range(1, len(nums)): if nums[i] != nums[i-1] + 1: errors.append(f"题号不连续: {nums[i-1]} -> {nums[i]}") for q in questions: if len(q['options']) < 2: errors.append(f"第 {q['num']} 题选项不足") if q['answer'] is None: errors.append(f"第 {q['num']} 题缺少答案") return errors with open('questions.json', 'w', encoding='utf-8') as f: json.dump(questions, f, ensure_ascii=False, indent=2) errs = validate_questions(questions) for e in errs: print(e)

逻辑上就是遍历检查,把问题打印出来人工修。参数ensure_ascii=False保证中文正常显示,indent=2方便肉眼核对。校验通过之后,这份 JSON 就可以直接喂给题库系统。

5.2 生成不带答案的练习卷

有了结构化数据,生成无答案版本就是过滤字段的事。用 python-docx 反向写一个 .docx:

from docx import Document def export_practice(questions, output_path): doc = Document() for q in questions: doc.add_paragraph(f"{q['num']}. {q['stem']}") for opt in q['options']: doc.add_paragraph(f"{opt['label']}. {opt['text']}") doc.add_paragraph("") # 空行分隔 doc.save(output_path) export_practice(questions, '练习卷_无答案.docx')

这段代码把题干和选项重新写进新文档,不写 answer 字段。参数上没什么特别的,注意add_paragraph("")用来留空行,不然题目会挤在一起。生成之后可以再转 PDF 分发。

5.3 一个容易被忽略的校验技巧

最后分享一个我踩过坑之后养成的习惯:随机抽 5 道题,把提取出的题干和选项跟原文档肉眼比对。自动化校验只能查结构,查不了语义。有一次正则把“以下哪个不是”的“不是”吃掉了,结构完全正常,但题目意思反了。所以每次跑完,我都会抽几道题看一眼。这个习惯帮我省过好几次返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询