ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实现作业许可考试单选题抽取与题库数字化

Python实现作业许可考试单选题抽取与题库数字化 简介《西南油气田分公司作业许可培训考试题单选》是一份面向油气田一线作业人员、安全管理人员及属地监督的作业许可与风险管控考核资料覆盖动火、受限空间、临时用电、起重、动土等高频作业场景。资源以单选试题形式呈现共1个doc文档约45KB题量紧凑、便于自测。目前已有63人学习适合用于岗前培训、持证复审或班组安全学习。内容不仅包含作业许可签发人职责、许可证交接流程、基础JSA表修订等管理要求还细化了气体检测距离、机械开挖间距、打磨机护罩间隙、临时架空线高度、链条磨损报废标准等具体数值并涉及触电急救、电气火灾灭火、防毒替代等应急处置常识。通过逐题练习可帮助读者快速掌握作业许可审批链条、风险削减措施顺序及现场安全技术交底要点强化对《风险作业管理目录》评审周期和隔离方案编制监督等制度的理解是一份贴近现场实际的安全考核练习题。1. 一张作业许可培训考试单选 doc先想清楚该变成什么西南油气田分公司作业许可培训考试题_单选.doc 这个文件名在培训管理员手里是一张待打印的卷子在信息化工程师手里就是一批待清洗的结构化数据。作业许可培训考试覆盖动火、受限空间、高处、临时用电等特殊作业每道单选题背后都对应着安全规范和企业制度里的硬性条款比如气体检测合格值、票证有效期、监护人能否离岗。把这些 doc 里的题抽成带知识点标签的 JSON由系统按岗位自动组卷、机考判分、按知识点统计正确率一次性的考试才能变成可持续改进的培训数据闭环。下面按抽题、组卷、判分、分析四个环节把落地这条链路的踩坑点和可复现代码拆开讲。2. 用 Python 把 doc 里的作业许可单选题抽成结构化题库作业许可培训考题通常混着单选、多选、判断三类。单选是其中最值得先数字化的题形答案唯一、机器判分零成本、题干加四个选项加一个答案的结构稳定适合先把整条数据链路跑通。但 doc 里的单选排版并不省心题号有 1. 1、 1 三种写法答案标记从答案A到参考答案: A都有解析脚本必须先做归一化否则后面组卷和分析全建立在脏数据上。2.1 单选是题库数字化的最佳起点也是数据陷阱最少的一块多项选择题的判分规则在企业内部往往不统一漏选算不算错、多选怎么扣不同培训管理人员给的口径不一样判断题只有两个干扰项区分度低统计价值有限。这两类题的清洗和统计成本都比单选高所以常见做法是第一期只做单选项把库表结构、组卷、判分、报表整条链路跑通二期加多选和判断时复用同一张表只在题型字段里加枚举值。单选项还有一个要写进系统说明的边界它验证的是再认能力即看到正确表述能认出来不能替代实操考核和现场答辩。作业许可培训考试如果只有机考单选安全管理部门是不认的。系统里要在考试结果页保留实操考核字段机考成绩只是记录链的一环这一点在需求评审时就要说清楚避免后续返工。提示单选机考只覆盖知识记忆层动火、受限空间这类高风险作业最终要靠实操评定考试系统不要把合格两个字写得太满。2.2 老格式 .doc 不能直接交给 python-docx先用 LibreOffice 转纯文本.doc是 Word 97-2003 的二进制格式python-docx 只认.docx直接读会报错或读到乱码。常见做法是在服务器上装 LibreOffice用无界面模式批量转 txtantiword也能抽文本但遇到中文排版复杂、带表格和脚注的文档时丢行率更高。下面这套转换在 Linux 内网机上可以直接用。# 批量把 doc_dir 下的 .doc 转成 utf-8 纯文本输出到 txt_out/ soffice --headless --convert-to txt:Text --outdir txt_out ./doc_dir/*.doc# convert.py import subprocess from pathlib import Path def doc_to_txt(doc_path: str, out_dir: str txt_out) - str: 用 LibreOffice 无界面模式把 .doc 转成纯文本返回内容。 subprocess.run( [soffice, --headless, --convert-to, txt:Text, --outdir, out_dir, doc_path], checkTrue, capture_outputTrue, ) txt Path(out_dir) / (Path(doc_path).stem .txt) return txt.read_text(encodingutf-8)参数说明--convert-to后面的txt:Text是 LibreOffice 的导出过滤名写成txt有时会导出成带格式的文本--headless必须加有图形界面的机器不加会弹窗把转换进程卡住capture_outputTrue是为了吞掉 soffice 的日志不污染脚本输出。转换完先抽查一篇确认每题的行结构是题号加题干一行、每个选项一行、答案一行正则解析才有稳定的输入。如果出现大段文字挤在一行说明原始 doc 里用了表格排版这时要先在 Word 里把表格转成纯文本再转。2.3 按行解析题干、选项和答案兼容三种题号变体解析尽量用按行状态机而不是一个大正则一把梭。大正则在选项和题号之间出现空行、脚注或乱码时会整体失配报错时也定位不到具体行按行扫描每行只判一种模式哪一题断了改哪一行就行。# parser.py import re def _norm_letter(ch: str) - str: 全角 A-D 转半角半角小写转大写统一成 A/B/C/D if ch : return chr(ord(ch) - 0xFEE0) return ch.upper() def parse_questions(text: str) - list[dict]: text text.replace(\u3000, ).replace( , ) text re.sub(r\n\s*[一二三四五六七八九十]、.*\n, \n, text) # 去掉分节标题 qs, cur [], None for line in text.splitlines(): line line.strip() if not line: continue m re.match(r^(?:\s*)?(\d)(?:\s*)?[\.、]?\s*(.)$, line) if m: if cur: qs.append(cur) cur {seq: int(m.group(1)), stem: m.group(2).strip(), options: {}, answer: None} continue m re.match(r^([A-D-])\s*[\.、:]\s*(.)$, line) if m and cur: cur[options][_norm_letter(m.group(1))] m.group(2).strip() continue m re.match(r^(?:答\s*案|参考答案|标准答案)\s*[:]\s*[(]?\s*([A-Da-d-]), line) if m and cur: cur[answer] _norm_letter(m.group(1)) continue if cur and cur[stem]: cur[stem] line # 题干跨行拼回上一题 if cur: qs.append(cur) return qs逻辑说明判定顺序是题号、选项、答案、跨行题干顺序不能换——A. 开头的行不会被题号正则匹配选项不会被误判成新题。选项行和答案行里出现的全角字母由_norm_letter统一转成半角大写避免字典键出现和A两套。答案正则把答案参考答案:标准答案三个前缀和全角冒号一次兼容。题干跨行的处理放在最后落单的普通行拼回上一题Word 里换行断开的题干就不会被拆成两截。边界情况如果分节标题用了数字序号比如1、动火作业会被题号正则误判成新题。常见做法是解析前先把这类标题行统一改成一、动火作业格式或者把分节标题正则扩成同时匹配数字序号。解析完的 JSON 先不落库走一遍 2.4 的校验再进。2.4 落库前跑一次校验把重题、缺选项、裸答案拦在门外题库表结构按下面这组字段设计组卷和统计阶段都够用字段类型说明idint自增主键组卷、判分、统计都引用它seqint原文题号只用于回查原文stemstr题干已去掉首尾空白optionsjson四个选项JSON 对象键为 A-Danswerstr归一化后的正确答案 A-Dtagstr知识点如动火作业受限空间rolestr岗位标签如监护人审批人sourcestr来源 doc 文件名便于溯源# validate.py import json, sys def validate(bank: list[dict]) - list[str]: errors, stems [], set() for q in bank: if q.get(answer) not in (A, B, C, D): errors.append(fseq{q.get(seq)} 答案缺失或异常) if len(q.get(options, {})) ! 4: errors.append(fseq{q.get(seq)} 选项数{len(q.get(options, {}))}) if q[stem] in stems: errors.append(fseq{q.get(seq)} 题干重复) stems.add(q[stem]) return errors if __name__ __main__: bank json.load(open(sys.argv[1], encodingutf-8)) errors validate(bank) print(ftotal{len(bank)} errors{len(errors)}) for e in errors[:50]: print(e)说明answer校验放在最前解析器没接住答案的题直接报错题干去重用 set同一份 doc 内的重复题当场拦住跨 doc 合并题库时把 stem 的哈希建索引再比对一遍即可。校验不过的题单独丢进repair.md人工修不混进正式题库——组卷时random.sample抽到废题会让整卷直接作废这个入口宁可严一点。3. 按岗位权重与风险场景组卷作业许可在线考试的抽题实现作业许可考试跟通用知识竞赛不一样考生是带着岗位去现场签票、监护、审批的人同样是 50 道单选监护人和审批人应考的侧重点完全不同。卷子如果从全库均匀随机抽可能出现一个监护人抽到一堆临时用电题、只碰到两三道动火题的情况考完分数再高也说明不了问题。常见做法是把题库在知识点、岗位、难度三个维度上打标组卷时按岗位权重做分层随机。3.1 三个组卷维度知识点、岗位角色、难度知识点维度直接沿用特殊作业分类动火作业、受限空间作业、高处作业、临时用电、盲板抽堵、断路作业、动土作业、吊装作业外加一个通用管理兜底。岗位维度常见分四类作业申请人、作业负责人、监护人、审批人每个岗位签的票和承担的职责不一样考题配比就该不一样。难度维度按题目性质分三档记忆型条款原文设空、参数型气体检测合格值、票证有效期这类数字、场景判断型给一段作业描述判断违章点其中参数型和场景判断型区分度高是拉开及格率的关键。打标可以半自动题干里出现有效期检测浓度监护人这类高频词就先自动打上候选标签再由培训管理员抽检修正。这里有一个很实际的坑知识点标签如果有的写动火、有的写动火作业后面 4.1 的聚合 SQL 会把同一类题拆成两行正确率直接失真。入库时对 tag 字段做枚举校验写死的可选值列表放在配置表里不允许自由输入。3.2 用带权重的分层随机抽题避免一套卷子考偏# paper.py import random ROLE_WEIGHTS { # 角色: 知识点 - 占50题的百分比 监护人: {动火作业: 30, 受限空间: 25, 高处作业: 15, 临时用电: 10, 盲板抽堵: 10, 通用管理: 10}, 审批人: {动火作业: 20, 受限空间: 18, 高处作业: 10, 临时用电: 8, 盲板抽堵: 8, 通用管理: 36}, } def build_paper(bank: list[dict], role: str, total: int 50) - list[dict]: if role not in ROLE_WEIGHTS: raise ValueError(funknown role: {role}) picked [] for tag, percent in ROLE_WEIGHTS[role].items(): num round(total * percent / 100) pool [q for q in bank if q[tag] tag and q[answer] in (A, B, C, D)] if len(pool) num: raise ValueError(f{tag} 题库不足: 需要 {num} 题, 实际 {len(pool)} 题) picked random.sample(pool, num) random.shuffle(picked) return picked逻辑说明按角色权重字典先算出每个知识点抽几题再从对应池子里random.sample不重复抽取最后整体random.shuffle避免相邻题目来自同一章节。参数说明round带来的取整误差会让各知识点的题数加起来略小于 total常见做法是把差值补到最后那个兜底知识点上题库不足时直接抛异常而不是静默少抽这样题库管理员能第一时间知道哪个知识点需要补题。想控难度的话把 pool 按难度分成三个桶按 6:3:1 的比例逐桶抽整卷难度就基本稳定了。3.3 判分只存 question_id 和作答选项不缓存整道题机考成绩落库常见两套口径。第一套是把整张卷子的题面、选项、答案连同考生作答存成一条 JSON 大字段出分快、复核方便但按知识点统计时要到 JSON 里扒数据而且题库修正答案后历史成绩会把错题记成对的。第二套是明细表只存 exam_id、question_id、selected、is_correct 四个字段题面在报表时实时 join 题库。作业许可考试要应对安全监管追溯常见做法是两套并存exam_snapshot 存 JSON 快照当证据exam_detail 存 ID 供统计。# grading.py def grade_and_log(paper: list[dict], answers: dict[int, str], conn, exam_id: int) - dict: rows [] for q in paper: sel answers.get(q[seq], ).upper() rows.append((exam_id, q[id], sel, 1 if sel q[answer] else 0)) conn.executemany( INSERT INTO exam_detail(exam_id, question_id, selected, is_correct) VALUES (?, ?, ?, ?), rows, ) return {total: len(rows), correct: sum(r[3] for r in rows)}说明answers的键是考生页面上看到的题号换算时用 q[seq] 对齐落库用 q[id]因为 seq 在单份 doc 内唯一、跨 doc 会重复不能当主键。is_correct在写入时就判定完后面 4.1 的聚合 SQL 直接 SUM 这一列不需要临时再对答案。考生没答的题按 0 分计但要单独记一个 selected 为空串方便和无作答记录区分开。3.4 合格线、补考与补考卷的难度等值作业许可培训的合格分数线常见设为 80 分也就是 50 题答对 40 题但这只是常见企业口径具体由分公司制度定系统里做成配置项而不是写死。补考要处理难度等值的问题补考卷如果从同一个题库里完全重抽难度会随机浮动抽到简单卷的考生相对占便宜。常见做法是组卷时记录整卷平均难度补考卷与正考卷的平均难度差控制在 ±0.05 以内超了就重新抽。配置项常见默认值说明合格线80 分50 题答对 40 题按分公司制度配置补考次数1 次设置成可配置项超过进入下一期重训正考与补考难度差≤ ±0.05整卷平均难度差先跑一个月看分布再调难度差的 0.05 不是行业标准是内网考试系统里常用的工程口径。落地时先按 ±0.05 跑一批如果补考通过率明显高于正考就把阈值收紧到 0.03反过来放宽。这个数据要在组卷表里留字段记录不然调参时没有历史依据。4. 从成绩单反推培训短板作业许可考试正确率的三个分析口径考试结束不是终点。作业许可培训负责人真正关心的是下一轮培训资源往哪个方向投。只要 3.3 的 exam_detail 落了库三条 SQL 就能把成绩单变成培训决策依据而且每一条都能在报表系统里直接复用。4.1 按知识点聚合正确率样本量不够不参与排名SELECT q.tag, COUNT(*) AS answered, SUM(e.is_correct) AS correct, ROUND(SUM(e.is_correct) * 1.0 / COUNT(*), 3) AS accuracy FROM exam_detail e JOIN question_bank q ON q.id e.question_id WHERE e.exam_id ? GROUP BY q.tag ORDER BY accuracy ASC;说明accuracy从低到高排排在最前面的就是要优先补训的知识点answered必须一起看正确率 0.5 但只有 10 次作答和 500 次作答的 0.5 置信度完全不同。常见做法是先过滤掉answered 30的知识点再排序避免小样本误报。多班次考试结束后可以撤掉exam_id条件按季度聚合看趋势比看单场稳季度报表里也直接用它。4.2 误选分布比正确率多一层信息定位系统性错误记忆正确率只能告诉你有多少人错了误选分布能告诉你错误集中在哪个选项上。如果一道题的错误作答高度集中在同一个干扰项说明学员对某个具体概念存在系统性误解比如把受限空间的氧含量下限记成了另一个数值而不是随机瞎蒙。统计错题里每个选项被选的次数SELECT q.tag, q.stem, e.selected AS wrong_option, COUNT(*) AS picked FROM exam_detail e JOIN question_bank q ON q.id e.question_id WHERE e.exam_id ? AND e.is_correct 0 GROUP BY q.tag, q.stem, e.selected ORDER BY picked DESC LIMIT 20;参数说明is_correct 0只取错题picked降序排列前 20 行就是强干扰项清单。把这份清单交给培训课件制作人可以直接拿干扰项内容当反例做进下一期课件。这里有个工程细节exam_detail 里 selected 存的是选项字母报表需要 join 题库表把字母翻译回选项文本否则读报表的人面对一列 A、B、C、D 还得翻题。这个口径只适用单选题多选和判断的作答是个集合不是单个字母聚合方式要单独设计。4.3 与作业票证数据交叉验证培训方向是否对准现场风险正确率是卷面数据和真实作业行为之间还隔着一层。更有说服力的做法是把高错误率知识点和作业许可票证系统里的数据对齐动火作业题正确率最低就去查最近一个季度票证系统里动火作业票的审批退回率、气体检测不合格重测次数两个口径都偏高说明培训方向对只有题目错得多而现场数据正常就要怀疑题面偏离生产实际该回看题目而不是加课。分析口径数据来源输出结论知识点正确率exam_detail join question_bank下一轮重点培训方向强干扰项分析错题 selected 聚合被系统性记错的具体概念与票证数据交叉票证退回率、重测次数验证考试内容与现场风险是否一致交叉分析在实现上不复杂两个系统各自按作业类型聚合成小表再用作业类型编码字典 join 到一起。真正的难点是编码字典——培训系统里可能叫动火作业票证系统里存的是动火两套编码不一致就 join 不上结果全是空。常见做法是统一维护一份作业类型编码映射表培训系统和票证系统都引它而不是各写各的写死枚举。另一点是数据权限票证数据属于生产运行数据交叉报表通常要限制到 HSE 管理岗分析系统的账号体系要在立项时一起规划不能等报表做出来再补权限。5. doc 题库批量入库前最值得抄的三段清洗代码5.1 全角符号归一化避免同一篇题被拆成两半老 Word 导出的 txt 里全角空格、全角冒号、全角句点混着来是常态。A全角点和 A. 不归一化选项匹配会漏掉一半的题。下面是 2.3 解析前可用的增强版归一化def normalize_text_full(s: str) - str: s s.replace(\u3000, ).replace( , ) # 全角空格 for fw, hw in zip(, ABCD():.): # 全角符号转半角 s s.replace(fw, hw) s s.replace(、, .) # 分隔符统一 s re.sub(r[ \t], , s) # 连续空白压平 return s.strip()注意这个函数只用于解析阶段定位题号、选项、答案入库时题干和选项存的仍是原始文本解析用归一化副本否则题目文字被改写日后和原 doc 对不上。答案字段单独走 5.2不进这个函数。5.2 答案标记五连归一到 A-D一份 doc 里同时出现答案A参考答案:A正确答案是A标准答案 A【答案】A并不罕见用一条宽松正则就能兜住def answer_of(raw: str) - str | None: raw raw.replace(, ().replace(, )) raw raw.replace(【, ).replace(】, ) m re.search(r[A-D-], raw) if not m: return None letter m.group(0) return chr(ord(letter) - 0xFEE0) if letter else letter.upper()这条正则故意不限定答案二字前提是它只在答案行上调用——在这之前题面行和选项行已经被 2.3 的 continue 分流走了。提取不到就返回 Nonevalidate会把这道题判为答案缺失而不是静默放过入库前保证有回报。5.3 一个命令跑完全库 sanity checkpython validate.py question_bank.json | tee check_report.txtvalidate.py复用 2.4 的校验函数输出总题数、异常题数、前 50 条错误明细。批次入库前的标准是 check_report.txt 里 errors0 再执行 INSERT如果一批题异常占比超过 2%大概率不是答案写错而是 2.2 的文本转换丢了行回到转换步骤重新导比逐题手改快得多。把这条命令写进题库更新流程的 Makefile 或内网 CI 定时任务里以后每次题库变更都会先过一遍门禁脏数据进不了生产库。本文还有配套的精品资源点击获取
返回列表