☰
用python-docx解析Word题库,打造AI能力评估与在线自测系统
2026/10/15 23:31:05 网站建设 项目流程

简介:这是平安集团IQ测试(基础性向测试)的经典试题及答案,面向求职应聘者、企业招聘人员及希望提升逻辑思维能力的读者。压缩包内含1个doc文档,大小约511KB,内容完整可直接打开。已有1983人学习浏览,适合笔试前反复演练。试题共60个题目,建议45分钟内完成,涵盖类比推理、数字序列分析、逻辑推断、词语解码、概念理解、数学计算、空间想象等常见IQ题型。每道题附有选项和答案解析,能帮助读者理解解题路径并总结规律,例如通过排除法处理硬币面值问题、利用传递关系判断逻辑命题、结合生活常识解答功能对应题。系统练完这套题,可有效提升抽象思维、推理能力与解题速度,是企业IQ测试的实用备考资料。

1. paIQ测试题与答案:一份被低估的AI能力评估文档

很多团队的面试资料库里都躺着一份叫“paIQ测试题及答案.doc”的老文档。第一次看到它的人,往往会把它当成普通笔试题归档,直接忽略掉。但如果你仔细拆开这套题,会发现它是一套结构化程度相当高的AI应用能力评估试卷,题面覆盖了概念理解、代码排错和场景设计,答案部分也不只是给个“对错”,而是带着判分要点和参考实现。真正的问题在于:doc格式让它又老又硬,没法直接在线用、没法自动批改、也没法做数据统计,于是多数人看完就扔回了文件夹。

这个标题所代表的,本质上是“一套静态测试资源如何变成一套动态评估工具”的问题。它能帮你解决三件事:第一,在技术招聘中用同一把尺子筛人,避免面试官主观评分;第二,让团队内部做AI技能摸底,知道谁的薄弱项在哪个维度;第三,给开发者一份自测清单,自己对照答案找差距。适合的人群很明确:技术面试官、团队技术负责人、以及想量化自己AI工程水平的开发者。读完后你会得到一个可复现的流程:把doc里的题面和答案拆出来,结构化,配上评分规则,再跑成一个在线自测页面。

2. 把 .doc 里的题面与答案拆成结构化数据:基于 python-docx 的提取方案

很多人拿到这份文档,第一反应是手动打开Word,把题目一条条复制到在线问卷里。如果只有十来题还可以,但paIQ这类题集一般在五六十题以上,复制粘贴让体力消耗很大,关键是后续改版时你会想死。所以第一步必须把doc内容转成结构化数据,让每一道题都能独立访问、单独修改、方便统计。

2.1 为什么先要转成结构化数据

doc是老式二进制格式,python-docx只支持处理新的docx,所以直接拿python-docx去读.doc不可行。你可能会想到用antiword或textract直接把文本抽出来,但这两个工具只能拿到纯文本,题号和选项的对应关系会丢失。更稳的路径是先转成docx,再用python-docx读取docx底层的XML结构。docx本质是一个ZIP包,里面的word/document.xml存着所有段落,标题级、表格、分页信息都保留,这为后续按题号切分提供了基础。

另外,结构化数据直接决定了后面评分和在线系统能不能跑起来。如果停留在Word排版里,你想随机抽题就必须自己人工数题号;想做自动批改就必须让程序能定位答案所在位置。把题目按“ID、题干、选项、答案”拆成JSON,相当于给题库建了一张表,后面不管是做评分权重还是搭网页,都是在消费这份数据。所以这一步值得多花时间。

2.2 提取题目、选项和答案的完整脚本

常见做法是先用LibreOffice的headless模式把.doc批量转成.docx,注意转换时要保留原排版,这样后续解析才不容易丢字段。命令如下:

soffice --headless --convert-to docx --outdir ./converted "./paIQ测试题及答案.doc"

--headless指定无界面模式,--convert-to docx指定目标格式,--outdir指定输出目录,文件路径建议用双引号包住避免中文或空格出问题。转换完成后,你会得到converted/paIQ测试题及答案.docx,再运行下面的Python脚本。

import docx import re import json doc = docx.Document("./converted/paIQ测试题及答案.docx") questions = [] current_q = None option_pattern = re.compile(r"^([A-D])[.、\s]") answer_pattern = re.compile(r"答案[::]\s*([A-D]+)", re.IGNORECASE) for para in doc.paragraphs: text = para.text.strip() if not text: continue # 题号匹配:如 1.、2.、或用括号包裹的数字 qnum_match = re.match(r"^(\d+)[.、]", text) if qnum_match: if current_q: questions.append(current_q) current_q = {"id": int(qnum_match.group(1)), "stem": text, "options": {}, "answer": ""} elif current_q is not None: opt = option_pattern.match(text) if opt: current_q["options"][opt.group(1)] = text[2:].strip() else: ans = answer_pattern.search(text) if ans: current_q["answer"] = ans.group(1) else: current_q["stem"] += "\n" + text if current_q: questions.append(current_q) with open("paiq_questions.json", "w", encoding="utf-8") as f: json.dump(questions, f, ensure_ascii=False, indent=2) print(f"提取完成,共 {len(questions)} 题")

逻辑说明:脚本按段落顺序遍历docx正文。每遇到一个以“数字+点/顿号”开头的段落,就认为是新题,同时把上一题存入列表。之后的行里,如果匹配“A.”、“B、”这类模式,就当作选项;如果匹配“答案:”字样,就当作标准答案;其他行都追加到题干中,这样能处理题干跨多行的情况。最后把所有结果写入JSON文件。

参数说明:实际跑之前,先确认题库中答案标记用的是中文冒号还是英文冒号,answer_pattern里的[::]已经包含了两种。如果标记是“正确答案”,就把正则前缀改成“正确答案”。选项分隔符如果是“A、”,option_pattern同样能匹配,但如果你用的是“A)”这种,需要把括号补进去。多选题答案如果是“AB”,[A-D]+会直接抓取;如果是“A、B”,则需要先去顿号再存。

有些doc的题目会放在表格里。比如第一列是题号、第二列是题干、第三列是选项、第四列是答案。这种情况遍历doc.tables更直接:

for table in doc.tables: for row in table.rows: cells = [cell.text.strip() for cell in row.cells] if len(cells) >= 4 and cells[0].rstrip('.').isdigit(): q = { "id": int(cells[0].rstrip('.')), "stem": cells[1], "options": {}, "answer": cells[3] if len(cells) > 3 else "" } # 选项列通常是 "A.xxx B.xxx C.xxx D.xxx" for m in re.finditer(r"([A-D])[.、]\s*([^A-D]+)", cells[2]): q["options"][m.group(1)] = m.group(2).strip() questions.append(q)

表格方案的关键在于确认列顺序,不要想当然。有些文档会倒过来,前三列是选项,最后一列是答案。跑一次后打印questions的前几项,比什么都管用。

2.3 针对排版混乱的 doc 的容错处理

现实中的doc很少像模板一样规整。常见翻车点之一是选项之间没有换行,而是用两个空格分隔。这种情况下上面的段落解析只能识别整个块,option_pattern匹配不到单独选项。解决思路是:先判断一行里是否同时出现“A.”和“B.”,如果出现了,就用正则把行拆成多个选项再入库。第二个翻车点是答案写在题目末尾,比如“1.什么是xxx?答案:A”,这时正确答案会被当成题干的一部分合并掉。我一般会先检查题干里是否包含“答案”两个字,包含则先截取答案,剩余部分再作为题干。第三个翻车点是Word自动编号让题号不出现在段落文本中,python-docx读取时会丢数字。这种情况建议在Word里另存为规范文本,或者直接读取paragraph._p的XML属性来提取自动编号值,工作量稍大但可行。

提取之后一定要做质量验证。我一般会检查JSON里answer为空的题目占比,以及options数量是否为0。如果答案覆盖率低于95%,说明题目排版和正则没有对齐,需要先抽样查看原始段落,不要急着往下做评分系统。

3. 根据 paIQ 测试结果做能力评级:评分权重与阈值设定

结构化数据不等于能直接打分。paIQ这类测试题通常混合了概念题、代码题和场景题,如果每道题都按1分计算,代码题需要半小时才能答完,概念题可能一分钟就能选完,分数完全失衡。所以评分必须分成三步:给题目分类、定权重、算维度得分。

3.1 维度拆解与权重分配

我在处理题库时先用规则自动分类,再人工抽检修正。常见分类是三个大维度:基础概念、代码能力、场景设计。可以用下面这段逻辑:

def categorize(stem): if any(k in stem for k in ["什么是", "区别", "原理"]): return "concept" if any(k in stem for k in ["写出", "修复", "运行结果", "代码"]): return "code" if any(k in stem for k in ["如何设计", "方案", "场景"]): return "scenario" return "concept"

逻辑说明:any会遍历关键词列表,只要题干里出现任意一个关键词就返回对应类别。这段逻辑不完美,但能覆盖大部分题目,人工抽检时把明显的错分类改掉就行。实际使用中可以把关键词列表放到外部配置文件,避免每次改代码。

权重分配方面,我按岗位经验配了一个默认方案:基础概念30%、代码能力40%、场景设计30%。这个比例对应AI工程师的日常职责。概念跟不上会导致技术选型偏差,代码能力弱直接影响交付,场景设计能力决定架构质量。如果团队偏应用开发,可以把代码能力提到50%,概念降到20%。权重不是拍脑袋出来的,而是根据岗位JD里高频技能出现次数来调整。调整权重时只需要改一个字典,不要动题库本身。

3.2 三个必调的评分参数

第一个必调参数是题目分值。很多题库每道题默认1分,但代码题的工作量可能是概念题的5倍。我通常在JSON里给每道题增加weight字段,概念题1分,代码题3分,场景题2分。计算时用raw_score = sum(correct * weight) / sum(weight) * 100,这样大分值题目对总分的影响更合理。

第二个必调参数是及格线。建议不要拍脑袋定60分,而是用团队历史测试数据去校准。具体做法是:让现有团队成员先做完这套题,记录分数分布,然后取P40分位作为及格线,P80作为优秀线。比如12个人的样本,正序后第5个人的分数就是P40。这样可以避免及格线设置脱离实际。如果团队刚组建没有历史数据,就先临时给定一个值,等积累几次测试后再回来调。

第三个必调参数是多选题判分规则。常见规则有三种:完全匹配才算对、漏选得一半、错选不得分。建议用“错选不得分,漏选得半分”。AI场景里少考虑一个边界,通常比选错一个方案更常见,这样给分会更平滑。计算函数可以写成:

def score_multi_answer(predicted, correct): if set(predicted) == set(correct): return 1.0 if set(predicted).issubset(set(correct)): return 0.5 return 0.0

参数说明:predicted和correct都是字符串,比如“ABC”和“AB”。漏选时用户答案是正确的子集,给一半分;只要有错选,返回0。如果你业务方坚持全对才算分,把issubset那条分支删掉即可。

3.3 从原始分到归一化报告

原始分折算成百分制后,还要做归一化才能横向对比。不同候选人可能做的是不同版本试卷,题目数量不同,原始分没有可比性。所以我不会直接输出总分,而是输出“维度得分”:该维度题目得分之和除以该维度题目满分之和。比如代码维度满分是40分,你得28分,那代码维度得分就是70%。

生成报告的代码通常是这样:

def generate_report(answers, config): report = {} for dim, dim_config in config["dims"].items(): total = 0 earned = 0 for qid in dim_config["question_ids"]: weight = config["weights"].get(qid, 1) total += weight if answers.get(qid) == dim_config["correct_ids"].get(qid): earned += weight report[dim] = round(earned / total * 100, 1) if total else 0.0 overall = round(sum(report.values()) / len(report), 1) report["overall"] = overall return report

参数说明:config字典里维护着每个维度的题目ID列表、正确答案和权重。真实项目中这些数据会自动从JSON里加载,而不是写死在代码里。推荐把config单独存成一个YAML文件,方便面试官根据岗位临时调整权重,不用改代码。输出示例大概是{"concept": 80.0, "code": 62.5, "scenario": 75.0, "overall": 72.5}。最终报告里我会把每个维度的丢分点列出来,而不是只给一个总分,这样候选人知道该补哪里,团队也知道面试追问该从哪块切入。

4. 用 paIQ 题库搭一个在线自测系统:从 doc 到 Web 表单的最短路径

题库已经变成JSON,评分规则也定了,接下来就可以让这套题真正跑起来。很多人会想到复杂的前后端分离架构,但对内部自测来说,一个单页HTML加一个JSON文件就够了。这章给出最短路径。

4.1 试卷结构映射与题目随机抽

在线自测不能把全部题一股脑丢给用户,一是体验差,二是题量大会让人疲劳。常见做法是按维度比例抽题。比如每次测20题,基础概念抽6题、代码抽8题、场景抽6题,这样能保底覆盖所有维度。

先给题库补上category字段,然后写一个抽题函数:

async function loadQuestions() { const resp = await fetch("./paiq_questions.json"); const raw = await resp.json(); return raw; } function sampleByCategory(questions, sizeByCategory) { const shuffled = {}; for (const q of questions) { const cat = q.category || "concept"; if (!shuffled[cat]) shuffled[cat] = []; shuffled[cat].push(q); } const selected = []; for (const [cat, size] of Object.entries(sizeByCategory)) { const bucket = shuffled[cat] || []; // Fisher-Yates 洗牌 for (let i = bucket.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [bucket[i], bucket[j]] = [bucket[j], bucket[i]]; } selected.push(...bucket.slice(0, size)); } return selected; }

逻辑说明:loadQuestions用fetch抓取同目录下的JSON文件。sampleByCategory先把题目按类别分桶,再用Fisher-Yates打乱,最后从每个桶前面截取设定数量。这样随机性足够,又不至于让前端依赖后端接口。

参数说明:sizeByCategory是一个对象,比如{ concept: 6, code: 8, scenario: 6 }。注意如果某个类别题目不够抽,实际数量会少于设定,所以页面要显示“实际抽中X题”,避免候选人疑惑。

4.2 自动批改的判据设计

自动批改要区分单选、多选和主观题。单选直接比较字符串,多选用漏选得半分的规则,主观题不做自动判分,只留文本框和“参考要点”折叠区。判题器可以写成下面这样:

function gradeQuestion(userAnswer, question) { if (question.type === "single") { return userAnswer === question.answer ? question.weight : 0; } if (question.type === "multi") { const correct = question.answer.split("").sort().join(""); const user = (userAnswer || "").split("").sort().join(""); if (user === correct) return question.weight; // 漏选给一半分 const correctSet = new Set(question.answer.split("")); const userSet = new Set(userAnswer || ""); const isSubset = [...userSet].every(c => correctSet.has(c)); return isSubset ? question.weight * 0.5 : 0; } return 0; // 主观题不自动判 }

逻辑说明:多选先把答案字符串拆成数组、排序后再拼回字符串,这样用户选“BA”和“AB”都会被判为同一答案。漏选判断用every检查用户答案的每个字符是否都在正确答案集合里,只要有一个不在,就是错选。主观题权重保留,但最终得分由人工复核时填入。

这里要注意一个容易忽略的点:判断题、填空题不要混进这个函数。如果题库里有填空题,需要单独用归一化后的答案匹配,比如把空格和大小写都忽略。

4.3 数据持久化与结果导出

在线自测如果不想引入后端数据库,可以直接把测试结果存到localStorage,然后导出CSV。提交时把每次作答的题目ID、用户答案、得分、用时存成一个对象,再追加到localStorage的数组里。导出用Blob生成CSV文件,方便后续用Excel做统计。

function exportCSV(results) { const header = ["时间", "题目ID", "题干", "用户答案", "正确答案", "得分"]; const rows = results.map(r => [ r.timestamp, r.qid, r.stem, r.userAnswer, r.correctAnswer, r.score ]); const csv = [header, ...rows].map(arr => arr.map(v => `"${v}"`).join(",")).join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "paiq_results.csv"; a.click(); }

参数说明:加\uFEFF是为了让Excel正确识别UTF-8编码,避免中文乱码。这个方案不依赖后端,适合一百人以内的小规模团队自测,数据量大了建议还是落数据库。导出字段可以根据需要增减,但至少留时间戳,否则后续没法按测试轮次过滤。

4.4 静态部署与跨域问题

写好的HTML和JSON不能直接双击打开,因为浏览器会因跨域限制拦截fetch请求。常见做法是在项目目录起一个本地静态服务:

python -m http.server 8080

然后浏览器访问http://localhost:8080。如果团队要用内网访问,把服务绑定到局域网地址即可。部署阶段要注意JSON文件的路径,建议用相对路径而非绝对路径,这样目录变了不用改代码。

5. paIQ 测试文档使用避坑:从格式损坏到答案争议的 5 个实战记录

这个方案跑起来后,场景又杂又乱。下面这几条都是实际过程中反复遇见的,每条都按现象、原因、解决给你拆开。

5.1 转换与提取时最容易翻车的三件事

现象一:python-docx打开.doc直接报错。
原因很简单,.doc是二进制格式,python-docx只支持.docx。解决方法是先走LibreOffice转换,不要想着直接读。如果转换时报“无权限”或“文件被占用”,就是Word还开着文档,关掉再转。

现象二:转换后题目顺序丢失,出现乱序。
原文档里如果用文本框或分页符分隔题目,python-docx遍历正文段落时,文本框内容会混在正常段落后面,导致抽题时ID顺序错乱。解决方法是先另存为规范格式,把文本框全部删除,再用脚本解析;或者直接改用表格方案,因为表格的行顺序更可靠。

现象三:答案提取率低于50%,大量题目没有answer字段。
我遇到过答案写在文档末尾的独立答案页,而不是每题下方。段落到段落的解析方式看不到最后一页的映射。解决方法是先读取答案页的表格或列表,按题号建立映射字典,再回头补到每道题里。这一步建议在2.2的脚本里加一个回溯逻辑,而不是人工手动填。

5.2 答案与评分规则上的两个大坑

现象四:多选题判分时,用户选了AB,正确答案是ABC,程序给了50分,但业务方认为应该0分。
原因是我们默认“漏选得半分”,但业务方期望的是“完全匹配才得分”。这类规则分歧最好在测试上线前确认清楚,把评分规则配置到前端页面上,让使用方在发起测试前就能看到。不要因为“大家默认都一样”而省这一步。

现象五:题库泄露,候选人第二次测试分数虚高。
静态题库抽题范围小,曝光后很容易被背题。解决方法是维护多套平行卷,或者用下一章讲的动态生成引擎。同时记录每次测试的题目ID集合,出现两次测试完全重叠的情况就需要排查是否泄题。

6. 进阶:用模板参数生成平行题,让 paIQ 不再是一潭死水

最后一招,也是最值得投入的方向:把静态题库变成动态生成引擎。paIQ测试题如果一直用同一套题,一个月后答案就会满天飞。常见做法是给题干里数字、变量名、场景上下文做模板化,每次测试生成不同变体。

具体套路是给题干加占位符。比如原题是“给定一个数组,找出出现次数超过2次的元素”,可以把“2次”抽成{threshold},把“数组”抽成{data_type}。然后用Python的字符串模板生成新题:

from string import Template template = Template("给定一个${data_type},找出出现次数超过${threshold}次的元素") new_stem = template.substitute(data_type="列表", threshold=3)

新题的答案也要联动替换。如果是代码题,参考答案里的数字阈值必须跟着变。如果手动维护很容易漏,建议把变体规则和答案放在同一个配置项里。另外,每道生成的新题都计算一个MD5指纹,存到题目库里防止重复。我常用的做法是取“题干模板名称+阈值+data_type”拼起来做哈希,命中相同哈希就直接跳过。

这个方法让我踩过一次大坑。当时我只把题干做了模板化,忘了同步修改答案中的数值,结果生成出来的平行卷里,题目问“找出出现次数超过3次的元素”,答案却还是按照2次写的,导致一批测试结果直接作废。从那以后我给自己定了个习惯:任何生成逻辑必须先跑一遍“自答验证”,让程序按照新答案把模板题做一遍,确认得分是满分再投入使用。希望这个技巧也能帮到你少走一段弯路。

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

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

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

立即咨询