从Word文档构建在线题库:解析、建模到自动评分实践 简介这份IQ-100题(附答案).doc是一份面向逻辑推理、常识积累和思维能力训练的中文趣味智力题集适合学生、职场人士及日常脑力练习者在碎片时间使用。资源为1个doc文档压缩包大小27KB内容包含飞机失事、地理人口、历史文化、文字谜语、健康常识、道德困境、数学运算等100道经典题目每题附参考答案与解析思路覆盖逻辑推理、语言理解、科学常识、生活判断等多个维度。目前已有173人学习下载适合希望跳出常规思维、提升综合分析与应变能力的读者作为测试交流或休闲挑战均较为实用。1. 先别急着转 PDF把《最新IQ-100题(附答案).doc》当作一份线题库的交付物一个人的时候这事看起来简单打开 Word把 100 道题复制出来再配个答案表齐活。可一旦这文件要同时供给 HR 面试、团队测评、前端报名页面和后台判分模块手工复制出来的题目很快就会变成“数据事故现场”——题号错位、选项丢空、图形题在纯文本里消失、答案还混在题干段尾。所以我的处理顺序从来不是“转格式”而是“先把文件读干净再把题目建成结构最后让它能被程序出题和收卷”。这里真正难的不是解析本身而是保证 100 题的题面、选项、答案和解析四个要素在自动化之后仍然一一对应。如果你也需要从这类文档里批量生成可检索、可校验、可作答的题库下面这套方案可以直接照搬。2. 先把 .doc 读干净从《最新IQ-100题(附答案).doc》里稳定抽出题面2.1 先分清 .doc 和 .docx探测格式再选解析器拿到文件的第一件事不是直接写 Python而是确认它到底是老版二进制.doc还是实际上扩展名写错了的.docx。python-docx只能读后者读到老.doc会抛出异常反过来说antiword只能处理老格式遇到新格式则输出乱码。我一般先用file命令看真实类型file 最新IQ-100题(附答案).doc如果输出里包含Composite Document File这是老格式常见做法是转成纯文本后做正则切分如果显示Microsoft Word 2007直接用python-docx读取段落。需要说明的是.doc老格式里图片、文本框、分栏混排的情况很常见直接读文本容易丢内容所以我会先让 LibreOffice 把老.doc无损转成.docx再进入统一解析流程libreoffice --headless --convert-to docx 最新IQ-100题(附答案).doc --outdir ./converted这个方案不需要 Windows 上的 Word COM 对象Linux 服务器上也能跑通。转完后用unzip -l检查转换结果里是否存在word/media/下的图片文件因为 IQ 题里常有图形推理题这些图片一般在文档的word/media/目录中而不是嵌在文字段里。抽出图片时建议按rId与正文的关联顺序重命名否则后面的图形题会“有图无题”或者“有题无图”。2.2 按题号切块别信分页要信编号正则打开转换后的文档前三分钟先摸一下排版规律。最常见的有三种题目从第 1 题连续排到最后题目和答案区域分开前 100 题是题干末尾集中给答案对照表以及题目和答案混排在同一段比如题干末尾跟着“【答案】A”。对应的切分策略不一样所以第一步是抽几段出来看格式python -c from docx import Document doc Document(./converted/最新IQ-100题(附答案).docx) for i, p in enumerate(doc.paragraphs[:30]): print(i, repr(p.text[:80])) 看到实际输出之后再决定切块规则。我通常不按页码或分页符切因为 Word 的分页在转换后不稳定真正稳定的是题号。用正则匹配行首或行中出现的“数字点/顿号/右括号”在这里切一份完整题目。注意“1、”和“1”在中文文档里都常见正则要写成兼容两种import re # 匹配 1、 1. 1) 或 第1题 等常见题号前缀 split_pattern re.compile(r^\s*(\d{1,3})\s*[、.)]\s*)切出来的内容先按“题号-文本块”存成中间文件不要直接进数据库。这一步的作用是把乱序的段落按题号重排同时把跨页的题拼回一个逻辑块。2.3 一个能落地的抽取脚本输出 CSV 中间格式基于上面的切块策略我给出一段最小可运行的抽取脚本。它会把每道题切成“题号、题干、选项A-D、答案”五个字段结果先落到 CSV方便人工抽查import re from docx import Document # 读取已被 LibreOffice 转好的 docx doc Document(./converted/最新IQ-100题(附答案).docx) # 合并所有段落成一个长文本按段落边界保留换行 full_text \n.join(p.text.strip() for p in doc.paragraphs if p.text.strip()) # 按题号切块 block_pattern re.compile(r(?m)^\s*(\d{1,3})\s*[、.)]\s*(.*?)(?^\s*\d{1,3}\s*[、.)]\s*|\Z)) blocks block_pattern.findall(full_text) # 按中文括号或“答案”标记抽取答案 ans_pattern re.compile(r[【\[]?\s*答案\s*[】\]]?\s*[:]?\s*([A-Da-d]|[√×])) # 选择题选项一般以 A. B. C. D. 开头 opt_pattern re.compile(r(?m)^\s*([A-Da-d])[\.、]\s*(.*)$) for num, body in blocks: # 先找答案 ans_match ans_pattern.search(body) answer ans_match.group(1).upper() if ans_match else # 去掉题干里的答案标记 body ans_pattern.sub(, body) # 拆选项 opts opt_pattern.findall(body) ...这段代码有三个关键点。第一合并段落时保留\n是为了让“A. xxx”这类选项能独立成行供选项正则匹配如果全部接成一行^的锚点会失效。第二切分正则要求每个题号前有换行因此原始文档如果题号不在行首需要先做一次行首修正。第三答案单独用中文括号或方括号匹配防止题干里出现“回答这道题”之类的干扰词。落到 CSV 后我会先检查答案字段的空值和选项数量而不是直接入库。2.4 抽取失败的三种典型表现与对策运行完上面脚本不要直接看成功数量先看三类高频错误。第一类题号和题干连在一起正则把“第 12 题”误判成题号对策是改题号正则排除“第”字后面紧跟数字的情况。第二类答案是“A/B/C/D”而题干本身包含“AB”这类选项答案提取时会把“A B”误认为双选对策是先按列提取选项再做答案映射。第三类图形题在文字里只剩一个占位符或空行图片文件没有对应题号对策是从文档的word/media/目录按顺序导出图片再用“图片序号与题号”的映射关系人工核对。做完这三项检查才算拿到一份可信的题面。提示如果文档里大量使用文本框python-docx的paragraphs可能读不到内容。真要应对这种文档跳过docx直接解压 XML 并遍历w:txbxContent节点或者干脆用 PDF 渲染后 OCR但这圈成本至少要翻三倍。文档里 90% 的 IQ 题都属于普通段落或表格不值得一上来就上 OCR。3. 题目建模用答案、题型、难度把 100 道题变成可检索数据3.1 为什么不能把题面存在一个 text 字段里很多从文档直接转题库的系统最后只建一张表id, question, answer。看起来够用但 IQ-100 题这种带图形推理、数字序列、文字逻辑的题库一旦出现“多选、顺序敏感、图形描述与图片并存”的题目文本表就崩了。比如一道题涉及四张图片把所有图片地址拼进一个question字段里前端渲染时就要自己拆字符串再比如答案“A C”到底表示“A 和 C 都选”还是“随机顺序里第 A 组第 C 个”没有结构歧义就藏在解析里。所以我在设计题库表时明确要求题目、选项、答案、解析、难度、题型六个维度分开存。这样后续随机组卷、难度加权、正确率统计才不会拿到一堆没法运算的长文本。3.2 题型规范化单选、判断、图形推理的统一字段IQ 题常见题型不止单选需要一套统一的题型枚举值。我的方案是single_choice表示四选一multi_choice表示多选true_false表示判断题答案只允许T/Fimage_choice表示图形推理题干带图片且每个选项也可以是一张图片。注意判断题在老文档里常写作“√/×”入库前必须统一转成T/F否则判分逻辑要同时兼容三种真值写法。题型规范化之后还要解决“答案顺序”问题。部分 IQ 题中控制变量在图里文字只是干扰项所以选项顺序不能洗牌而另一类言语理解题选项顺序可以随机。我在题库表里增加一个shuffle_able布尔字段默认false只有明确标记为允许乱序的题组卷时才会打乱选项。这一设计直接避免了图形题换选项后画错图的低级事故。3.3 题库表结构和 JSON 示例下面给出我实际使用的字段设计存进关系型数据库或 JSON 文件都合适。核心表结构如下字段类型说明idint题库内唯一编号source_noint原文中的题号例如 1100typestring题型枚举titletext题干纯文本image_idsjson题干图片 ID 列表按顺序排列optionsjson选项数组每项含key和text/image_idanswerstring规范答案多选用逗号分隔explaintext答案解析可空difficultyint15 难度shuffle_ablebool是否允许打乱选项对应的 JSON 示例我倾向于存储为一份可版本管理的题库文件方便走 Git 评审{ id: 37, source_no: 37, type: image_choice, title: 观察前三个图形选出能延续规律的选项, image_ids: [q37_main.png], options: [ {key: A, text: , image_id: q37_a.png}, {key: B, text: , image_id: q37_b.png}, {key: C, text: , image_id: q37_c.png}, {key: D, text: , image_id: q37_d.png} ], answer: C, explain: 图形按顺时针旋转 45 度同时阴影块每步移动一格, difficulty: 3, shuffle_able: false }这个字段设计里最重要的约定是answer永远只存规范值不存“答案文字”。多选答案存成A,C而不是[A, C]。原因是导入导出时JSON 数组容易被 Excel 展开成单元格而字符串在 CSV 里不会变形判分时再split(,)即可。image_ids同理不要把文件路径和题目正文混在一个字段里。3.4 把“附答案”变成可校验的 answerKey 文件从原文档抽取出的答案通常长这样1-A 2-B 3-AB 4-√...。我问过自己一个问题这些答案可信吗文档标题里的“附答案”不代表答案就正确常见错误是题号抄错、AB 与 BA 混乱、判断题符号被 OCR 识别成乱码。所以我不会直接把答案写进正式题库而是先输出一份独立的answerKey清单与题目分开存储。例如一行表示一题1,A,single_choice 2,B,single_choice 3,A|C,multi_choice 4,T,true_false用|分隔多选值而不是逗号。原因很简单CSV 主字段内部再用逗号会炸列|在 IQ 题里几乎不出现安全系数更高。这个 answerKey 是后续校验、回代比对、人工抽查的锚点。没有它任何自动化抽题都只是在处理一堆字符串。4. 在线组卷与自动评分让《最新IQ-100题(附答案).doc》跑成一套测验服务4.1 随机抽题、难度分布与服务端校验题库结构就绪后下一步是提供 API 让前端能取题、作答、判分。这里有一个原则答案永远不能跟着题目一起出到前端。常见做法是每次测验生成一个quiz_id后端把题面和选项发给前端但answer、explain只保留在后端内存或按测验 ID 存储。抽题策略上100 题库我一般默认抽 20 题难度配比是 12 级 6 题、3 级 8 题、45 级 6 题避免一套卷全难或全易。组卷时还要带随机种子这样同样的种子能生成相同试卷方便 debug 和复盘。抽题参数推荐这样设置参数默认值说明quiz_size20单次测验题目数difficulty_dist{1:3, 2:3, 3:8, 4:3, 5:3}难度分布总和必须等于 quiz_sizeallow_shuffletrue允许打乱shuffle_abletrue的选项seed随机数传入相同 seed 复现同一套卷子time_limit600测验时限单位秒4.2 最少代码完成组卷与判分使用 FastAPI 做后端时核心只有两个接口一个创建测验一个提交答案并判分。下面是一个最小实现重点不在完整工程而在判分逻辑怎么稳from fastapi import FastAPI from pydantic import BaseModel from typing import Dict import random, itertools app FastAPI() # 简化题库正式环境从数据库或 JSON 加载 BANK [...] # 题库数组每项是一个题目 dict class SubmitBody(BaseModel): quiz_id: str answers: Dict[str, str] def pick_questions(size20, seed42): random.seed(seed) selected random.sample(BANK, size) for q in selected: if q.get(shuffle_able): opts q[options][:] random.shuffle(opts) q[options] opts return selected def normalize_answer(ans: str) - str: return .join(ans.strip().upper().split(,)).replace(|, ) app.post(/quiz/start) def start_quiz(size: int 20): quiz_id fq-{random.randint(100000, 999999)} questions pick_questions(sizesize, seedrandom.randint(0, 99999)) return { quiz_id: quiz_id, questions: [{id: q[id], title: q[title], options: q[options]} for q in questions] } app.post(/quiz/submit) def submit(body: SubmitBody): score 0 detail [] for q in BANK: if str(q[id]) not in body.answers: continue given normalize_answer(body.answers[str(q[id])]) right normalize_answer(q[answer]) ok given right score ok detail.append({id: q[id], correct: ok}) return {score: score, detail: detail}这段代码把判分逻辑压缩到了最短但有个隐藏细节normalize_answer会先把答案转大写再去掉内部逗号最后把|替换为空。这么做的原因是前端多选答案可能传成A,C或A|C如果直接比对A,C和AC就会误判。在 IQ 题这种答案多选但顺序不敏感的场景下更严谨的做法是set(given.split(,)) set(right.split(,))避免把“AB”和“BA”判错。上面的示例代码为保持可读性先做字符归一化真实落地时我建议直接使用集合比较。4.3 防“答案泄漏”的现实对策文档名里带了“附答案”三个字这里直接给出三个必做的隔离措施。第一API 返回的题目对象中剔除answer、explain、source_no字段前端拿不到原始题号CTRLF 搜原文档也对应不上。第二接口幂等性要做同一quiz_id只允许提交一次第二次提交直接拒绝防止用户刷分数。第三图片题目不要用固定文件名组卷时给每个图片加时间戳参数避免从浏览器的资源列表按顺序推导答案。这些不是安全设计的全部但足以把文档泄漏风险降到可控范围。4.4 题库导入与增量更新当文档后续修订成第 101120 题时清洗脚本要支持增量导入而不是整库重建。我的做法是把source_no作为原文题号的唯一约束导入时对已存在的题号执行“封存旧版本、写入新版本”的更新。具体到数据表就是增加一个active字段旧题目标记为false新题目插入后线上随机抽题只查activetrue的记录。这样既保留了历史答案和统计记录又不会让过时的图形题继续出现在新试卷里。增量导入脚本的输入格式继续沿用 3.4 节的 answerKey跑完后再做一次计数校验新文档题数和库里activetrue的记录数必须一致。5. 交付前验证用回代校验守住 100 题的一一对应最后一关不是“我看了一遍没问题”而是用脚本把答案重新投影回原文文档逐题核对。具体技巧叫“回代校验”从题库里取题干的前 20 个字符和原始文档每一段文本重新做一次包含匹配如果一道题在原始文档中找不到对应起点说明切块阶段就已经丢题了。判断逻辑按准确率分层100 题中匹配率低于 95%直接视为抽取失败回到第 2 章重跑匹配率高于 99% 才允许进入线上题库。下面这段命令用 Python 实现回代校验的核心部分python -c from docx import Document import json orig \n.join(p.text for p in Document(converted/最新IQ-100题(附答案).docx).paragraphs) bank json.load(open(bank.json, encodingutf-8)) missing [] for q in bank: head q[title][:20].strip() if head not in orig: missing.append(q[source_no]) print(missing:, missing) print(match_rate:, (len(bank)-len(missing))/len(bank)) 参数说明head取题干前 20 个字符是根据最终交付文档通常不会在题干开头直接讲废话来定的如果题库里题干第一句是“第 37 题 观察前三个图形”那head里的“第 37 题”会直接影响匹配所以我切块时已去掉“第 N 题”前缀只保留纯题干。这个命令跑完还要针对missing列表里的题号做一次人工复核核对原文档对应页面。验证技巧的第二个部分是答案级回代而不是题干级用答答案字段去原文的“答案栏”找对应项。原文中如果答案集中列在文档末尾那么从题库生成一份题号-答案对照清单与原文末尾答案段做逐行 diff理想情况是零差异。一旦 diff 结果出现移位优先怀疑原文档存在跳号而不是题库错误。这里就不再展开更多流程了你拿到一份最新IQ-100题(附答案).doc时把这套脚本存成项目里的verify.py每次改题库都跑一遍就能把 100 题的准确性从“肉眼保证”变成“自动校验保证”。本文还有配套的精品资源点击获取