资讯动态

电力监理继续教育题库PPTX转SQLite全文检索与刷题工具

发布时间:2026/9/18 3:51:23 来源:尧图企业网站定制
简介本资源为2025年电力监理工程师继续教育配套题库面向电力工程监理从业人员、备考继续教育考核的监理工程师及相关施工单位技术人员帮助其在质量监督、验收评定与事故处理等环节快速查漏补缺。题库以单选、多选等题型组织覆盖电力质量监督检查程序自检→预监检→监检、DL/T5210验收范围、质量监督机构对建设单位质量行为的检查、竣工验收组织主体、抽查抽检率、监理机构全过程跟踪监督原则以及监理工程师发布违规指令后个人责任不免除、单元工程质量检验签证、合同约定标准与新颁技术标准冲突时的执行判断等内容并延伸至国有企业文件材料归档、水利水电与水电水利施工监理规范中开工前监理职责的界定。包内仅1个pptx文件约70KB按题组与答案对照编排便于随身查阅与自测。目前已有50人学习下载适合利用碎片时间对照考点逐条核对答案、梳理易错知识点。1. 题库还在 PPT 里一页页翻先把它变成能查的数据继续教育季一到手里那份「2025电力监理工程师继续教育题库含答案.pptx」就变成了负担两三百页幻灯片一页一道题答案用红色字压在选项下面想找一个「旁站监理」相关的考点只能按 CtrlF 一页页跳。更麻烦的是错题——翻到了、记住了、下次又从第一页开始。这份文件本质上是结构化数据被压进了一个演示文稿容器里题干、选项、答案、页码其实都是字段只是没有按字段存。把 PPTX 拆成题目记录用 SQLite 建中文全文索引再挂一个本地刷题页和错题复习队列整套东西一个人一天能搭完不需要服务器也不需要账号。适合两类人手里已经有这份题库、想把它用起来的监理从业者以及需要处理类似「题库类 PPTX」的开发者——同样的解析路径换个文件就能复用。下面从解析讲到索引、从刷题讲到版本维护每一步都给出能直接跑的代码和参数。2. 用 python-pptx 解析电力监理继续教育题库 PPTX解析这一步决定后面所有环节的上限。题目抽错一行索引再快也没意义。所以先把 PPTX 的文本结构摸清再谈正则。2.1 先摸清 PPTX 的文本容器文本框、占位符、组合形状、表格PPTX 在磁盘上就是一个 zip 包幻灯片正文在ppt/slides/slideN.xml文字的最小单位是a:t节点若干a:t组成a:p段落段落组成文本框。python-pptx 已经把这层封装好直接操作shape.text_frame.paragraphs就行但有几个坑必须先知道。第一个坑是 run 拆分。题干里某个词被加粗或者标了颜色在 XML 里就会被切成多个a:t如果按run遍历取文本一句话会碎成好几段。正确做法是按paragraph合并所有 run 的文本。第二个坑是形状顺序。slide.shapes的返回顺序不保证等于视觉顺序有时候答案文本框排在题干前面。必须按shape.top、shape.left排序才能还原「题干在上、选项居中、答案在下」的阅读顺序。第三个坑是容器嵌套。有些题库把选项做成组合形状或者干脆用表格排版一列题号、一列内容。组合形状要递归下钻表格要按行按单元格取文本这两类都得单独处理。2.2 抽取每页文本的最小可运行脚本# parse_pptx.py —— 把题库 PPTX 拆成「页码 文本行」保持视觉阅读顺序 from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def shape_lines(shape): 递归取出单个形状里的所有文本行 lines [] if shape.shape_type MSO_SHAPE_TYPE.GROUP: for sub in shape.shapes: # 组合形状必须递归下钻 lines.extend(shape_lines(sub)) return lines if getattr(shape, has_table, False) and shape.has_table: for row in shape.table.rows: # 表格按行拼接单元格用制表符分隔 cells [c.text.strip().replace(\n, ) for c in row.cells] if any(cells): lines.append(\t.join(cells)) return lines if shape.has_text_frame: for para in shape.text_frame.paragraphs: # 加粗/变色会把一句话拆成多个 run必须按 paragraph 合并 text .join(run.text for run in para.runs).strip() if text: lines.append(text) return lines def slide_lines(slide): 按 top/left 排序还原题干在上、答案在下的视觉顺序 shapes [s for s in slide.shapes if s.has_text_frame or s.shape_type MSO_SHAPE_TYPE.GROUP or (getattr(s, has_table, False) and s.has_table)] shapes.sort(keylambda s: (s.top or 0, s.left or 0)) out [] for s in shapes: out.extend(shape_lines(s)) return out if __name__ __main__: prs Presentation(2025电力监理工程师继续教育题库含答案.pptx) for i, slide in enumerate(prs.slides, 1): for line in slide_lines(slide): print(f{i}\t{line}) # 页码 原文落成 TSV 方便 grep 校验逻辑说明shape_lines用递归把组合形状摊平用has_table分支处理表格题slide_lines的排序键是(top, left)的 EMU 原值EMU 是 PPTX 的最小长度单位直接比大小即可不用换算成磅值。参数上唯一需要调的是排序容差——如果同一行的题干文本框和题号文本框 top 值相差几百 EMU可能出现题号跑到题干后面的情况。提示先把这个脚本的输出重定向到raw.tsv用wc -l看一眼总行数。两三百页的题库通常在 1500 到 3000 行之间行数明显偏少说明有大量文本藏在图片或 SmartArt 里。2.3 题干、选项、答案的分割正则与状态机拿到行文本之后用一套正则加一个状态机把行流切成题目对象。核心是把「新题干开始」「选项行」「答案行」三种事件区分开。import re RE_NUM re.compile(r^\s*[(]?\s*(\d{1,4})\s*[)、.]\s*(?\S)) RE_OPT re.compile(r^\s*([A-Ha-h])\s*[、.)]\s*(.)$) RE_ANS re.compile(r^\s*(?:答案|正确答案|参考答案|【答案】)\s*[:]?\s*(.)$) RE_JUDGE re.compile(r^(对|错|正确|错误|√|×|是|否|T|F)$, re.I) def normalize_answer(raw): 把各种写法归一判断题给 T/F选择题给大写字母串 raw raw.strip() if RE_JUDGE.match(raw): return T if raw.upper() in (对, 正确, √, 是, T) else F return .join(sorted({c.upper() for c in re.findall(r[A-Ha-h], raw)})) def build_questions(rows): rows 是 (页码, 文本) 的序列返回题目列表 qs, cur [], None for page, line in rows: m_ans RE_ANS.match(line) if m_ans and cur: cur[answer] normalize_answer(m_ans.group(1)) qs.append(cur); cur None # 答案出现即视为一题结束 continue m_opt RE_OPT.match(line) if m_opt and cur: cur[options].append({label: m_opt.group(1).upper(), text: m_opt.group(2).strip()}) continue m_num RE_NUM.match(line) if m_num: if cur: qs.append(cur) # 上一题没有显式答案也要落库标记待补 cur {page: page, no: int(m_num.group(1)), stem: line[m_num.end():].strip(), options: [], answer: } continue if cur and not cur[options]: cur[stem] line # 题干跨行时的补丁 if cur: qs.append(cur) return qs逻辑说明状态机靠cur指针对应「当前正在收集的题目」遇到答案行就收口。三个正则里RE_NUM用了(?\S)前瞻避免把「3.1 质量控制」这种小节标题误判成题号RE_OPT允许 A 到 H 八个选项覆盖多选RE_ANS兼容「答案」「正确答案」「【答案】」几种写法。normalize_answer先把判断题压成 T/F再用re.findall从「ABD」「A、B、D」里抠出字母并排序去重这样后面判分可以直接字符串比较。2.4 解析质量校验题号连续性、选项数、答案越界解析完不要急着入库先跑一遍自检。这类题库最常见的三类错误是答案被挂到相邻题目上、选项被拆成两行导致只剩一个选项、答案字母不在选项集合里。def validate(qs): problems [] for q in qs: labels {o[label] for o in q[options]} if q[options] and len(labels) 2: problems.append((q[no], 选项少于 2 个可能是选项跨行)) if not q[answer]: problems.append((q[no], 缺答案)) elif q[answer] not in (T, F): bad set(q[answer]) - labels if bad: problems.append((q[no], f答案 {q[answer]} 越界选项只有 {sorted(labels)})) nos [q[no] for q in qs] if nos and nos ! list(range(nos[0], nos[0] len(nos))): problems.append((0, 题号不连续中间有题目丢失)) return problems不同排版形态对应的处理策略差异很大动手前先对照一下自己手里的文件属于哪一类排版形态典型表现解析策略一页一题题干选项答案同页最省事的情况按段落顺序直接切答案统一放在最后一页前面全是题末尾一张答案表按题号 join 回题目选项挤在同一段落「A.旁站 B.巡视 C.平行检验 D.见证」用re.split按[A-H][、.]二次切分答案靠红色字体区分没有「答案」二字按run.font.color.rgb判断需逐 run 取值表格排版一列题号一列内容走has_table分支按行取注意校验脚本报出的问题不要直接删题统一导出成待确认清单人工过一遍。电力监理的题目里有不少带「不包括」「错误的是」的否定题干正则在题干跨行时容易把否定词切到下一行人工复核比自动修补稳。3. 用 SQLite FTS5 给电力监理题库建中文全文索引数据落成 JSON 之后下一步是能查。SQLite 自带 FTS5 全文索引单文件、零部署中文题干用 trigram 分词器就能实现任意长度子串匹配。3.1 表结构题目、选项、考点标签分开存把题目和选项拆成两张表是为了让选项乱序、选项级统计这些需求做起来不用改题干文本。再留一个chapter字段装考点分类比如「质量控制」「安全管理」「进度控制」「合同管理」后续按考点组卷就靠它。PRAGMA journal_mode WAL; -- 一边刷题一边导入新题时不锁库 CREATE TABLE questions ( id INTEGER PRIMARY KEY, qtype TEXT NOT NULL, -- single / multi / judge stem TEXT NOT NULL, -- 题干已去掉题号 answer TEXT NOT NULL, -- 单选 B多选 ABD判断 T/F source_page INTEGER, -- 原 PPT 页码方便回查原稿 chapter TEXT, -- 考点分类可为空 fingerprint TEXT UNIQUE -- 题干归一化哈希用于去重 ); CREATE TABLE options ( qid INTEGER NOT NULL, label TEXT NOT NULL, text TEXT NOT NULL, PRIMARY KEY (qid, label), FOREIGN KEY (qid) REFERENCES questions(id) ON DELETE CASCADE ); CREATE TABLE wrong_book ( qid INTEGER PRIMARY KEY, wrong_times INTEGER NOT NULL DEFAULT 1, last_wrong_at TEXT, next_review_at TEXT ); CREATE VIRTUAL TABLE q_fts USING fts5( stem, options_text, tokenizetrigram );fingerprint上加 UNIQUE 是关键一笔重复导入同一份文件时会自动被INSERT OR IGNORE拦掉。q_fts用独立表而不是 external content是为了让rowid直接等于questions.id查询时一句 join 就能带出答案。3.2 建表语句与批量导入脚本import sqlite3, hashlib, re def fingerprint(stem): 去掉标点空白括号只留汉字字母数字屏蔽排版差异带来的哈希漂移 return hashlib.md5(re.sub(r[^\u4e00-\u9fa5A-Za-z0-9], , stem).encode()).hexdigest() def qtype_of(q): if not q[options]: return judge return multi if len(q[answer]) 1 else single conn sqlite3.connect(dljl.db) cur conn.cursor() for q in questions: opts \n.join(f{o[label]}.{o[text]} for o in q[options]) cur.execute( INSERT OR IGNORE INTO questions(qtype,stem,answer,source_page,chapter,fingerprint) VALUES(?,?,?,?,?,?), (qtype_of(q), q[stem], q[answer], q[page], q.get(chapter), fingerprint(q[stem]))) if cur.rowcount 0: # 指纹撞车说明这题已在库里 continue qid cur.lastrowid for o in q[options]: cur.execute(INSERT INTO options(qid,label,text) VALUES(?,?,?), (qid, o[label], o[text])) cur.execute(INSERT INTO q_fts(rowid,stem,options_text) VALUES(?,?,?), (qid, q[stem], opts)) conn.commit()导入顺序有讲究先插questions拿lastrowid再插选项和 FTS 行三者用同一个 id 对齐。选项文本单独拼成options_text放进 FTS这样搜「见证取样」既命中题干也命中选项内容找「选项里提到某个术语的题」特别好用。3.3 中文分词trigram 与 jieba 预分词的取舍FTS5 默认的unicode61分词器按空白和标点切词中文整句会被当成一个 token搜「旁站」永远搜不到「旁站监理的实施要点」。三种方案的差别如下方案中文效果索引体积适用场景unicode61差整句一个 token最小纯英文题干trigram好按 3 字符滑窗切分约为原文 23 倍中文题干关键词检索首选jieba 预分词好按词切分中等需要词频排序、同义词扩展trigram 的代价是索引膨胀一份 3000 题的库大约 23 MB对本地使用完全无感。它还有个隐式限制查询串长度小于 3 时退化成全表扫描所以搜「监理」这种两字词要写成MATCH 监理*走前缀匹配或者干脆用LIKE %监理%加questions.stem普通索引兜底。3.4 查询写法按考点、按题型、按答案分布统计-- 按题干/选项关键词查trigram 支持任意长度子串 SELECT q.id, highlight(q_fts, 0, 【, 】) AS hit, q.answer FROM q_fts JOIN questions q ON q.id q_fts.rowid WHERE q_fts MATCH 旁站监理 LIMIT 20; -- 只要多选题限定考点按原稿页码排序 SELECT id, stem FROM questions WHERE qtype multi AND chapter 安全管理 ORDER BY source_page; -- 取今天该复习的错题 SELECT q.id, q.stem FROM wrong_book w JOIN questions q ON q.id w.qid WHERE w.next_review_at datetime(now) ORDER BY w.next_review_at LIMIT 30;highlight会把命中的词用指定符号包起来前端直接渲染就能看到命中位置省得自己写高亮逻辑。最后一条按答案分布统计虽然简单但很值钱正常题库四个选项的答案分布大致均匀如果某个字母占了六成以上基本可以确定解析时答案错位了回头查原 PPT 比逐题核对快得多。4. 本地跑通一个电力监理继续教育刷题与错题复习工具索引建好之后刷题界面就是一层很薄的皮。用 Flask 起三个接口足够抽题、取题、提交判分。4.1 Flask 后端抽题、判分、写入错题本from flask import Flask, jsonify, request import sqlite3, random app Flask(__name__) DB dljl.db def db(): conn sqlite3.connect(DB) conn.row_factory sqlite3.Row return conn app.get(/api/paper) def paper(): n min(int(request.args.get(n, 20)), 100) # 单次组卷上限 100防手滑 chapter request.args.get(chapter) sql, args SELECT id FROM questions WHERE 11, [] if chapter: sql AND chapter ?; args.append(chapter) rows db().execute(sql, args).fetchall() picked random.sample(rows, min(n, len(rows))) # 每次组卷都重抽破掉顺序记忆 return jsonify([r[id] for r in picked]) app.get(/api/question/int:qid) def question(qid): conn db() q conn.execute(SELECT * FROM questions WHERE id?, (qid,)).fetchone() opts conn.execute(SELECT label, text FROM options WHERE qid? ORDER BY label, (qid,)).fetchall() opts [dict(o) for o in opts] random.shuffle(opts) # 选项乱序避免背题号 return jsonify({qtype: q[qtype], stem: q[stem], options: opts}) app.post(/api/submit) def submit(): body request.get_json() conn db() q conn.execute(SELECT answer FROM questions WHERE id?, (body[qid],)).fetchone() ok sorted(body[picked].upper()) sorted(q[answer]) if not ok: conn.execute( INSERT INTO wrong_book(qid, wrong_times, last_wrong_at, next_review_at) VALUES(?, 1, datetime(now), datetime(now,1 day)) ON CONFLICT(qid) DO UPDATE SET wrong_times wrong_times 1, last_wrong_at datetime(now), next_review_at datetime(now, || (wrong_times 1) || day), (body[qid],)) conn.commit() return jsonify({correct: ok, answer: q[answer]})选型理由上用 Flask 而不是 FastAPI是因为这三个接口没有并发压力Flask 的同步模型写起来更短sqlite3.Row直接dict()就能序列化。判分用sorted()比较两边正好兼容多选答案「顺序不同但集合相同」的情况。有个容易被忽略的细节SQLite 同一条 UPDATE 里的多个赋值表达式读的都是更新前的值所以next_review_at里的wrong_times 1用的是旧值实际得到的是「错 1 次排 1 天后、错 2 次排 2 天后」的递增间隔正好符合复习规律不用额外写变量。4.2 前端最小页面题干渲染与选项乱序!doctype html meta charsetutf-8 title电力监理继续教育刷题/title div idbox/div script async function load(qid) { const q await (await fetch(/api/question/${qid})).json(); const html [h3${q.stem}/h3]; q.options.forEach(o html.push( labelinput type${q.qtype multi ? checkbox : radio} nameopt value${o.label} ${o.label}. ${o.text}/labelbr)); html.push(button onclicksubmit(${qid})提交/buttonpre idfb/pre); document.getElementById(box).innerHTML html.join(); } async function submit(qid) { const picked [...document.querySelectorAll(input[nameopt]:checked)] .map(i i.value).join(); const r await (await fetch(/api/submit, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({qid, picked}) })).json(); document.getElementById(fb).textContent r.correct ? 答对了 : 正确答案${r.answer}; } load(new URLSearchParams(location.search).get(qid) || 1); /script判断题没有选项行options是空数组前端会渲染成只有一个「提交」按钮的页面。补一句if (!q.options.length)时渲染两个固定单选项对/错即可值分别传T和F后端判分逻辑不用改。4.3 错题复习队列简化版间隔重复参数错题本不需要完整实现 SM-2按「错误次数决定下次复习间隔」的简化模型就够用参数建议如下参数建议值说明首次错题间隔1 天当天记住的往往是短期记忆间隔增长序列1、2、3、5、8 天由wrong_times 1自动推导单次复习题量2030 题超过 30 题正确率断崖式下降连续答对阈值2 次达到后从wrong_book删除避免队列无限膨胀复习流程就是每天跑一次 3.4 里那条next_review_at datetime(now)的查询把返回的 id 丢给组卷接口。答对时执行wrong_times 0并累加一个right_streak字段连对两次就DELETE FROM wrong_book WHERE qid ?。这套逻辑加起来不到二十行比背着一堆参数配置更实用。5. 题库长期维护版本比对、去重与图片题的兜底处理题库每年更新一次真正的麻烦不在第一次解析而在第二年拿到新版文件时怎么知道多了哪些题。5.1 用指纹做增量比对把新旧两版分别导入两个库文件一条 SQL 就能拉出新增题ATTACH DATABASE dljl_2024.db AS old; -- 先建好上一版的库 SELECT q.id, q.stem, q.source_page FROM main.questions q WHERE q.fingerprint NOT IN (SELECT fingerprint FROM old.questions);反过来把main和old调换位置就是被删掉的题。指纹用的是「去标点后的题干」所以题干里改一个数字、换一个标点都会被当成新题——这是刻意的取舍宁可多出几十条待确认记录也不要把一道实质改过的题悄悄判成重复。实际跑下来一份 3000 题的库年更新差异通常在 200 到 400 条之间人工过一遍只要半小时。5.2 图片题与公式题的兜底清单电力监理题库里有一类题没法纯文本处理接线图、系统图、带公式的计算题。判断方法很简单逐页统计图片形状数量from pptx.enum.shapes import MSO_SHAPE_TYPE for i, slide in enumerate(prs.slides, 1): pics [s for s in slide.shapes if s.shape_type MSO_SHAPE_TYPE.PICTURE] texts [line for line in slide_lines(slide)] if pics and len(.join(texts)) 20: # 有图且几乎没有文字 print(i, 图片题需人工补录)阈值 20 是个经验值题干加选项少于 20 个字符、同时页面上有图片基本就是纯图片题。这类题不要硬凑文本直接从索引里排除单独出一份「图片题清单」按页码人工处理比让它们在库里以残缺题干存在要干净。导出给 Excel 核对时用utf-8-sig编码否则中文列名在 Excel 里全是乱码import csv with open(题库.csv, w, newline, encodingutf-8-sig) as f: w csv.writer(f) w.writerow([题号, 题型, 题干, 选项, 答案, 页码]) for q in questions: opts | .join(f{o[label]}.{o[text]} for o in q[options]) w.writerow([q[no], qtype_of(q), q[stem], opts, q[answer], q[page]])导出之后先别关看一眼「页码」列是不是单调不减、答案列有没有空格。页码列乱序说明解析阶段形状排序出了问题答案列出现空格说明normalize_answer漏掉了某种写法比如「答案A、」这种末尾带顿号的情况——在正则末尾补一个[、,;]*就能吃掉。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价