尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

统计学期末试卷PDF解析:OCR切题与SQLite FTS5题库检索

统计学期末试卷PDF解析:OCR切题与SQLite FTS5题库检索 简介这份资源是福州大学《统计学》课程的期末试卷及答案PDF面向高校统计学、经济管理类专业的备考学生与任课教师用于检验基础概念掌握程度与综合计算能力。试卷包含单选题、多选题、判断改错、辨析与计算分析五大题型覆盖定性数据与定量数据的区分、平均数中位数众数等集中趋势指标、标准差方差等离散程度指标以及抽样误差、总体与样本关系等基础理论。辨析题重点考察假设检验中弃真与纳伪两类错误的辨识计算分析题则涉及同季平均数与季节比率、移动平均与指数平滑预测、置信区间估计、Z检验判断样本均值是否显著偏离设定值以及相关系数与回归方程的求解。资源包仅含1个PDF文件体量约36KB下载后可随时打印练习或对照参考答案逐题复盘。目前已有313人学习适合课末冲刺与知识点查漏补缺使用。1. 统计学期末试卷 PDF 的解析难点为什么复制出来全是乱序学期末拿到一份《统计学》期末试卷 PDF很多人的第一反应是全选、复制、粘进 Word然后改成能背的题库。真做一遍就会发现三件事同时发生正文文字层被中文字体子集切碎复制出来夹杂方框选择题的选项在视觉上是四行抽出来变成一行公式和表格是扫描插图压根没有文字。福州大学这类高校试卷通常一学期一版文件名末尾挂着导出时间戳20210923194830同一门课几年下来能攒十几份全靠手工整理不现实。要做的事其实是一条固定管线探测文本层判断哪些页面需要 OCR按题号切块抽出题干、选项、分值、答案落成结构化数据最后进全文检索让「置信区间」这四个字能一键定位到具体某年某题。这套流程不只对学生有用做文档解析、教育类数据管道、题库类产品的工程师复用度很高。需要先说清一点整理出来的内容只作个人复习用别再往外分发。2. 统计学期末试卷 PDF 的文本层探测与提取pdfplumber 与 PyMuPDF 分工2.1 先判断这份 PDF 是文本型还是扫描型不要一上来就上 OCR。统计学试卷里通常混着两种情况理论题是排版文字计算题里的分布表、直方图、公式是截图上贴的。整份文件按页统计字符密度是最省事的判断方式。import fitz # PyMuPDF doc fitz.open(福州大学《统计学》期末试卷(20210923194830).pdf) for i, page in enumerate(doc): text page.get_text(text).strip() chars len(text) area page.rect.width * page.rect.height # 每万平方点的字符数作为密度指标 density chars / area * 10000 print(f第{i1}页 字符{chars} 密度{density:.2f})page.rect返回的点point单位是 PDF 内部坐标1 点约等于 1/72 英寸不同纸张大小下这个密度指标比单纯看字符数可靠。阈值参考每万平方点字符数判定处理动作小于 5扫描页按 300 DPI 渲染走 OCR5 到 30混合页先取文本层缺失区块回退 OCR大于 30文本页直接用 pdfplumber 抽词与表格2.2 用 pdfplumber 抽出字符、行和坐标pdfplumber 的价值在于它把每个字符的位置都留下来了切题号、判断选项是否换行全靠这些坐标。import pdfplumber with pdfplumber.open(福州大学《统计学》期末试卷(20210923194830).pdf) as pdf: page pdf.pages[3] words page.extract_words( x_tolerance2, # 横向间距小于该值视为同一个词 y_tolerance3, # 纵向间距小于该值视为同一行 keep_blank_charsFalse, extra_attrs[size, fontname], # 附带字号与字体便于区分题干和选项 ) for w in words[:8]: print(w[text], round(w[x0], 1), round(w[top], 1), round(w[size], 1), w[fontname])x_tolerance调大会把相邻的两个词粘成一个中文排版里字间距本来就小建议从 2 起调y_tolerance决定行聚类结果遇到上下标比如 χ² 的 2会被判成独立行宁可先调小再在后续步骤里按top差值合并。取出词之后按top分桶就得到行文本按x0排序就恢复阅读顺序——这一步解决的就是「复制出来乱序」的问题。2.3 用 PyMuPDF 读页面对象确定哪些块必须 OCR文本层里没有字不代表这页是纯扫描更常见的是正文有文本层、插图没有。用get_text(dict)看块类型分布最直接type 0 是文本块type 1 是图片块。page doc[5] d page.get_text(dict) stat {} for b in d[blocks]: stat[b[type]] stat.get(b[type], 0) 1 print(stat) # 例如 {0: 6, 1: 2}说明正文可抽图需另处理 for img in page.get_images(fullTrue): print(img[0], img[2], img[3]) # xref、宽、高拿到图片的 xref 与尺寸后可以只把这块区域裁出来送 OCR而不是整页识别。计算题的分布表往往就是这个类型整页 OCR 反而会把旁边的正文一起识别错。2.4 两个库怎么分工维度pdfplumberPyMuPDF字符级坐标完整直接可用完整字段名不同表格抽取有extract_tables()需自己按线聚类页面渲染成图依赖外部后端get_pixmap()一步到位大批量速度较慢明显更快典型用途正文行、表格探测、渲染、裁图我的分工一直是这样探测与渲染交给 PyMuPDF正文词与表格交给 pdfplumber两边用同一 DPI 和同一坐标原点对齐后面裁剪 bbox 才不会错位。如果抽出来是\ufffd或者成片方框说明字体子集缺 ToUnicode 映射这时别硬扛直接按扫描页处理。3. 扫描版统计学试卷 PDF 的 OCR版面切分与公式表格补救3.1 300 DPI 渲染、去线与二值化OCR 的准确率有一半取决于预处理。试卷扫描件的典型问题是表格线穿过文字、纸张底色偏灰、边缘有黑边。import fitz, cv2, numpy as np doc fitz.open(src) page doc[7] pix page.get_pixmap(dpi300, colorspacefitz.csGRAY) img np.frombuffer(pix.samples, dtypenp.uint8).reshape(pix.height, pix.width) # Otsu 自适应二值化比固定阈值更抗灰度底 _, bw cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 横向与纵向长核只抹长直线不动文字笔画 h_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1)) v_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40)) lines_h cv2.morphologyEx(255 - bw, cv2.MORPH_OPEN, h_kernel) lines_v cv2.morphologyEx(255 - bw, cv2.MORPH_OPEN, v_kernel) clean cv2.subtract(255 - bw, cv2.add(lines_h, lines_v)) cv2.imwrite(page_08_clean.png, 255 - clean)参数的作用与边界参数建议值调大之后会发生什么渲染 DPI300升到 400 耗时长150 会丢掉下标小字号二值化方式Otsu换成固定阈值灰度底会断笔去线核长度40太短去不掉表格线太长会把分数线和下划线一起抹掉中值滤波核3×35×5 以上会把五号字糊成团3.2 按题号切块再识别不要整页丢进去整页 OCR 的常见后果是跨栏串行、题号与题干错配。做法是先做水平投影找出空白行把页面切成若干块每块单独识别。from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) # 具体入参随安装版本略有差异 res ocr.ocr(page_08_clean.png, clsTrue) for line in res[0]: box, (text, score) line x0 min(p[0] for p in box); y0 min(p[1] for p in box) print(round(x0), round(y0), round(score, 2), text)输出里的score要留着低于 0.85 的行单独打标人工过一遍比事后全篇校对省时间。use_angle_clsTrue处理轻微倾斜的扫描件代价是耗时增加试卷通常不倾斜可以先关掉试一批看错行率再决定。3.3 统计学专有名词纠错词典通用 OCR 对统计学术语的识别错误有很强的规律性用一张替换表能消掉大部分。OCR 常见错形正确词成因置僖区间 / 置倍区间置信区间僖、倍与信字形近显箸性水平显著性水平著、箸混淆后又被简化样本均値样本均值日文异体字混入方差分折方差分析折与析偏旁相似x2 分布χ² 分布希腊字母被识别成拉丁字母加数字自由度 df自由度单位与变量粘连t 检验 / T 检验t 检验大小写不统一REPLACE_MAP { 置僖区间: 置信区间, 置倍区间: 置信区间, 显箸性: 显著性, 方差分折: 方差分析, 样本均値: 样本均值, x2分布: χ²分布, } def fix(text: str) - str: for wrong, right in REPLACE_MAP.items(): text text.replace(wrong, right) return text替换表要按字符串长度从长到短排序再应用否则「置信区间」还没替换「区间」这类短词先被别的规则改掉了。3.4 公式与表格的降级策略统计学试卷里的 Σ 求和式、分式、矩阵形式的方差协方差表目前 OCR 出来基本不能用。务实做法是不要把公式转成文本而是在题干里留占位符把原图裁下来存盘同时记录 bbox。clip fitz.Rect(x0, y0, x1, y1) # 与前面字符坐标同一坐标系 pix page.get_pixmap(clipclip, dpi300) pix.save(fformula_{qno:03d}.png) stem stem.replace(raw_text, f[[FORMULA_{qno:03d}]])占位符的好处是正文检索不受公式干扰复习时又能在原页上精确回看。表格同理文本型的直接用 pdfplumber 抽成二维数组扫描型的裁图人工录入一次即可一份卷子通常不超过三张表。4. 期末试卷 PDF 切题题干、选项、答案的结构化解析4.1 题号正则与题型判定切题的核心是一组稳定的正则。中文试卷的题号写法看着杂实际就那么几类。目标正则命中示例大题型^[一二三四五六七八九十]\s*[、.]三、计算题阿拉伯小题号^\s*(\d{1,2})\s*[、.]\s*12.括号小题号^[(]\s*(\d{1,2})\s*[)]3选项([A-D])\s*[、.]\s*A.分值[(]\s*(\d(?:\.\d)?)\s*分\s*[)]10分import re BIG re.compile(r^\s*([一二三四五六七八九十])\s*[、.]\s*(.{0,20}?题)) SUB re.compile(r^\s*(\d{1,2})\s*[、.]\s*) OPT re.compile(r([A-D])\s*[、.]\s*([^\n]*)) SCORE re.compile(r[(]\s*(\d(?:\.\d)?)\s*分\s*[)]) def classify(stem: str) - str: if len(OPT.findall(stem)) 3: return choice if 判断 in stem[:10]: return judge if SCORE.search(stem): return calc return shortclassify的判断顺序很关键先看选项个数再看关键词最后看分值。反过来的话「计算题15分」会被误判成单选因为它也带分值。4.2 选项、答案与解析的抽取选项在 PDF 里可能是四行也可能是两列先按top分行再按x0判断是否同一行有多个选项。答案有三个来源按优先级处理卷末的「参考答案」页、题干括号内的答案、以及题号后面的方括号标注。用关键字定位答案页范围def find_answer_pages(pages_text): start None for i, t in enumerate(pages_text): if re.search(r参考答案|答案与评分标准|答案要点, t): start i break return start找到起始页后把该页之后的全部内容单独切题与正文题目按题号做笛卡尔配对。对不上的题号单独列出来通常是排版把题号拆到了行首之外。4.3 输出 JSON 与入库 SQLite 的字段设计中间产物用 JSON 存一份方便肉眼核对正式查询走 SQLite。{ paper_id: fzu_stat_2021_20210923194830, section: 三、计算题, qno: 12, stem: 某工厂产品重量服从正态分布抽取 16 件样本样本均值 500g……求 95% 置信区间。, options: [], answer: [[FORMULA_012]], score: 10.0, chapter: 参数估计, page: 4, bbox: [72.0, 310.5, 520.3, 402.8], source_hash: 9f2c1a... }CREATE TABLE questions ( id INTEGER PRIMARY KEY, paper_id TEXT NOT NULL, section TEXT, qno INTEGER, stem TEXT NOT NULL, options TEXT, -- JSON 数组选择题才有 answer TEXT, score REAL, chapter TEXT, page INTEGER, bbox TEXT, -- JSON 数组用于回裁原页 source_hash TEXT UNIQUE -- 归一化题干后的哈希用于跨卷去重 );source_hash的算法题干去掉空白、标点、数字后取 md5。同一道题在不同年份的卷子里几乎一字不差靠这个字段能把重复题拦在入库之前否则题库越攒越大、检索结果全是同一道。4.4 文件名里的时间戳与多版本对齐20210923194830这样的 14 位串按%Y%m%d%H%M%S解析得到的是文件导出时刻不是考试时间但作为paper_id的稳定后缀非常合适——同一次导出的所有页共享同一个值。from datetime import datetime def parse_paper_id(filename: str) - dict: m re.search(r\((\d{14})\), filename) ts datetime.strptime(m.group(1), %Y%m%d%H%M%S) if m else None return {exported_at: ts.isoformat() if ts else None, paper_id: ffzu_stat_{ts:%Y}_{m.group(1)} if ts else filename}归档目录按课程/年份/paper_id三层放人找卷子和脚本查找都说得通。同一门课攒够三份之后再跑一次跨卷去重题库的净增量会明显下降。5. 统计学题库的全文检索与错题复盘SQLite FTS5 实战结构化入库之后真正的收益在检索。中文全文检索最容易踩的坑是分词器选错FTS5 默认的unicode61把一整段中文当成一个 token「置信区间」这种词根本匹配不到中间结果。tokenizer中文表现适用场景unicode61连续中文视作单个词元英文题干、变量名trigram按三字符滑窗切分中文短语、子串匹配porter英文词干化纯英文题库trigram需要 SQLite 3.34.0 及以上按三字符切窗所以查询串长度至少三个字符才能命中对「置信区间」「方差分析」这类统计学术语刚好合适。CREATE VIRTUAL TABLE q_fts USING fts5( stem, options, answer, contentquestions, content_rowidid, tokenizetrigram ); INSERT INTO q_fts(q_fts) VALUES(rebuild); -- 增量同步用触发器避免每次重建整表 CREATE TRIGGER q_ai AFTER INSERT ON questions BEGIN INSERT INTO q_fts(rowid, stem, options, answer) VALUES (new.id, new.stem, new.options, new.answer); END;查询时把 bm25 排序和错题次数揉在一起复习优先级就出来了SELECT q.id, q.qno, q.chapter, q.page, snippet(q_fts, 0, [, ], …, 12) AS hit, bm25(q_fts) AS rank, COALESCE(w.wrong_count, 0) AS wrong FROM q_fts JOIN questions q ON q.id q_fts.rowid LEFT JOIN wrong_book w ON w.qid q.id WHERE q_fts MATCH 置信区间 ORDER BY wrong DESC, rank LIMIT 20;snippet的第五个参数控制摘要长度中文建议不超过 16太长在前端列表里会被截断。bm25返回的是负数越小越相关所以要写在ORDER BY前面配合DESC。验证抽取质量有个省事办法随机取 20 条记录用存下来的bbox从原 PDF 裁图拼成一张对照图人工扫一遍。题号错位、选项粘连、公式占位遗漏这三类问题看一遍图基本都能揪出来。裁图代码复用第 3.4 节的get_pixmap(clip...)只是把formula_前缀换成verify_。最后一个技巧错题表别只存次数把每次答错的时间戳记下来查询时按最近 30 天内的错误次数加权比单纯累加更能反映当前薄弱章节——统计学里回归分析和假设检验的错题隔一个学期再做一遍错的往往还是同一批。本文还有配套的精品资源点击获取
返回列表