Python解析docx题库:从非结构化文档到SQLite全文检索
2026/9/19 11:44:16 网站建设 项目流程

简介:这份2025年公共基础知识题库及参考答案(完整版)面向公务员、事业单位及各类公职考试备考人群,以单选题形式系统梳理公共基础知识高频考点,帮助考生在冲刺阶段快速自测与查漏补缺。资源包内含1个docx文档,压缩包约54KB,文档按题目、选项、参考答案与解析的结构编排,覆盖意识定义与分类、社会保障制度目的、我国人口占世界比重、社会主义改革性质、公文种类区分、科学发展观根本着眼点、辩证唯物主义学习意义、偶然性在世界历史中的作用、行政处罚种类及案件管辖等核心模块,每道题均附详细解析,便于理解命题逻辑与知识延伸。目前已有153人学习下载,适合需要集中刷题、巩固记忆或梳理知识框架的备考者使用,也可作为考前快速回顾的便携资料。

1. 从一份 docx 题库说起:为什么“完整版”三个字最值得警惕

每年考试季前后,技术群里总有人丢来一个文件名——2025年公共基础知识题库及参考答案(完整版).docx。点开一看,几百页,题目、选项、答案、解析挤在一起,格式乱得像被 Word 自动编号折磨过。很多人第一反应是“直接背”,但真正做过资料整理的人会先问一句:这份 docx 到底能不能被程序读、能不能被检索、能不能被校验?

公共基础知识这类考试,题库的价值不在“多”,而在“可定位、可去重、可核对”。一份 docx 如果只是给人看的,那它天然不适合批量处理:标题层级靠加粗而不是样式、答案混在解析里、选项用全角空格对齐、题号一会儿“1.”一会儿“第1题”。这些在肉眼阅读时无所谓,一旦要导入刷题工具、做错题本、或者比对两版题库差异,就会全部变成坑。

这一篇不讲怎么押题,讲的是把这份 docx 当成一份非结构化数据源,用一套可复现的流程把它变成结构化题库:解析、清洗、去重、校验答案、导出成可检索的格式。适合手里已经拿到 docx、想自己搭一套本地题库的考生,也适合需要批量处理同类文档的 IT 从业者。

2. 解析 docx 题库:python-docx 读取段落与表格的最小可用方案

2.1 先判断这份题库是“段落型”还是“表格型”

拿到 docx 不要急着写正则。先用最笨的办法确认结构:把文件后缀改成.zip解压,看word/document.xml里题目是以<w:p>段落存在,还是以<w:tbl>表格存在。两种结构的解析路径完全不同。

常见做法是先用python-docx把两种元素都遍历一遍,统计数量,再决定主解析逻辑。下面这段代码就是干这个的,不涉及任何业务规则,只做结构侦察。

from docx import Document doc = Document("2025年公共基础知识题库及参考答案(完整版).docx") para_count = len(doc.paragraphs) table_count = len(doc.tables) print(f"段落数: {para_count}, 表格数: {table_count}") # 打印前 30 个非空段落,观察题号、选项、答案的书写习惯 non_empty = [p.text.strip() for p in doc.paragraphs if p.text.strip()] for i, text in enumerate(non_empty[:30]): print(i, repr(text[:80]))

逻辑说明:doc.paragraphs只返回正文段落,不包含表格内的文字;doc.tables单独返回表格对象。如果table_count很大而para_count很小,说明题目在表格里,需要按行按列取;反之则按段落流处理。repr()是为了看清全角空格、制表符这些肉眼容易忽略的字符。

参数说明:Document()的入参是文件路径,路径含中文和括号没问题,但要确保文件没有被 Word 占用。如果文件很大(几百页),doc.paragraphs会一次性加载到内存,通常几十 MB 以内可以接受,超过就考虑用zipfile直接流式读 XML。

2.2 用正则切分题干、选项、答案、解析

确认是段落型之后,核心工作是把连续段落切成一道一道题。公共基础知识题库的题号格式相对固定,常见有1.1、第1题(1)几种。选项一般是A.B.C.D.,答案常见写法是答案:A【答案】A参考答案:A

下面是一个可运行的切分脚本,把每道题整理成字典。

import re from docx import Document doc = Document("2025年公共基础知识题库及参考答案(完整版).docx") lines = [p.text.strip() for p in doc.paragraphs if p.text.strip()] # 题号:行首数字 + . 、 或 题 q_start = re.compile(r"^(\d{1,4})\s*[.、.]\s*(.+)") # 选项:A. / A、 / A. opt_pat = re.compile(r"^([A-D])\s*[.、.]\s*(.+)") # 答案:答案:A / 【答案】A / 参考答案 A ans_pat = re.compile(r"(?:参考)?答案[::]\s*([A-D]+)") questions = [] cur = None for line in lines: m_q = q_start.match(line) m_opt = opt_pat.match(line) m_ans = ans_pat.search(line) if m_q: if cur: questions.append(cur) cur = {"num": int(m_q.group(1)), "stem": m_q.group(2), "options": {}, "answer": None, "analysis": ""} elif m_opt and cur: cur["options"][m_opt.group(1)] = m_opt.group(2) elif m_ans and cur: cur["answer"] = m_ans.group(1) elif cur: # 既不是题号也不是选项,归入解析 cur["analysis"] += line + "\n" if cur: questions.append(cur) print(f"共解析出 {len(questions)} 道题") print(questions[0])

逻辑说明:按行顺序扫描,遇到题号就开一道新题并把上一道收尾;遇到选项就挂到当前题的options;遇到答案行就提取答案;其余内容当作解析累加。这种“状态机”写法比一次性大正则更稳,因为题库里解析经常跨多行。

参数说明:q_start里的\d{1,4}限制题号最多四位,避免把年份“2025”误判成题号;opt_pat只认 A 到 D,如果有多选题用 A 到 F,把[A-D]改成[A-F]ans_patsearch而不是match,因为答案可能出现在行中间。

注意:如果题库是表格型,doc.paragraphs拿不到表格里的文字,需要改成遍历doc.tablesrowscells,逻辑类似,只是数据源不同。

2.3 解析结果先落盘成 JSON,别急着入库

解析完不要直接写数据库。先导出 JSON,方便肉眼抽查、版本对比和二次清洗。JSON 的字段设计要预留扩展位,比如sourcepageraw

import json with open("questions_raw.json", "w", encoding="utf-8") as f: json.dump(questions, f, ensure_ascii=False, indent=2) # 统计缺失答案和缺失选项的题目数量 no_ans = [q["num"] for q in questions if not q["answer"]] no_opt = [q["num"] for q in questions if len(q["options"]) < 2] print("缺答案:", no_ans[:20]) print("缺选项:", no_opt[:20])

逻辑说明:ensure_ascii=False保证中文正常显示,indent=2方便人工阅读。统计缺失项是质检第一步,缺答案的题往往是因为答案写成了“见解析”或者答案在表格里没被读到。

参数说明:如果题目量上万,indent=2会让文件变大,可以改成separators=(",", ":")压缩。no_opt的阈值设成 2 是因为单选题至少两个选项,低于这个数基本是解析出错。

3. 清洗与去重:把“完整版”里的重复题和脏字符处理干净

3.1 全角半角、空白字符、题号噪声的统一处理

docx 转出来的文本里,全角空格\u3000、不换行空格\xa0、零宽字符\u200b都很常见。这些字符不影响阅读,但会让去重和检索失效。清洗要放在解析之后、去重之前。

import re def clean_text(s: str) -> str: if not s: return "" # 统一空白 s = s.replace("\u3000", " ").replace("\xa0", " ").replace("\u200b", "") # 去掉题号残留和多余空格 s = re.sub(r"\s+", " ", s) # 去掉常见的解析前缀噪声 s = re.sub(r"^[【\[]?(解析|答案解析)[】\]]?[::]?", "", s) return s.strip() for q in questions: q["stem"] = clean_text(q["stem"]) q["options"] = {k: clean_text(v) for k, v in q["options"].items()} q["analysis"] = clean_text(q["analysis"])

逻辑说明:先替换特殊空白字符,再用\s+把连续空白压成一个空格。解析前缀的清理用正则去掉“解析:”“【答案解析】”这类开头,避免同一道题的解析因为前缀不同而被判为不同。

参数说明:re.sub(r"\s+", " ", s)会把换行也压掉,如果解析需要保留段落,改成re.sub(r"[ \t]+", " ", s)只压空格和制表符。

3.2 基于题干指纹的去重:SimHash 与精确哈希怎么选

题库“完整版”最常见的质量问题就是重复题。重复分两种:完全一样的,和只差几个字的。完全一样的用 MD5 精确去重就够;近似重复需要 SimHash 或编辑距离。

方法适用场景时间复杂度误判风险
MD5 精确哈希题干完全一致O(n)
SimHash题干有少量改动O(n)需设阈值
编辑距离题量小、要求高O(n²)低但慢
关键词集合 Jaccard题干长短不一O(n²)中等

题量在几千道以内,直接用 MD5 加题干归一化就够了。下面这段代码先归一化再哈希,能干掉大部分重复。

import hashlib def fingerprint(q): # 只用题干和选项,不含答案和解析 base = q["stem"] + "|" + "|".join(f"{k}:{v}" for k, v in sorted(q["options"].items())) return hashlib.md5(base.encode("utf-8")).hexdigest() seen = {} deduped = [] for q in questions: fp = fingerprint(q) if fp in seen: # 保留解析更完整的那条 old = seen[fp] if len(q["analysis"]) > len(old["analysis"]): deduped[deduped.index(old)] = q seen[fp] = q else: seen[fp] = q deduped.append(q) print(f"去重前 {len(questions)},去重后 {len(deduped)}")

逻辑说明:指纹只取题干和选项,不取答案,因为同一道题在不同版本里答案可能被修正,但题目本身是同一道。遇到重复时保留解析更长的那条,相当于做了一次“择优”。

参数说明:sorted(q["options"].items())保证选项顺序不影响指纹。如果题库里选项顺序会变,这一步很关键;如果选项顺序固定,可以省掉排序。

3.3 答案校验:用选项分布和解析关键词做交叉检查

清洗去重之后,还要检查答案是否可信。一个实用技巧是统计答案分布,如果某份题库 90% 的答案都是 A,那大概率是解析出错或者答案列错位。

from collections import Counter ans_dist = Counter(q["answer"] for q in deduped if q["answer"]) total = sum(ans_dist.values()) for k, v in ans_dist.most_common(): print(f"{k}: {v} ({v/total:.1%})") # 检查答案字母是否在选项范围内 bad = [q["num"] for q in deduped if q["answer"] and any(a not in q["options"] for a in q["answer"])] print("答案与选项不匹配的题号:", bad[:20])

逻辑说明:Counter统计各答案出现次数,正常题库四个选项分布应该大致均衡。bad列表找出答案字母不在选项里的题,这类基本可以确定是解析错位。

参数说明:多选题答案可能是ABD这种字符串,any(a not in q["options"] for a in q["answer"])会逐字母检查,适用于多选。如果答案里混入了空格或逗号,先在清洗阶段去掉。

注意:答案分布只能作为异常信号,不能作为修改依据。发现分布异常时,应该回到原始 docx 核对,而不是程序自动改答案。

4. 导出与检索:把题库变成可查询的 SQLite 和全文索引

4.1 建表:题目、选项、答案、解析的字段设计

结构化之后,落库是自然选择。SQLite 单文件、零配置,适合本地题库。表设计要兼顾查询和扩展。

CREATE TABLE questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, num INTEGER, stem TEXT NOT NULL, option_a TEXT, option_b TEXT, option_c TEXT, option_d TEXT, answer TEXT, analysis TEXT, fingerprint TEXT UNIQUE, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE INDEX idx_questions_answer ON questions(answer); CREATE INDEX idx_questions_fingerprint ON questions(fingerprint);

逻辑说明:选项拆成独立列而不是 JSON,是为了方便用 SQL 直接查“选项里包含某个关键词”的题。fingerprint加 UNIQUE 约束,插入重复题时直接报错,相当于数据库层再做一次去重。

参数说明:如果有多选题超过四个选项,把option_aoption_d扩展成option_eoption_f,或者改成单独的options表。created_at用本地时间,方便按导入批次管理。

4.2 用 Python 批量写入并开启 FTS5 全文检索

SQLite 自带的 FTS5 扩展可以做中文全文检索,虽然中文分词不如专业搜索引擎,但配合LIKE已经够用。

import sqlite3 conn = sqlite3.connect("question_bank.db") cur = conn.cursor() # 建 FTS5 虚拟表,content 指向 questions cur.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS questions_fts USING fts5( stem, analysis, content='questions', content_rowid='id' ); """) for q in deduped: try: cur.execute(""" INSERT INTO questions (num, stem, option_a, option_b, option_c, option_d, answer, analysis, fingerprint) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( q["num"], q["stem"], q["options"].get("A"), q["options"].get("B"), q["options"].get("C"), q["options"].get("D"), q["answer"], q["analysis"], fingerprint(q) )) except sqlite3.IntegrityError: pass # 指纹重复,跳过 conn.commit() # 同步 FTS 索引 cur.execute("INSERT INTO questions_fts(questions_fts) VALUES('rebuild')") conn.commit() conn.close()

逻辑说明:IntegrityError捕获指纹重复,实现幂等导入。FTS5 的content='questions'表示外部内容表,不额外存一份数据,rebuild命令重建索引。

参数说明:FTS5 默认按空格分词,中文需要配合tokenize='unicode61'或者用LIKE兜底。如果检索要求高,可以在应用层用jieba分词后再查。

4.3 三个常用查询:按关键词、按答案、按错题

落库之后,检索就是写 SQL 的事。下面三个查询覆盖最常见的刷题需求。

-- 1. 题干包含“宪法”的题 SELECT num, stem, answer FROM questions WHERE stem LIKE '%宪法%'; -- 2. 答案不是 A 的单选题,用于抽查 SELECT num, stem, answer FROM questions WHERE answer != 'A' AND length(answer) = 1 LIMIT 20; -- 3. 解析里提到“错误”的题,往往是易错题 SELECT num, stem, answer, analysis FROM questions WHERE analysis LIKE '%错误%';

逻辑说明:LIKE '%关键词%'是最直接的中文检索方式,缺点是全表扫描。题量在几万以内可以接受,再大就上 FTS5 或者外部搜索引擎。length(answer) = 1用来筛单选题,多选题答案长度大于 1。

参数说明:LIMIT 20用于抽查,正式查询去掉。如果经常按答案查,idx_questions_answer索引会生效;LIKE前置通配符用不上索引,这是 SQLite 的固有限制。

5. 进阶技巧:用脚本比对两版题库差异,定位“完整版”的增删改

5.1 基于指纹的版本 diff:新增、删除、修改三类

手里有两版 docx 时,最有价值的不是重新背一遍,而是知道哪些题变了。用指纹做集合运算,三类差异一目了然。

def load_fingerprints(json_path): with open(json_path, encoding="utf-8") as f: data = json.load(f) return {fingerprint(q): q for q in data} old = load_fingerprints("questions_2024.json") new = load_fingerprints("questions_2025.json") added = new.keys() - old.keys() removed = old.keys() - new.keys() common = new.keys() & old.keys() # 修改:题干相同但答案或解析变了 changed = [] for fp in common: if old[fp]["answer"] != new[fp]["answer"] or old[fp]["analysis"] != new[fp]["analysis"]: changed.append((old[fp], new[fp])) print(f"新增 {len(added)},删除 {len(removed)},修改 {len(changed)}")

逻辑说明:指纹只取题干和选项,所以“修改”指的是题目没变但答案或解析变了。这类差异最值得人工核对,因为可能是答案修正,也可能是录入错误。

参数说明:如果两版题库的题号变了但题目没变,指纹不受影响,这正是用指纹而不是题号做 diff 的原因。

5.2 导出成 Markdown 和 Anki 可导入格式

结构化数据最终要回到使用场景。导出 Markdown 方便阅读,导出制表符分隔的文本可以直接导入 Anki。

# 导出 Markdown with open("question_bank.md", "w", encoding="utf-8") as f: for q in deduped: f.write(f"### {q['num']}. {q['stem']}\n\n") for k, v in sorted(q["options"].items()): f.write(f"- {k}. {v}\n") f.write(f"\n**答案:{q['answer']}**\n\n") if q["analysis"]: f.write(f"解析:{q['analysis']}\n\n") # 导出 Anki 制表符格式:正面\t背面 with open("anki_import.txt", "w", encoding="utf-8") as f: for q in deduped: front = q["stem"] + "<br>" + "<br>".join(f"{k}. {v}" for k, v in sorted(q["options"].items())) back = f"答案:{q['answer']}<br>{q['analysis']}" f.write(f"{front}\t{back}\n")

逻辑说明:Markdown 用三级标题加列表,适合直接丢进笔记软件。Anki 格式用制表符分隔正反面,<br>是 Anki 支持的换行标签。

参数说明:Anki 导入时选择“制表符分隔”,字段映射为正面和背面。如果解析里有制表符,先替换成空格,否则会错列。

5.3 一个容易忽略的坑:docx 里的隐藏修订和批注

最后说一个实际处理中经常翻车的地方。有些 docx 带着修订记录或批注,python-docx默认读不到修订内容,但 Word 打开时能看到。如果题库经过多人编辑,答案可能藏在修订里。

处理办法是解压 docx,检查word/comments.xmlword/document.xml里的<w:ins><w:del>标签。如果存在修订,要么在 Word 里接受全部修订后另存,要么用lxml直接解析 XML 提取。这一步不做,解析出来的答案可能和最终版不一致,而且很难排查。

提示:批量处理前先对原始 docx 做一次“接受所有修订并另存为”,能省掉后面大量对不上的麻烦。

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

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

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

立即咨询