简介:《西南油气田分公司作业许可培训考试题(单选)》是一份面向油气田一线作业人员、安全管理人员及属地监督的作业许可与风险管控考核资料,覆盖动火、受限空间、临时用电、起重、动土等高频作业场景。资源以单选试题形式呈现,共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,用无界面模式批量转 txt;antiword也能抽文本,但遇到中文排版复杂、带表格和脚注的文档时丢行率更高。下面这套转换在 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], check=True, capture_output=True, ) txt = Path(out_dir) / (Path(doc_path).stem + ".txt") return txt.read_text(encoding="utf-8")参数说明:--convert-to后面的txt:Text是 LibreOffice 的导出过滤名,写成txt有时会导出成带格式的文本;--headless必须加,有图形界面的机器不加会弹窗把转换进程卡住;capture_output=True是为了吞掉 soffice 的日志,不污染脚本输出。转换完先抽查一篇,确认每题的行结构是"题号加题干一行、每个选项一行、答案一行",正则解析才有稳定的输入。如果出现大段文字挤在一行,说明原始 doc 里用了表格排版,这时要先在 Word 里把表格转成纯文本再转。
2.3 按行解析题干、选项和答案,兼容三种题号变体
解析尽量用按行状态机,而不是一个大正则一把梭。大正则在选项和题号之间出现空行、脚注或乱码时会整体失配,报错时也定位不到具体行;按行扫描每行只判一种模式,哪一题断了改哪一行就行。
# parser.py import re def _norm_letter(ch: str) -> str: """全角 A-D 转半角,半角小写转大写,统一成 A/B/C/D""" if "A" <= ch <= "D": 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-DA-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-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"和"A"两套。答案正则把"答案:""参考答案:""标准答案"三个前缀和全角冒号一次兼容。题干跨行的处理放在最后,落单的普通行拼回上一题,Word 里换行断开的题干就不会被拆成两截。
边界情况:如果分节标题用了数字序号,比如"1、动火作业",会被题号正则误判成新题。常见做法是解析前先把这类标题行统一改成"一、动火作业"格式,或者把分节标题正则扩成同时匹配数字序号。解析完的 JSON 先不落库,走一遍 2.4 的校验再进。
2.4 落库前跑一次校验,把重题、缺选项、裸答案拦在门外
题库表结构按下面这组字段设计,组卷和统计阶段都够用:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 自增主键,组卷、判分、统计都引用它 |
| seq | int | 原文题号,只用于回查原文 |
| stem | str | 题干,已去掉首尾空白 |
| options | json | 四个选项,JSON 对象,键为 A-D |
| answer | str | 归一化后的正确答案 A-D |
| tag | str | 知识点,如"动火作业""受限空间" |
| role | str | 岗位标签,如"监护人""审批人" |
| source | str | 来源 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(f"seq={q.get('seq')} 答案缺失或异常") if len(q.get("options", {})) != 4: errors.append(f"seq={q.get('seq')} 选项数={len(q.get('options', {}))}") if q["stem"] in stems: errors.append(f"seq={q.get('seq')} 题干重复") stems.add(q["stem"]) return errors if __name__ == "__main__": bank = json.load(open(sys.argv[1], encoding="utf-8")) errors = validate(bank) print(f"total={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(f"unknown 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():.", "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-DA-D]", raw) if not m: return None letter = m.group(0) return chr(ord(letter) - 0xFEE0) if "A" <= letter <= "D" else letter.upper()这条正则故意不限定"答案"二字,前提是它只在答案行上调用——在这之前,题面行和选项行已经被 2.3 的 continue 分流走了。提取不到就返回 None,validate会把这道题判为"答案缺失"而不是静默放过,入库前保证有回报。
5.3 一个命令跑完全库 sanity check
python validate.py question_bank.json | tee check_report.txtvalidate.py复用 2.4 的校验函数,输出总题数、异常题数、前 50 条错误明细。批次入库前的标准是 check_report.txt 里 errors=0 再执行 INSERT;如果一批题异常占比超过 2%,大概率不是答案写错,而是 2.2 的文本转换丢了行,回到转换步骤重新导,比逐题手改快得多。把这条命令写进题库更新流程的 Makefile 或内网 CI 定时任务里,以后每次题库变更都会先过一遍门禁,脏数据进不了生产库。
本文还有配套的精品资源,点击获取