拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从Word试卷到题库:Python提取数学试题与结构化分析实践

简介这套由吉林多所名校联合命制的2017年七年级下学期第一次月考数学卷适合刚进入下学期的学生用于阶段自测也适合教师用来评估教学进度、家长辅助家庭辅导。试卷依据课标和命题要求设计包含选择、填空、解答等多种题型在数与代数方面覆盖整数、分数、小数的运算及方程化简几何部分涉及线段、角度、三角形和四边形的性质与简单证明统计与概率板块包含图表绘制、平均数、中位数、众数计算及基础事件概率同时穿插图形平移、旋转、对称等变换问题并通过实际情境锻炼数学思考与估算能力。资源包内为1个doc文档大小2.29MB除完整试题外还配有逐题答案解析便于学生核对思路、教师分析典型错因目前已有110人学习浏览。无需注册即可免费下载既能作为考前模拟卷使用也可为校本命题或备课提供参考样例。1. 一份旧试卷比新题库更值得拆解的地方一份标题叫《吉林名校调研省命题2017年七年级下第一次月考数学试卷及答案精选.doc》的旧文档多数人只会拿它当过期教辅。但换个角度它是一份完整带答案与解析的数学试题数据集题目组织、难度排布、答案规范全在文件里。对做教育数据、题库清洗、学情分析甚至智能组卷的人来说“省命题”卷的真实度远高于网络随机拼题适合用来验证解析脚本和知识点标注规则。文档覆盖七年级下学期第一次月考范围也就是相交线与平行线、实数、平面直角坐标系这些内容。结构基本可预测选择题、填空题、解答题题号连续。熟悉这套结构后从Word里把题目和答案拆出来写一次脚本后续同类试卷就能批量处理。学生刷题、老师做阶段评价、开发者拿它当高质量语料各取所需。2. 从Word文档逆推出试卷的知识点结构与题型骨架分析试卷之前先要看清它的文体层。省命题卷在排版上有很强惯性这既是便利也是约束。便利在于规则稳定约束在于个别学校会改动题号或插入附加题。先把这种结构拆出来再谈识别。2.1 为什么2017年“省命题”卷适合做结构分析省命题卷与市统考、学校随堂卷不同题目经过教研审核每道题对应的课标条目更清晰。而且这类卷子通常以免费文档形式传播没有加密层适合做文本提取。比起扫描版PDFWord文档天然带段落样式和表格题号、选项、答案的换行规律相对稳定给自动标注提供了可复现的基础。2017年的卷子已经足够老知识点与当前课标接近又不会太旧到出现大幅删改做历史题库时是很合适的种子语料。七年级下学期第一次月考的内容按当时进度一般在相交线与平行线、实数、平面直角坐标系等章节内出题。这套“精选”文档中答案和解析是齐全的。做自动分析时第一步不是让机器学习读题而是先观察文档的物理结构题号以“1”“2”或“一、选择题”这样的标题开头选项多夹在题干后或另起一段答案与解析则集中在文档后半部分。识别这些结构往往比直接做语义识别更可靠。题型常见文本位置适合提取的知识点选择题文档前半部分题干后接A/B/C/D相交线与平行线判定填空题选择题后空格处填数值实数与坐标系整数点解答题文档后半部分含“证明”等要求平行线性质、实数混合运算这张表描述的是该阶段常见卷的布局。实际拿到文档后我会用下面的方式验证布局假设统计“选择题”这个关键词出现的位置把文档前半段和后半段分开检索看答案区与题目区的分界点在哪一段。2.2 识别题型与答案的通用标注规则所有题型识别都基于文本模式。经典逻辑是先按换行拆分段落再根据段落首部匹配题号然后在段落内部按A. B. C. D.切出选项。对解答题则以“解答题”或“三、”为进入标记。下面这个例子只依赖标准库的re可以跑在离线环境正好适合处理这类Word导出的文本。import re def detect_question_block(text): # 将文本按照行切分去掉空行 lines [ln.strip() for ln in text.splitlines() if ln.strip()] questions [] current None for line in lines: # 匹配 1. 1 1、 三种题号写法 m re.match(r^(\d)[.、]\s*(.*), line) if m: if current: questions.append(current) current {qid: m.group(1), body: m.group(2), options: [], answer: None} else: # 匹配 A. B. C. D.也兼容 A opt re.match(r^([A-Da-d])[.、]\s*(.*), line) if opt and current is not None: current[options].append(f{opt.group(1)}: {opt.group(2)}) elif current is not None: current[body] line if current: questions.append(current) return questions逻辑说明函数把文本切成非空行后逐行做两件事匹配题号或者匹配选项。匹配到新题号时将前一道题推入结果列表。选项单独保存没有匹配到的普通行拼接到题干后面避免题干被换行截断。qid字段保留字符串类型方便后续排序options统一改成“A: 内容”的格式省去原始分隔符差异。参数说明正则^(\d)[.、]同时兼容英文句点、中文句号和顿号因为教辅文档里经常混用。[A-Da-d]只匹配单字母选项如果文档出现“(A)”这种带括号的正则要改成^[\(]?([A-Da-d])[\)]?[.、]。处理真实文件时我会先用chardet做字符集检测再决定以什么编码打开文本否则很容易被UnicodeDecodeError卡住。提示从Word里复制的文本段落经常带大量空白和制表符建议在切行前先用re.sub(r[ \t], , text)把连续空白收敛成单个空格降低干扰。这一层的本质不是自然语言理解而是把版面位置转化为键值结构。后面无论做题库还是学情分析都基于这一层结构化结果。另外需要注意七年级下第一次月考的图形题较多纯文本抽取只能看到“如图”这类占位词真正的图形需要单独处理这正好是第三章要解决的问题。3. 用python-docx把月考试卷拆成可检索的题目清单上一篇的结构化偏重“从文本到题号”但实际拿到的文档是.doc二进制格式直接用文本方式打开会看到乱码。所以要先解决格式兼容问题再用docx接口拆解段落与表格。3.1 先处理.doc的老格式兼容问题原始文件名是.docpython-docx只支持Office Open XML格式直接打开会抛异常。常见做法是先用LibreOffice或Word把文件转成.docx再从.docx抽取。如果机器上装了LibreOffice一条命令可以批量转换soffice --headless --convert-to docx --outdir ./converted ./input/JL2017.doc命令说明--headless要求不启动GUI--convert-to docx指定目标格式--outdir指定输出目录最后给输入文件路径。转换产生的排版差异基本不影响文本内容。如果环境是Windows且没有LibreOffice可以用COM调用Word的Document.SaveAs但那会逐个启动进程不适合批量跑我一般优先用LibreOffice。转换方式依赖优点缺点LibreOffice headlesssoffice跨平台可批量需要安装Word COMMS Office排版保真度高只能在Windows/macOS在线转换服务网络零安装不能批量且可能有扰如果转换后的docx里面找不到图片先确认原.doc是否包含OLE嵌入对象而不是普通图片。LibreOffice在转换时可能只是重新封装OLE这种情况需要先转PDF再从PDF截图像我会在3.3里给出参考做法。3.2 按段落和表格抽取题目与答案python-docx的接口很直接Document.paragraphs返回所有段落Document.tables返回所有表格。一套试卷的题干、选项、答案在文档里往往以多个连续段落出现答案和解析可能被写成表格放在文档末尾。下面的代码演示如何把段落文本和表格都捞出来。from docx import Document def extract_docx_structure(path): doc Document(path) body [] for p in doc.paragraphs: style p.style.name if p.style else # 全角空格统一成半角保留题干内换行 text p.text.strip().replace(\u3000, ) if text: body.append({style: style, text: text}) for i, table in enumerate(doc.tables): rows [] for row in table.rows: cells [c.text.strip().replace(\u3000, ) for c in row.cells] rows.append(cells) body.append({style: fTABLE_{i}, text: rows}) return body path converted/JL2017.docx struct extract_docx_structure(path) for item in struct[:10]: print(item)逻辑说明这里将段落和表格统一抽象成列表元素用style和text区分。段落的style能帮助识别“一、选择题”这类标题表格数据在答案匹配时很有用因为很多Word版的解析表会把题号、答案、分值放在同一行的不同单元格里。replace(\u3000, )是处理全角空格导致的切分问题先替换成半角后面统一清洗。参数说明doc.tables中的row.cells在遇到合并单元格时可能返回同一个对象多次去重需要通过单元格的_tcid或者干脆按文本值去重。答题区经常出现“B”这种带中文句号的选项清洗时要把全角句号转半角如果你只需要题目文本可以后面再做过滤但保留全部结构更保守。3.3 清洗脏数据题号、空格、图片占位Word转存后常见三种脏数据。第一种是题号后面跟制表符或全角空格导致split出错第二种是选择题选项被自动编号成数字而不是A、B、C、D第三种是几何题的图片在文本层只剩一个空行或“图”字。下面这两个函数分别处理文本归一化和图片抽取import re import os import zipfile def clean_question_text(raw: str) - str: # 全角空格转半角制表符变空格 s raw.replace(\u3000, ).replace(\t, ) # 连续空白压成一个空格便于正则匹配 s re.sub(r[ \t], , s) # 把题号后的中文句号统一成半角点 s re.sub(r^(\d)[.、], r\1., s) return s.strip() def extract_images_from_docx(path, out_dirimgs): os.makedirs(out_dir, exist_okTrue) with zipfile.ZipFile(path) as z: names [n for n in z.namelist() if n.startswith(word/media/) and not n.endswith(/)] for i, name in enumerate(names): data z.read(name) ext name.rsplit(., 1)[-1] if ext.lower() not in (png, jpg, jpeg, gif): ext img with open(os.path.join(out_dir, fimg_{i}.{ext}), wb) as f: f.write(data) return len(names)逻辑说明clean_question_text不改变题干只把标点和空白归一化例如全角空格转半角、题号后的全角句号转半角点。extract_images_from_docx利用docx就是zip包的特性从word/media目录里把图片全部解出按出现顺序编号。返回的图片数量可以用来和题目数量做对比如果某道几何题没有对应图片多半是原文档里用了文本框而不是嵌入图。参数说明zipfile.ZipFile可以直接处理.docx因为它的本质是zip压缩包。names列表按文档打包顺序返回通常对应插入先后但顺序并非绝对可靠严谨的做法是读取word/document.xml里的r:embed关联这里不做展开。过滤掉文本扩展名是为了防止把字体文件当图片导出。提示如果在转换后的docx里找不到图片优先检查原.doc是否为OLE嵌入格式。LibreOffice对OLE对象只是重新封装不会变成可解出的JPG这种情况建议改用soffice --convert-to pdf然后再用PDF渲染器把页面转成PNG代价是图片位置需要用坐标来对齐。最后在跑完整流程前建议先做一次统计段落总数、表格数、图片数以及“选择题”标记出现的次数。如果题号最多到20但选择题只有15道说明中间混入了填空题这时候要把detect_question_block和当前章节状态结合而不是单纯依赖题号连续。4. 把试题和答案解析改造成离线题库数据表在第3章拿到文档的段落和表格后接下来的任务是把这些松散结构变成统一的数据表。这样无论是做练习系统还是导成Excel脚本都只需要读一遍数据接口。4.1 字段设计从文本片段到结构化记录将每道题变成一个字典字段包括 id、题型、题干、选项、答案、解析、知识点、来源文件。答案和解析从文档末尾答案表提取而不是靠自然语言推理这样才能保留原卷权威性。字段表如下字段类型示例说明idstr2017JL_01来源卷 题号sectionstr选择题型分组stemstr下列图形中∠1与∠2是对顶角的是题干optionslist[A: ①, B: ②]选项列表answerstrB标准答案analysisstr对顶角的两边互为反向延长线解析knowledgestr相交线与平行线考点标签pageint2原卷页码可选字段设计的核心是保证id可追溯。2017JL_前缀能避免后续合并多个学校试卷时冲突。答案区和题目区分离的试卷必须先把题目读出来再拿题号去答案表里回填答案否则会出现题干和答案错位。4.2 用Python生成JSON题库下面这段代码把第3章的struct转成题库JSON并演示如何从表格行回填答案。它假设答案表的结构是“题号 | 答案 | 解析”三列这也是Word试卷最常见的组织方式。import json import re def guess_section(style_name: str) - str: if 选择 in style_name: return 选择 if 填空 in style_name: return 填空 return 解答 def build_question_bank(struct): bank [] cur None in_answer_section False for item in struct: text item.get(text, ) # 表格元素可能是答案表 if isinstance(text, list): for row in text: if row and len(row) 2 and str(row[0]).strip().isdigit(): bank attach_answer(bank, row) continue if 答案 in text and 解析 in text and 选择题 in text: in_answer_section True continue if in_answer_section: continue m re.match(r^(\d)\.\s*(.*), text) if m: if cur: bank.append(cur) cur { id: 2017JL_ m.group(1), stem: m.group(2).strip(), options: [], answer: , analysis: , knowledge: , section: guess_section(item.get(style, )) } else: opt re.match(r^([A-Da-d])[.、]\s*(.*), text) if opt and cur is not None: cur[options].append(f{opt.group(1).upper()}: {opt.group(2).strip()}) elif cur is not None: cur[stem] text.strip() if cur: bank.append(cur) return bank def attach_answer(bank, row): qid 2017JL_ str(row[0]).strip() for q in bank: if q[id] qid: q[answer] row[1].strip() if len(row) 2: q[analysis] row[2].strip() break return bank # 这里的 struct 来自第3章 extract_docx_structure(path) bank build_question_bank(struct) with open(jl2017_bank.json, w, encodingutf-8) as f: json.dump({source: 吉林名校调研(省命题)2017, questions: bank}, f, ensure_asciiFalse, indent2)逻辑说明build_question_bank在遇到表格时先尝试当答案表解析只有表头第一列是数字的行才回填答案。段落分支中先判断是否进入答案区再判断题号和选项普通行拼接到题干。这段代码与第3章的detect_question_block不同它直接操作包含style的struct因此能保留题型分节信息。参数说明guess_section的输入是Word样式名如果一个文档没有样式这里全部返回“解答”这时需要靠“一、选择题”这类文本标记来切分。json.dump中ensure_asciiFalse让中文以明文写入indent2便于阅读若生成结果较大可以把indent去掉并用JSONL格式。检查输出时重点看answer为空的记录数如果超过总题数的5%说明原始的答案表不是三列结构需要查看表格原始数据再改attach_answer。4.3 用关键词映射表回填知识点试卷解析中如果出现“对顶角”“同位角”等词可以直接用关键词映射到知识点标签。下面是一个适用于这份试卷的知识点映射表关键词知识点对顶角、邻补角、同位角、内错角相交线与平行线算术平方根、立方根、无理数实数坐标、象限、原点平面直角坐标系实现时遍历题干和解析命中一个关键词就写入知识字段多个都命中就合并成用逗号分隔的短语。这个做法比训练文本分类器快得多尤其是对这种学科术语稳定的试卷。要注意“平行”这个词在七年级数学里几乎每道几何题都会出现所以不能只匹配一个词至少用双词组合例如“平行线证明”再触发标签。提示用关键词映射回填后最好抽样10道题人工核验因为同一道题可能同时涉及“实数”和“坐标系”只保留单个标签会流失信息。题库最终还需要落到可查询的存储。常见做法是用sqlite3导入导入之前把options里的换行符替换成\n避免SQL语句被截断。下面是一个最小插入示例import sqlite3 import json with open(jl2017_bank.json, encodingutf-8) as f: data json.load(f) conn sqlite3.connect(questions.db) conn.execute(CREATE TABLE IF NOT EXISTS questions ( id TEXT PRIMARY KEY, section TEXT, stem TEXT, options TEXT, answer TEXT, analysis TEXT, knowledge TEXT )) for q in data[questions]: conn.execute(INSERT OR REPLACE INTO questions VALUES (?,?,?,?,?,?,?), (q[id], q[section], q[stem], json.dumps(q[options], ensure_asciiFalse), q[answer], q[analysis], q[knowledge])) conn.commit() conn.close()逻辑说明使用INSERT OR REPLACE让题库可反复导入options转成JSON字符串保存避免另建子表。查询时用json.loads还原列表。相比一次插入一行这个写法也能看清楚每个字段的映射关系。参数说明SQLite的文本类型可以存JSON字符串但排序和精确过滤不如直接用列。如果将来要做选项级统计建议拆出question_options子表用外键关联题目id。5. 用题库数据反推七年级学生薄弱点的最小实现当题库有了结构化记录学情分析就能直接从JSON读取不需要再回头翻Word。下面给出一个两层级的实现先计算每道题的答题对错再统计知识点级别的正确率。5.1 把答题结果映射到知识点from collections import Counter def knowledge_accuracy(bank, submissions): stats {} for s in submissions: qid 2017JL_ str(s[question_id]) q next((x for x in bank if x[id] qid), None) if q is None: continue correct q[answer].strip().upper() s[result].strip().upper() kp q.get(knowledge) or 未标注 item stats.setdefault(kp, {correct: 0, total: 0}) item[total] 1 if correct: item[correct] 1 return {kp: v[correct] / max(v[total], 1) for kp, v in stats.items()} # 模拟两名学生的答案question_id对应原卷题号 submissions [ {question_id: 1, result: B}, {question_id: 2, result: A}, {question_id: 1, result: A}, {question_id: 2, result: B}, ] acc knowledge_accuracy(bank, submissions) for k, v in sorted(acc.items(), keylambda x: x[1]): print(k, v)逻辑说明函数以题库中的answer字段为基准比对submissions里的回答结果再把正确率按知识点聚合。排序用sorted(acc.items(), keylambda x: x[1])升序输出正确率最低的知识点自然排在前面这就是薄弱点的初筛结果。参数说明submissions里的question_id必须是原卷题号代码会拼接成2017JL_1去匹配。如果存在学生缺考或漏答题total会变小严格做法是预先对同一个学生补齐缺失记录否则统计出来的正确率会偏向已答题目。对于一次月考题目数量有限正确率波动很大我一般会追加一个规则total 3的知识点不进入最终排序避免样本太小。5.2 验证正确率是否可信单份试卷的统计结果很容易过拟合因此要用一道“换卷验证”来检查从题库中随机抽出一半题目用另一半题目预测同一学生的薄弱知识点比较两次排序的重合度。如果重合度低于阈值说明原卷的题目太少需要把多次月考试卷合并后再做分析。这种验证不需要复杂框架直接把题库按知识点分层抽样分别调knowledge_accuracy即可。由于本题集只有一张卷更实际的做法是只输出“错误集中度”用Counter统计错题的知识点分布哪个知识点出现次数最多就把它列为优先复习项。输出到CSV时记得用utf-8-sig编码避免Excel打开后中文乱码。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门