从旧版.doc到结构化题库:老试卷的自动化解析与OCR处理流程
2026/9/19 8:47:05 网站建设 项目流程

简介:这份资源是泰州市2005~2006学年度第一学期期末联考高一数学试题,适合高一学生期末复习、教师命题参考或数学爱好者自测使用。试卷覆盖面广,难度适中,包含选择题、填空题与解答题,重点考查集合、函数性质、立体几何、直线与圆、对数函数及实际应用等核心内容。资源包内共1个doc文件,大小约215KB,文件为完整试卷及参考答案,下载后可直接浏览或打印练习。已有98人学习浏览,适合希望通过典型综合题巩固基础、提升运算与逻辑推理能力的学生。通过研读试题中的体积表面积计算、三视图、函数值域、向量运算、圆与直线相交等题目,可以系统检验知识掌握程度,并在解答题部分体会数学建模与分类讨论思想,有助于期末备考中查漏补缺。

1. 为什么一份高一数学期末卷值得你专门写套脚本处理

拿到“泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].doc”这种文件,最常见的情况是:文件名带着年份和地区,打开却发现排版稀碎、公式乱码、图片错位,想筛选几道好题进自己的题库,却只能在 Word 里手动复制粘贴。做过一次就知道,这不是「打开另存为」能解决的问题,而是一套从 doc 抽取、识别、结构化到入库的完整流程。

这篇就按我实际处理这类试卷文档的路径来讲:怎么把老式 .doc 里的数学内容干净地抽出来,怎么对付扫描版或图片版题目,怎么用大模型把一道题拆成题干、选项、答案、解析,以及最后让几何图和公式不丢。整个过程用到的都是常见工具,有半年以上脚本经验的人跟着走一遍就能在自己机器上复现。目标不是写个一次性小工具,而是搭出一个以后任何学科试卷都能复用的处理管线。

2. 先拆 .doc:让文本、公式、图片各回各家

2.1 老式 .doc 与新 .docx 的边界在哪里

一份 2005-2006 学年的试卷,文件扩展名是 .doc,它可能是真正的 OLE2 二进制格式,也可能只是改了个后缀的 .docx,甚至可能是 RTF。这个区别决定了你能不能直接用 python-docx 处理。python-docx 只认 Office Open XML,也就是 .docx;遇到二进制 .doc,直接抛异常。

我一般先看文件头的魔数来判定:

file "泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].doc"

输出如果是Composite Document File V2 Document,说明是旧版 OLE2;如果是Microsoft Word 2007+,说明它其实是 docx。这一步不用猜,几秒钟就能确认后续该走哪条技术路线。

处理 OLE2 老格式,LibreOffice 的 headless 转换是当前最稳的免费方案。它不像某些在线转换工具那样会把公式渲染成低分辨率图片,而是尽量保留文本和对象结构。

2.2 用 LibreOffice 把 doc 批量转成 docx 和 pdf

soffice --headless --convert-to docx:"Office Open XML Text" --outdir ./converted "泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].doc" soffice --headless --convert-to pdf --outdir ./converted "泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].doc"

第一行把旧格式转成 docx 供后续 python-docx 读取;第二行同时生成一份 PDF,用来核对原始排版,尤其是分数、根号这类在纯文本提取后容易错位的元素。

参数说明:--headless表示不开图形界面;--convert-to docx:"Office Open XML Text"里的过滤器名是固定的,写成别的可能转换失败;--outdir指定输出目录,不写就输出到当前目录。转出来的 docx 里公式可能是 OMML(Office Math Markup Language),也可能是图片,取决于源文件当初怎么录入的。这一步不做任何清洗,只做格式搬家。

转换后建议顺手验证文件完整性:

python -c "from docx import Document; d=Document('./converted/泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].docx'); print(len(d.paragraphs))"

能打印出段落数就说明结构没坏。如果连 LibreOffice 都读不了,那这份文件大概率是损坏的或加密的,后续工作无从谈起。

2.3 抽取正文、表格和题号层级

docx 的结构是 paragraphs(段落)和 tables(表格)。数学试卷里题目通常以“1.”“2.”或“一、选择题”这类编号开头,用 python-docx 遍历段落,按正则匹配序号就能把题目边界切出来。

import re from docx import Document doc = Document('./converted/泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].docx') lines = [] for p in doc.paragraphs: text = p.text.strip() if text: lines.append(text) question_start = re.compile(r'^[一二三四五六七八九十]+、|^\d+[\.、]') current_q = None questions = {} for line in lines: if question_start.match(line): current_q = line[:20] # 截前20字做key questions[current_q] = [line] elif current_q: questions[current_q].append(line)

这段代码的作用是把连续段落归到最近的题号下面。正则里[一二三四五六七八九十]+、匹配“一、选择题”这种大块标题,\d+[\.、]匹配“1.”或“1、”。匹配成功后把当前题目标题存为 key,后续非题号行都追加进该 key 的列表。

实际处理时要注意两点:一是老试卷常有“(第Ⅰ卷)”这种带括号的标题,我的正则没覆盖,可以再加一层匹配;二是选择题的 A、B、C、D 选项在源文档里可能是独立段落,也可能挤在同一行,这会影响后续拆题。我一般会在这一步先看questions里每个 key 的列表长度,如果某个 key 下挂了几十行,说明题目边界没切对,需要回去调整正则。

3. 遇到扫描版或图片公式,OCR 要分两层做

3.1 先判断哪些题目是图片

很多流传的老试卷是纸质扫描后转成 doc 的,打开后每道题都是一张整图。前文提到的 DOCX 抽取对这种情况完全无效。判断方法很简单:统计文档里内嵌图片的数量,如果图片数量和题目数量接近,说明这个文档是图片型的。

from docx import Document from docx.opc.constants import RELATIONSHIP_TYPE as RT doc = Document('./converted/泰州市2005~2006学年度第一学期期末联考高一数学试题[精选].docx') img_count = 0 for rel in doc.part.rels.values(): if "image" in rel.reltype: img_count += 1 print("内嵌图片数:", img_count)

内嵌图片数量明显多于正常公式数量时,直接走 OCR 管道。你需要先把 docx 转成 PDF,再用pdftoppm把 PDF 按页渲染成高分辨率 PNG。分辨率我习惯用 200 DPI,太低识别不出上下标,太高会让 OCR 变慢且收益极小。

3.2 公式识别用 LaTeX-OCR,正文用 PaddleOCR

数学试卷 OCR 的难点不是英文和汉字,而是公式。普通文字识别模型会把x^2识别成x2,把分数识别成一行斜杠。处理这类文件,我一般会跑两个引擎:PaddleOCR 负责题目普通文本,LaTeX-OCR(也叫 pix2tex)负责渲染出的公式图片区域。

# 安装 pix2tex(LaTeX-OCR) pip install pix2tex[gui] # 命令行识别单张公式图 latexocr --image question_formula.png

实际跑的时候很少直接命令行,更多是写 Python 脚本调它的 API。把所有公式图丢进去后,返回的是 LaTeX 字符串,比如\frac{1}{2}这种。到这里,公式就算得救了。

但有个坑必须提醒:老试卷里的公式常常是 Word 域代码或者 MathType 嵌入对象,OCR 出来的 LaTeX 只是视觉近似,不是原始公式,可能丢符号。遇到这类特殊符号,识别错了要人工校对。我的做法是输出两个文件:原始识别结果和人工校对清单,不在脚本里做强纠错,因为数学符号的语义错误比字错误更隐蔽。

3.3 把 OCR 结果拼回题目结构

import json result = { "title": "泰州市2005~2006学年度第一学期期末联考高一数学试题[精选]", "questions": [ {"id": 1, "type": "choice", "text": "...", "options": ["A. ...", "B. ..."], "answer": ""}, ] } with open("paper_parsed.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)

这段代码把 OCR 出的文本整理成 JSON,核心是明确了题目结构:题型类别、题干文本、选项、答案、解析。answer为空是正常的,OCR 一般不识别答案,后续由人工或模型补全。

参数说明:ensure_ascii=False必须写,否则中文字符会被转成\u转义序列,既难读也不利于后续检索。indent=2是为了在 Git 里 diff 时能看清楚改了什么。

4. 用大模型把题目拆成结构化数据:提示词设计与边界

4.1 为什么要做结构化拆分

OCR 得到的文本是一堆连续字符串,适合人读,不适合查询和组卷。你希望以后能按“函数单调性”“数列求和”筛题,那就需要把每道题拆成题型、题干、选项、答案、解析、知识点标签。这个工作手工做几百道题会疯,用大模型处理很合适,但前提是提示词写得够清楚。

4.2 提示词模板:给模型的边界条件

import openai client = openai.OpenAI(api_key="sk-xxx", base_url="https://your-endpoint") prompt = """你是高中数学试卷解析助手。请把下面OCR得到的题目文本拆成JSON,字段如下: - id: 原题号 - type: 题型(选择题/填空题/解答题) - stem: 题干(不含选项) - options: 选项列表(选择题才有) - answer: 参考答案 - analysis: 简要解析 - tags: 知识点标签数组 要求:题干保留原始条件,不要化简或改写;公式用LaTeX;没有答案就留空字符串。 题目文本: {ocr_text} 只输出JSON,不要解释。""" resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.1, ) parsed = resp.choices[0].message.content

提示词里三个关键设计:一是定义了输出 JSON 的字段格式,比让模型自由发挥稳定得多;二是强调“题干不要改写”,防止模型自作主张改变已知条件;三是temperature=0.1,把随机性压到最低,数学题解析不追求文采,准确性优先。

用大模型的另一个实际好处是能顺手生成tags知识点标签。2005 年泰州高一期末的考点,大概率集中在集合、函数、数列这些章节。有了标签,后面做错题本或组卷就方便了。

4.3 质量校验不能省

import json def validate(parsed_str): try: data = json.loads(parsed_str) except json.JSONDecodeError: print("JSON解析失败,模型返回了多余文本") return None if not isinstance(data, list): data = [data] for q in data: assert "stem" in q, f"题目{q.get('id')}缺stem" assert q["stem"].strip(), f"题目{q.get('id')}的stem为空" return data

这段代码做了两件事:一是防模型返回 JSON 之外的多余解释(常见于未加“只输出 JSON”约束时);二是校验必填字段缺失。遇到JSONDecodeError时,最实用的重试策略是让模型「修正上次返回结果」,而不是从零再跑一次,省令牌也更快。

注意:大模型解析结果只能当草稿,答案和解析字段建议人工复核。数学解答题解法多样,模型给出的是参考路径,不能保证与官方标答一致。

5. 画几何图不用怕:从图片到 SVG 再回插文档

5.1 几何题的图用什么格式存

高一数学里函数图像、立体几何草图、平面向量示意图占了相当比例。这些图 OCR 提不出来,但转成 docx 时大概率以 EMF 或 WMF 格式嵌入。python-docx 不支持直接读 WMF,需要先转成 PNG 或 SVG。

# 先解压 docx 里的 media unzip -o converted/*.docx -d docx_extracted ls docx_extracted/word/media/

解压后如果media文件夹里的全是.emf文件,用 Inkscape 批量转成 SVG 或 PNG。至于 2005 年的原卷里那些图是不是这样嵌入的,其实不重要,重要的是你手里的文件用什么格式存,你就按什么流程转。

5.2 用 Matplotlib 重绘函数图比抠图更高效

老试卷扫描图里的函数图像往往模糊且有阴影,直接放进新文档很不专业。我一般直接看题目条件,用 Matplotlib 重绘。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-3, 3, 400) y = x**2 - 2*x - 3 fig, ax = plt.subplots(figsize=(4, 3)) ax.plot(x, y) ax.axhline(0, color='black', linewidth=0.8) ax.axvline(0, color='black', linewidth=0.8) ax.grid(True, linestyle='--', alpha=0.6) plt.tight_layout() plt.savefig("function_plot.svg", format="svg")

这段代码生成二次函数图像,np.linspace(-3, 3, 400)决定 x 轴范围和采样密度;ax.axhlineax.axvline画坐标轴;grid打开虚线网格。输出 SVG 的好处是后续插入网页不糊,转 PDF 打印也清晰。

重绘时要对照原题仔细检查:定义域端点开闭、交点坐标位置、渐近线有没有画。画错图比没图更误导学生,这个环节最值得花时间。

5.3 把 SVG 插回 Markdown 或 HTML 题库

最终题库我推荐输出成 Markdown,因为 GitHub、GitBook、语雀都原生支持。SVG 文件用标准图片语法引用:

![函数图像](./images/function_plot.svg) **第 8 题** 已知函数 $f(x) = x^2 - 2x - 3$,求: 1. 单调区间; 2. 在 $[-2, 4]$ 上的最大值与最小值。

图片路径建议用相对路径,方便整个题库目录整体移动。公式用$包裹的 LaTeX 写法,兼容性比 Word 域代码好得多。

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

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

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

立即咨询