资讯动态

Python解析docx题库:从非结构化文档到SQLite全文检索

发布时间:2026/9/19 11:44:17 来源:尧图企业网站定制
简介这份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_pat用search而不是match因为答案可能出现在行中间。注意如果题库是表格型doc.paragraphs拿不到表格里的文字需要改成遍历doc.tables的rows和cells逻辑类似只是数据源不同。2.3 解析结果先落盘成 JSON别急着入库解析完不要直接写数据库。先导出 JSON方便肉眼抽查、版本对比和二次清洗。JSON 的字段设计要预留扩展位比如source、page、raw。import json with open(questions_raw.json, w, encodingutf-8) as f: json.dump(questions, f, ensure_asciiFalse, indent2) # 统计缺失答案和缺失选项的题目数量 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_asciiFalse保证中文正常显示indent2方便人工阅读。统计缺失项是质检第一步缺答案的题往往是因为答案写成了“见解析”或者答案在表格里没被读到。参数说明如果题目量上万indent2会让文件变大可以改成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_a到option_d扩展成option_e、option_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, contentquestions, content_rowidid ); ) 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 的contentquestions表示外部内容表不额外存一份数据rebuild命令重建索引。参数说明FTS5 默认按空格分词中文需要配合tokenizeunicode61或者用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, encodingutf-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, encodingutf-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, encodingutf-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.xml和word/document.xml里的w:ins、w:del标签。如果存在修订要么在 Word 里接受全部修订后另存要么用lxml直接解析 XML 提取。这一步不做解析出来的答案可能和最终版不一致而且很难排查。提示批量处理前先对原始 docx 做一次“接受所有修订并另存为”能省掉后面大量对不上的麻烦。本文还有配套的精品资源点击获取

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

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

免费获取报价