news 2026/9/20 15:21:04

python-docx解析公共基础知识题库并建SQLite FTS5检索组卷

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python-docx解析公共基础知识题库并建SQLite FTS5检索组卷

简介:这份2025年公共基础知识试题题库及答案(通用版)面向备考事业单位、公务员及三支一扶等公职类考试的考生,也可作为在校生巩固常识与政治理论的练习材料。包内含1个docx文档,大小约44KB,以单选题为主,覆盖法律基础、马克思主义哲学、公文写作、历史人文与科技常识等模块,每道题均附参考答案与解析。目前已有85人学习下载,适合需要系统刷题、查漏补缺的备考者。解析部分注重法条引用与逻辑推导,例如行政处罚种类、宪法主持规则、生产关系决定因素等高频考点均给出明确依据,便于边练边记,也可用于考前集中复盘。全书共44页,题目按考点顺序排列,答案与解析紧接题后,方便对照核对。

1. 拿到《2025年公共基础知识试题题库及答案(通用版).docx》,先别急着转 PDF

同事丢过来一份《2025年公共基础知识试题题库及答案(通用版).docx》,三千多道题,问能不能做成内网里能搜、能随机抽题的小工具。多数人的第一反应是转成 PDF,或者全选复制进 Excel,结果是题干、四个选项和答案解析粘成一整段,题号乱掉,答案那一列的字母还跟选项内容串行。问题不在于工具选错了,而在于把这份文件当文档看,而不是当数据源看。

这份 2025年公共基础知识试题题库及答案(通用版)本质是一份没有 schema 的半结构化数据集:题干、选项、答案、解析之间没有字段标签,只靠段落顺序、题号写法和标点习惯这三条隐含约定来分隔。IT 从业者要干的活很明确——解析成结构化记录,校验字段完整度,落库存下来,再做关键词检索与随机组卷。

适合三类人:要把内部题库接进自测系统、小程序或内网刷题页的人;手上压着几千道题的 docx,需要批量转 JSON、Excel 或数据库的人;以及想搞清楚 python-docx 究竟能读到什么、读不到什么的人。后面就按解析、切题、落库检索、导出组卷这条路走一遍。

2. 用 python-docx 拆开 docx 题库:从 OOXML 包到段落序列

2.1 docx 不是文本文件,先 unzip 看 word/document.xml

docx 是标准 OOXML 包,本质是个 zip。知道内部结构,遇到解析异常时才知道去哪一层找原因,而不是干瞪眼。命令行里直接展开:

mkdir -p bank_raw && cd bank_raw # 中文文件名和括号都要加引号,否则 bash 会把括号当子 shell unzip -o "../2025年公共基础知识试题题库及答案(通用版).docx" -d extracted find extracted -maxdepth 2 -name "*.xml" | sort

展开后能看到word/document.xml(正文)、word/styles.xml(样式定义)、word/numbering.xml(自动编号与项目符号)、word/media/(图片)。正文里每个段落是<w:p>,段落内的文字被切成若干<w:r>。这一点很关键:答案:和后面的C常常是两个 run,因为字体或颜色不同,所以直接取整段文本反而比逐个 run 拼接更省事。

2.2 用 python-docx 把题库遍历成「序号 / 样式 / 文本」三元组

在动手写正则前,先把整份题库打印出来看一遍。这一步花的十分钟,能省掉后面几个小时的调正则。

from docx import Document doc = Document("2025年公共基础知识试题题库及答案(通用版).docx") for i, p in enumerate(doc.paragraphs): text = p.text.strip() if not text: continue # 空段落占比很高,先跳过,免得干扰观察 print(i, "|", p.style.name, "|", text[:60])

参数与行为说明:doc.paragraphs只覆盖正文 body 的直接段落,页眉页脚、文本框、以及表格单元格里的段落不在其中,表格要用doc.tables单独取。p.style.name常见取值是NormalHeading 1List Paragraph,样式名往往就是「题型分节」或「章节标题」的信号——比如以 Heading 1 出现的段落,基本就是第一章 法律常识这类分类名,可以直接当 category 用,比重正则在正文里捞分类名靠谱得多。

2.3 先给每一行贴标签:题干、选项、答案、解析的正则锚点

一份排版规范的题库,行形态就那么几种。先把锚点定下来,再谈怎么串起来。

import re RE_TYPE = re.compile(r'(单项选择题|多项选择题|判断题|填空题)') RE_STEM = re.compile(r'^\s*(\d{1,4})\s*[.、.]\s*(.+)$') RE_OPTION = re.compile(r'^\s*([A-Fa-f])\s*[.、.))]\s*(.+)$') RE_ANSWER = re.compile(r'^\s*[【\[]?\s*答\s*案\s*[】\]]?\s*[::]?\s*(.+)$') RE_ANALY = re.compile(r'^\s*[【\[]?\s*(解析|分析|答案解析)\s*[】\]]?\s*[::]?\s*(.*)$')

逐条说明设计意图:RE_STEM把题号限制在 1 到 4 位,是为了避开正文里以2025.开头的句子被误判成题干;RE_OPTION强制要求字母后面跟分隔符(.)),这样解析里出现的「B 选项表述不严谨」不会因为没有分隔符而被当成选项;RE_ANSWER里的冒号可选,因为不少题库写成「正确答案是 C。」而不用冒号;RE_ANALY允许解析正文为空,后续做完整度统计时能区分「没有解析段落」和「有段落但内容为空」两种脏数据。

下表是按常见排版整理的落位对照,调正则时按它对表检查:

行形态判定锚点落位字段典型误判
12.下列关于…的是( )^\d{1,4}[.、.]stem年份、金额开头的正文行
A.…/A、…^[A-F][.、.))]options解析中引用选项字母
【答案】C^【?答案】?[::]?answer题干里出现「答案」二字
解析:…^(解析|分析)analysis

2.4 段落之外:表格和样式里的题型信息不能丢

有些排版把题目放进表格,一行一题,四列分别是题干和 A/B/C/D。这种结构用正则处理反而绕,直接按行还原成文本再喂给同一套规则:

for ti, tbl in enumerate(doc.tables): for ri, row in enumerate(tbl.rows): cells = [c.text.strip().replace("\n", " ") for c in row.cells] if all(cells): print(f"table{ti}-row{ri}:", cells)

注意合并单元格会让row.cells出现重复内容,all(cells)这个前置判断能过滤掉大部分空行和跨页续表头。真实项目里,我一般会把表格还原的文本和普通段落按文档顺序合并成一个统一的list[str],后面只维护一套解析逻辑,避免出现两套代码各修各的 bug。

3. 把 docx 题库解析成结构化题目:状态机切题与字段落位

3.1 四个状态切题:遇题号开新题,遇答案收尾

单靠正则逐行匹配是不够的,因为题干、选项、解析都可能换行,一段文字属于哪个字段取决于它出现在什么位置。用状态机把「当前处在题干里还是解析里」记下来,未知行才有地方安放。

def parse_bank(lines: list[str]) -> list[dict]: questions, cur = [], None for idx, raw in enumerate(lines): line = raw.replace("\u3000", " ").strip() if not line: continue m_ans = RE_ANSWER.match(line) if m_ans and cur is not None and cur["answer"] is None: cur["answer"] = m_ans.group(1).strip() cur["state"] = "answer" continue m_ana = RE_ANALY.match(line) if m_ana and cur is not None: cur["analysis"] = m_ana.group(2).strip() cur["state"] = "analysis" continue m_opt = RE_OPTION.match(line) if m_opt and cur is not None and cur["state"] in ("stem", "option"): label, content = m_opt.group(1).upper(), m_opt.group(2).strip() if cur["options"] and cur["options"][-1][0] == label: # 同一标签再次出现,说明选项内容被换行切断了,拼接而非新增 cur["options"][-1][1] += content else: cur["options"].append([label, content]) cur["state"] = "option" continue m_stem = RE_STEM.match(line) if m_stem: if cur: questions.append(cur) cur = {"no": int(m_stem.group(1)), "stem": m_stem.group(2).strip(), "options": [], "answer": None, "analysis": "", "state": "stem", "src_row": idx, "category": None} continue # 兜底:不认识的行按当前状态归位,避免整题内容被吞掉 if cur is not None: if cur["state"] == "option" and cur["options"]: cur["options"][-1][1] += line elif cur["state"] == "analysis": cur["analysis"] += line elif cur["state"] == "stem": cur["stem"] += line if cur: questions.append(cur) return questions

逻辑说明:cur["answer"] is None是防止解析段落里再次出现「答案」字样把正确答案覆盖掉;state字段决定未知行的归属,选项阶段遇到不认识的短行接到最后一个选项后面,解析阶段遇到的长行接到解析后面,这是处理跨行内容最稳的做法。src_row记录原始行号,后面发现某道题解析错了,可以直接跳回 docx 对应位置核对。

3.2 单选、多选、判断、填空题的边界差异

四类题型的差异集中在两处:选项数量和答案形态。切题逻辑可以共用,答案归一化必须分开写。

题型选项特征答案形态归一化规则
单项选择题通常 4 个,偶见 3 或 5 个单个字母 A-D转大写、去空格
多项选择题4-5 个组合字母 ABC、A、B、C去分隔符、排序、去重
判断题无字母选项对/错、√/×、正确/错误、T/F统一映射为 T/F
填空题无选项,题干含下划线短语文本保留原文,仅去首尾空白

归一化函数:

import re def normalize_answer(raw: str, qtype: str) -> str: s = raw.strip().replace(" ", "").replace("\u3000", "") if qtype == "判断": return "T" if s in ("对", "正确", "√", "是", "T", "true") else "F" letters = re.findall(r"[A-Fa-f]", s) return "".join(sorted(set(l.upper() for l in letters))) or s

判断题映射成 T/F 之后,前端渲染和自动判分都不用再写「对/错/正确/错误」四五个分支;多选排序去重能同时吃下CBAA、B、CA,B,C三种写法。要注意判断题的答案行如果写成「正确答案是:错误」,正则接住的是整句,白名单匹配会落到 F 分支,看似正确,但如果原文是「不正确」这类否定表述,白名单就失效了——所以判断题的答案字段务必加一条长度校验,超过 4 个字符的一律进人工复核队列。

3.3 答案与解析分离:别让解析里的字母污染答案字段

「B 选项错误」「C 项不符合题意」这类句子在解析里极常见,如果答案正则在行首锚定不严,或者允许在任意位置匹配,多选题的答案就会被解析内容污染。前面RE_ANSWER^\s*[【\[]?\s*答\s*案从行首锚定,并且加了cur["answer"] is None的写入守卫,两道防线基本够用。

还有一类更隐蔽的情况:答案和解析写在同一行,答案:C 解析:行政复议机关是……。这时按答案正则抓到的 group(1) 会连着解析一起带出来。处理办法是在归一化之前,先用解析锚点把行切开:

line = "答案:C 解析:行政复议机关是上一级主管部门" head = RE_ANALY.split(line)[0] if RE_ANALY.search(line) else line # head 得到 "答案:C ",再交给 RE_ANSWER 抓取,答案字段就不会带尾巴

3.4 解析完先自检:四个字段完整度指标

不要等到前端接上才发现有几百道题没答案。解析完立刻跑一遍统计,用数据判断是哪一环出了问题。

import hashlib, unicodedata, re from collections import Counter def stem_hash(stem: str) -> str: s = unicodedata.normalize("NFKC", stem) s = re.sub(r"[\s\u3000]", "", s) s = re.sub(r"[(())【】\[\],。;:、,.;:!?!?]", "", s) return hashlib.md5(s.encode("utf-8")).hexdigest() def audit(questions: list[dict]) -> None: total = len(questions) no_answer = [q for q in questions if not q["answer"]] no_analysis = [q for q in questions if not q["analysis"]] odd_opt = [q for q in questions if q.get("qtype") != "判断" and len(q["options"]) not in (2, 3, 4, 5, 6)] hashes = Counter(stem_hash(q["stem"]) for q in questions) dup = [h for h, c in hashes.items() if c > 1] for name, val in [("无答案", no_answer), ("无解析", no_analysis), ("选项数异常", odd_opt), ("重复题干", dup)]: print(f"{name}: {len(val)} / {total}") audit(questions)

阈值怎么定:无答案比例超过 2%,说明切题逻辑有问题而不是数据脏,回去查state的迁移;选项数异常一般来自表格断行或分页,定位到src_row手工看一眼就能确认;重复题干超过 5%,先怀疑 stem_hash 的归一化规则太宽松,把标点剔除范围收窄再跑一次。这个audit函数后面会被反复用到,每改一次正则都要重跑。

4. 题库落库与检索:SQLite 三张表加 FTS5 的建法与导入

4.1 question / opt / tag 表结构设计

三千到三万道题这个量级,SQLite 完全够用,单文件便于分发,内网小工具直接读同一个 db 文件。

PRAGMA journal_mode = WAL; -- 读写并发更友好 PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS question ( id INTEGER PRIMARY KEY, qtype TEXT NOT NULL, -- 单选/多选/判断/填空 stem TEXT NOT NULL, answer TEXT NOT NULL, analysis TEXT, category TEXT, -- 来自卷内章节标题 stem_hash TEXT NOT NULL UNIQUE, -- 归一化题干指纹 src_row INTEGER -- 原始行号,便于回溯核对 ); CREATE TABLE IF NOT EXISTS opt ( question_id INTEGER NOT NULL REFERENCES question(id) ON DELETE CASCADE, label TEXT NOT NULL, content TEXT NOT NULL, PRIMARY KEY (question_id, label) ); CREATE TABLE IF NOT EXISTS tag ( question_id INTEGER NOT NULL REFERENCES question(id) ON DELETE CASCADE, name TEXT NOT NULL, PRIMARY KEY (question_id, name) );

stem_hash上的 UNIQUE 约束是这张表设计里最值钱的一行:有了它,重复导入、增量更新、换台机器再跑一遍,全部靠INSERT OR IGNORE天然幂等,不需要额外的去重脚本。opt(question_id, label)做联合主键,同一题同一个选项标签重复写入时自动被拦下。

4.2 批量导入:事务加 executemany,一万题两秒内跑完

import sqlite3 conn = sqlite3.connect("bank.db") conn.execute("PRAGMA foreign_keys = ON") with conn: # 退出 with 块自动 commit,抛异常自动 rollback for q in questions: cur = conn.execute( """INSERT OR IGNORE INTO question (qtype, stem, answer, analysis, category, stem_hash, src_row) VALUES (?, ?, ?, ?, ?, ?, ?)""", (q["qtype"], q["stem"], q["answer"], q["analysis"], q["category"], stem_hash(q["stem"]), q["src_row"])) if cur.rowcount == 0: continue # 撞上 UNIQUE,说明这题已在库里,跳过选项写入 qid = cur.lastrowid conn.executemany( "INSERT INTO opt(question_id, label, content) VALUES (?, ?, ?)", [(qid, label, content) for label, content in q["options"]])

参数说明:executemany比在循环里逐条execute少一次 SQL 编译,选项多的题库差距明显;with conn是事务边界,别在循环里手动 commit,那样每道题一次 fsync,速度会掉一个数量级;cur.rowcount == 0是判断是否命中了 UNIQUE 约束的标准做法,比先 SELECT 再 INSERT 少一次查询。

4.3 用 FTS5 的 trigram 分词器做中文题干检索

直接对上万条stemLIKE '%关键词%'是能出结果,但全表扫描,而且没法按相关度排序。SQLite 的 FTS5 扩展可以建全文索引,中文场景下分词器选型是关键:

分词器中文表现适用场景代价
unicode61整段中文被当成一个 token,MATCH 基本查不到纯英文/拉丁文内容索引小
trigram按三字窗口切分,中文子串匹配正常中文题干、解析检索索引体积明显增大

建表与回填:

CREATE VIRTUAL TABLE IF NOT EXISTS question_fts USING fts5( stem, analysis, content='question', -- 外部内容表模式,不重复存正文 content_rowid='id', tokenize='trigram' ); INSERT INTO question_fts(rowid, stem, analysis) SELECT id, stem, COALESCE(analysis, '') FROM question;

查询时用snippet()直接返回带标记的命中片段,前端高亮不用自己算偏移:

SELECT q.id, q.qtype, snippet(question_fts, 0, '<em>', '</em>', '…', 16) AS hit FROM question_fts f JOIN question q ON q.id = f.rowid WHERE question_fts MATCH '行政复议' ORDER BY rank LIMIT 20;

两个坑要提前避开:MATCH后面的字符串走的是 FTS5 的查询语法,用户输入里带引号、连字符、NOTOR会直接报语法错,稳妥做法是把用户输入用双引号整个包起来再拼进MATCH;外部内容表模式下,question表被 UPDATE 或 DELETE 时 FTS 索引不会自动同步,要么写触发器,要么在批量更新后执行INSERT INTO question_fts(question_fts) VALUES('rebuild')重建索引。

4.4 组卷与查重:精确哈希加候选集近似比对

精确重复用一条 SQL 就能查出来:

SELECT stem_hash, COUNT(*) AS c FROM question GROUP BY stem_hash HAVING c > 1;

近似重复(题干只改了两个字)不能靠哈希。一万道题两两比对是五千万次,不可接受,正确做法是先用 FTS 或 category 把候选集缩到几十条,再算文本相似度:

from difflib import SequenceMatcher def find_similar(conn, stem: str, category: str, threshold: float = 0.9): # 先用关键词召回调候选,再算相似度,避免全表 O(n^2) 比对 key = stem[:6] cands = conn.execute( """SELECT q.id, q.stem FROM question_fts f JOIN question q ON q.id = f.rowid WHERE question_fts MATCH ? AND q.category = ? LIMIT 50""", (f'"{key}"', category)).fetchall() return [(cid, s, SequenceMatcher(None, stem, s).ratio()) for cid, s in cands if SequenceMatcher(None, stem, s).ratio() >= threshold]

阈值 0.9 是实践里比较稳的经验值:再低会把「同一考点不同问法」的题误判成重复,组卷时抽到两道考同一知识点的题,体验反而更差。

5. 从 docx 到可分发题库:导出、随机组卷和中文题库特有的坑

5.1 导出 JSON 与 Excel:答案列必须锁成文本

导出 JSON 用一行json.dump(payload, open("bank.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)即可,ensure_ascii=False不加的话中文会变成\uXXXX,肉眼没法校对。导出 Excel 时有个必踩的坑:填空题答案如果是纯数字,openpyxl 会按数值写入,Excel 打开就变成1.23E+3或者把前导零吃掉。

from openpyxl import Workbook ws = Workbook().active for r, q in enumerate(rows, start=1): ws.cell(row=r, column=1, value=q["id"]) ws.cell(row=r, column=2, value=q["stem"]) cell = ws.cell(row=r, column=3, value=q["answer"]) cell.number_format = "@" # 文本格式,防止纯数字答案被科学计数法吞掉

5.2 随机组卷固定 seed,让同一套卷可复现

import random rng = random.Random(2025) # 固定种子,同一年同一套卷永远一样 paper = rng.sample(pool, k=100)

固定 seed 最大的价值不是「随机」,而是可复现——用户报「第 37 题答案不对」,你能立刻生成同一套卷去核对。要每次都不一样,把种子换成time.time()或者当天的日期数字。

5.3 中文题库的脏数据清单与三条验收指标

中文题库的脏数据集中在三类:全角空格\u3000混在选项里导致对齐错位;【答案】[答案]答案:三种写法混排;序号用了「(一)」「1.」这类全角或特殊字符。处理时对选项标签和答案字段做unicodedata.normalize("NFKC", s)是安全的,但不要对题干做同样的归一化——NFKC 会把题干里的全角括号一并转成半角,破坏原文排版,影响后续人工核对。

验收标准就三条,全过再谈接前端:无答案比例低于 2%;选项数与题型匹配率高于 99%;去重后题目总数与文档里最大题号偏差低于 1%。把这套检查写成 pytest 用例,每次重新解析 docx 时跑一遍,比人工抽查几百道题靠谱得多。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 15:19:58

IPC-A-620线束验收标准全解析:从压接到绝缘防护的判定要点

简介&#xff1a;IPC-A-620线束加工标准PDF文档&#xff0c;是面向电缆组装与线束加工行业从业者、质量检验人员及工艺工程师的规范性参考。资源完整呈现电缆组装的外观接受标准&#xff0c;涵盖检查与测试要求、术语定义、产品等级划分&#xff08;一般、专用、高科技电子产品…

作者头像 李华
网站建设 2026/9/20 15:18:12

SAP PI/PO REST适配器配置实战:5分钟打通同步接口的完整路径

做SAP PI/PO集成这么多年&#xff0c;每次听到"5分钟搞定XXX配置"这种说法&#xff0c;第一反应都是嗤之以鼻。但REST适配器这个场景&#xff0c;我要说句公道话&#xff1a;在准备工作到位的前提下&#xff0c;5分钟把同步接口从零配到能通&#xff0c;真不是标题党…

作者头像 李华
网站建设 2026/9/20 15:18:03

CAT 切面注解埋点实战:SpringMVC 环境下基于 AOP 的零侵入监控方案

可观测性指标监控告警APM后端链路追踪 【免费下载链接】cat CAT 作为服务端项目基础组件&#xff0c;提供了 Java, C/C, Node.js, Python, Go 等多语言客户端&#xff0c;已经在美团点评的基础架构中间件框架&#xff08;MVC框架&#xff0c;RPC框架&#xff0c;数据库框架&…

作者头像 李华
网站建设 2026/9/20 15:17:23

霍普金森杆实验数据处理程序的设计与实现

简介&#xff1a;这份资源是一篇关于霍普金森杆实验数据处理程序设计与实现的学术论文PDF&#xff0c;适合材料动力学、冲击力学及相关军事工程领域的研究人员、工程师和高年级学生阅读。内容系统阐述了SHPB实验原理、入射波/反射波/透射波的分离难点&#xff0c;并给出了基于V…

作者头像 李华
网站建设 2026/9/20 15:16:48

SQL注入实战:sqli-labs靶场6-10关盲注与文件写入技巧详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华