Word行测题库转本地刷题系统:解析、SQLite存储与错题统计
简介这份《2025国家公务员录用考试行测常识题库及答案》面向备考国考行测的考生尤其适合常识判断模块薄弱、需要集中刷题巩固的应试人群。题库覆盖地理、历史、文化、科技、法律等多领域常识如藏历新年问候语含义、世界主要粮食出口国、行星与月球表面相似性、古代名医华佗被谁所杀、我国现存最大皇家园林等题型贴近真题风格可用于日常自测与考前查漏补缺。资源包共1个docx文件约28KB内容以题目与答案对照形式编排便于快速浏览与记忆。目前已有118人学习下载适合利用碎片时间反复练习、积累常识考点帮助考生在有限时间内提升答题准确率与知识覆盖面。1. 一份行测常识题库文档怎么变成能每天刷的本地练习系统很多人拿到「2025国家公务员录用考试行测常识题库及答案.docx」这类文档第一反应是打印出来或者丢进手机里翻着看。翻了两周就会发现常识判断这个模块的复习效率极低题目散落在几十页 Word 里做完一遍不知道自己错在哪一类下次遇到同类题还是错。真正的问题不是题不够而是这份 docx 没有被结构化。我在帮几个备考的朋友处理这类文档时摸索出一套把 Word 题库转成可检索、可统计、可反复刷的本地练习系统的做法。核心思路是先把 docx 解析成结构化数据题干、选项、答案、解析、分类再存进一个轻量数据库最后用一个本地脚本或网页做随机抽题和错题记录。整套东西不需要联网、不需要服务器一台普通笔记本就能跑。适合两类人一是想自己动手把手上题库数字化的备考者二是想给身边人做个小工具的技术人。下面从文档解析讲到刷题系统落地中间会重点讲 Word 解析里那些让人翻车的坑。2. 把 docx 拆成结构化数据解析策略与字段设计2.1 先搞清楚这份文档长什么样在写任何解析代码之前必须先把 docx 打开人工翻至少二十道题确认它的排版规律。常见的行测常识题库 docx 有几种典型结构一种是「题干 四个选项 答案 解析」连续排列选项用 A. B. C. D. 或 A、B、C、D 标记另一种是题干和选项在同一段答案单独放在文末的答案表里还有一种是每道题用表格框起来题干在合并单元格里。这三种结构的解析难度完全不同。第一种最好处理按行切分就能拿到大部分字段第二种需要把答案表和题目做关联容易错位第三种要处理 Word 表格的合并单元格python-docx 读出来的行列索引会和你看到的不一样。我一般会先用一段脚本把文档的段落结构和表格结构 dump 出来看清楚再动手from docx import Document doc Document(行测常识题库.docx) # 打印前 60 个段落的文本和样式判断题目排版规律 for i, para in enumerate(doc.paragraphs[:60]): text para.text.strip() if text: print(f[{i}] style{para.style.name!r} | {text[:80]}) # 单独看表格数量和每个表格的行列数 print(表格总数:, len(doc.tables)) for ti, table in enumerate(doc.tables): print(f表格{ti}: {len(table.rows)} 行 x {len(table.columns)} 列)这段代码的作用是「侦察」段落样式能帮你判断题干和选项是不是用了不同的样式比如题干是 Heading 或加粗选项是正文表格信息能告诉你题目是不是被框在表格里。参数上doc.paragraphs只返回顶层段落表格内的段落不会出现在这里所以必须同时检查doc.tables。如果发现段落数很少但表格很多说明题目主要在表格里解析逻辑要围绕表格写。2.2 用正则把题干、选项、答案、解析拆开确认结构后核心工作是用正则表达式做字段抽取。假设文档是「题干一行、选项四行、答案一行、解析一行」的规整结构可以这样写import re # 匹配选项行A. xxx / A、xxx / Axxx OPTION_RE re.compile(r^([A-D])[\.、]\s*(.)$) # 匹配答案行答案B / 正确答案B / 【答案】B ANSWER_RE re.compile(r(?:正确)?答案[:]\s*([A-D])) # 匹配解析行解析xxx / 【解析】xxx ANALYSIS_RE re.compile(r(?:解析|【解析】)[:]?\s*(.)) def parse_paragraphs(paragraphs): questions [] current None for para in paragraphs: text para.strip() if not text: continue ans_match ANSWER_RE.search(text) ana_match ANALYSIS_RE.search(text) opt_match OPTION_RE.match(text) if ans_match: if current: current[answer] ans_match.group(1) continue if ana_match: if current: current[analysis] ana_match.group(1) continue if opt_match: if current: current[options][opt_match.group(1)] opt_match.group(2).strip() continue # 既不是选项也不是答案视为新题干 if current: questions.append(current) current {stem: text, options: {}, answer: , analysis: } if current: questions.append(current) return questions逻辑说明逐段扫描遇到答案行就回填到当前题目遇到选项行就按字母存进字典遇到解析行就存解析剩下的非空行当作新题干并触发上一题的收尾。这里的关键参数是三个正则的容错范围——[\.、]覆盖了英文句点、中文顿号和全角句点三种选项分隔符(?:正确)?答案覆盖了「答案」和「正确答案」两种写法。如果你的文档里选项是「A」这种全角符号不加进字符集就会漏匹配题目会被误判成新题干这是最常见的翻车点。2.3 字段设计为后面的错题统计留好接口解析出来的数据不要只存题干和答案字段设计直接决定后面能不能做分类统计。我一般会保留这几个字段字段名类型用途id整数唯一标识错题记录靠它关联stem字符串题干原文options字典A-D 四个选项answer字符串正确选项字母analysis字符串解析文本category字符串常识子类如法律、地理、历史source字符串来源标记便于多份题库合并category这个字段文档里通常没有需要自己补。有两种补法一是维护一个关键词到类别的映射表比如题干里出现「宪法」「刑法」就归到法律二是解析完后人工过一遍给每道题打标签。前者快但粗糙后者慢但准。我一般先用关键词映射跑一遍再对没匹配上的题人工补几百道题的规模一两个小时能搞定。这个字段是后面「按类别刷错题」的基础值得花时间。3. 存进 SQLite让题库可检索、可增量更新3.1 建表和写入的最小实现结构化数据拿到后存成 JSON 也能用但一旦要做「查所有法律类错题」「统计各类别正确率」这类查询SQLite 的优势就出来了。它不需要装服务一个文件就是整个数据库备份和迁移都方便。import sqlite3 import json def init_db(db_pathquiz.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, stem TEXT NOT NULL, options TEXT NOT NULL, -- JSON 字符串 answer TEXT NOT NULL, analysis TEXT, category TEXT DEFAULT 未分类, source TEXT DEFAULT , UNIQUE(stem) -- 题干去重防止重复导入 ) ) conn.execute( CREATE TABLE IF NOT EXISTS wrong_log ( qid INTEGER, wrong_count INTEGER DEFAULT 1, last_wrong_at TEXT, PRIMARY KEY (qid) ) ) conn.commit() return conn def insert_questions(conn, questions, source2025常识题库): cur conn.cursor() inserted 0 for q in questions: try: cur.execute( INSERT INTO questions (stem, options, answer, analysis, source) VALUES (?,?,?,?,?), (q[stem], json.dumps(q[options], ensure_asciiFalse), q[answer], q.get(analysis, ), source) ) inserted 1 except sqlite3.IntegrityError: # 题干重复跳过 pass conn.commit() print(f新增 {inserted} 题跳过 {len(questions) - inserted} 题)逻辑说明questions表用UNIQUE(stem)做去重这样同一份题库重复导入不会产生重复题多份题库合并时也能自动过滤交叉题目。options存成 JSON 字符串而不是拆成四列是因为选项数量固定但内容长度差异大JSON 更灵活。wrong_log表单独记录错题用qid关联wrong_count累计错误次数后面按错误次数排序就能优先刷高频错题。参数上ensure_asciiFalse保证中文正常存储不加这个参数查出来会是转义字符。3.2 增量更新题库改版了怎么办题库文档经常会有修订版比如「2025国家公务员录用考试行测常识题库及答案.docx」出了个补充版。这时候不能整个库删了重来因为错题记录会丢。正确做法是靠UNIQUE(stem)做增量插入新题自动进库老题自动跳过错题记录不受影响。但有个细节要注意如果修订版修改了某道题的答案或解析UNIQUE约束会导致新版本被跳过库里还是旧答案。这种情况需要额外处理def upsert_question(conn, q, source): cur conn.cursor() cur.execute(SELECT id, answer, analysis FROM questions WHERE stem ?, (q[stem],)) row cur.fetchone() if row is None: cur.execute( INSERT INTO questions (stem, options, answer, analysis, source) VALUES (?,?,?,?,?), (q[stem], json.dumps(q[options], ensure_asciiFalse), q[answer], q.get(analysis, ), source) ) else: qid, old_answer, old_analysis row if old_answer ! q[answer] or old_analysis ! q.get(analysis, ): cur.execute( UPDATE questions SET answer?, analysis?, options? WHERE id?, (q[answer], q.get(analysis, ), json.dumps(q[options], ensure_asciiFalse), qid) ) print(f题目 {qid} 答案或解析有更新) conn.commit()这段逻辑先查题干是否存在存在就比对答案和解析有变化才更新。这样既保住了错题记录又能同步修订内容。实际用的时候我会在更新前先备份一份quiz.db万一更新逻辑写错了还能回滚这是血泪经验——有次没备份一个字段映射写反几百道题的答案全被覆盖成空。4. 刷题端随机抽题、错题重刷与正确率统计4.1 命令行刷题脚本数据准备好了刷题端可以很简单。一个命令行脚本就能实现「随机抽题、即时判分、错题入库」的闭环import sqlite3 import json import random from datetime import datetime def quiz(conn, categoryNone, only_wrongFalse, n20): cur conn.cursor() if only_wrong: sql SELECT q.id, q.stem, q.options, q.answer, q.analysis FROM questions q JOIN wrong_log w ON q.id w.qid ORDER BY w.wrong_count DESC LIMIT ? cur.execute(sql, (n,)) elif category: cur.execute(SELECT id, stem, options, answer, analysis FROM questions WHERE category ? ORDER BY RANDOM() LIMIT ?, (category, n)) else: cur.execute(SELECT id, stem, options, answer, analysis FROM questions ORDER BY RANDOM() LIMIT ?, (n,)) rows cur.fetchall() right 0 for qid, stem, options_json, answer, analysis in rows: options json.loads(options_json) print(f\n{stem}) for k in sorted(options): print(f {k}. {options[k]}) user input(你的答案: ).strip().upper() if user answer: right 1 print(正确) else: print(f错误正确答案是 {answer}) print(f解析{analysis}) cur.execute(INSERT INTO wrong_log (qid, wrong_count, last_wrong_at) VALUES (?, 1, ?) ON CONFLICT(qid) DO UPDATE SET wrong_count wrong_count 1, last_wrong_at excluded.last_wrong_at, (qid, datetime.now().isoformat())) conn.commit() print(f\n本轮正确率{right}/{len(rows)} {right/len(rows)*100:.1f}%)逻辑说明only_wrongTrue时从错题表里按错误次数降序取题保证高频错题优先出现。ON CONFLICT(qid) DO UPDATE是 SQLite 的 upsert 语法答错一次wrong_count加一答对不减少——这样错题会一直保留直到你手动清理。参数n控制每轮题量我一般设 20太多会疲劳太少统计意义不强。ORDER BY RANDOM()在几千题的规模下性能没问题题量上万时建议改用随机 id 范围查询。4.2 正确率统计找出真正的薄弱类别刷了一段时间后光看单轮正确率没意义要看分类正确率。这需要把答题记录也存下来而不只是错题def category_report(conn): cur conn.cursor() cur.execute( SELECT category, COUNT(*) AS total, SUM(CASE WHEN w.qid IS NOT NULL THEN 1 ELSE 0 END) AS wrong FROM questions q LEFT JOIN wrong_log w ON q.id w.qid GROUP BY category ORDER BY wrong * 1.0 / COUNT(*) DESC ) print(f{类别:10}{题量:8}{错题数:8}{错误率}) for cat, total, wrong in cur.fetchall(): rate wrong / total * 100 if total else 0 print(f{cat:10}{total:8}{wrong:8}{rate:.1f}%)这个查询按类别聚合算出每个类别的错题占比按错误率降序排列。跑出来的结果往往和直觉不一样——很多人以为自己法律最弱实际数据可能显示地理类错误率最高。这就是把题库结构化的价值让复习决策基于数据而不是感觉。参数上LEFT JOIN保证没有错题的类别也会出现在结果里wrong * 1.0是为了避免整数除法。5. 避坑与排查Word 解析和刷题系统里最容易翻车的五件事5.1 现象解析出来的题目数量远少于实际原因通常是文档里题目在表格中而解析脚本只遍历了doc.paragraphs没处理doc.tables。Word 里用表格排版题目很常见尤其是从网页复制粘贴过来的题库。解决写一个统一的文本提取函数把段落和表格单元格里的文本都按顺序收集起来再送进解析逻辑。注意表格的读取顺序要按行遍历合并单元格会导致同一文本重复出现需要在收集后做一次去重。5.2 现象选项被误判成题干题目被拆得七零八落原因是选项的正则没覆盖文档里实际使用的分隔符。有的文档用「A」全角句点有的用「A、」有的用「A.」半角句点甚至同一份文档里混用。解决把分隔符字符集写全[\.、]三个都要有。更稳妥的做法是先统计文档里所有以单个大写字母开头的行看它们后面跟的是什么符号再针对性写正则。不要凭想象写要看实际数据。5.3 现象答案和题目对不上错位了原因是文档的答案集中放在文末解析脚本按顺序把答案表里的答案依次分配给题目但中间有题目被漏解析导致后续全部错位。解决这种情况不要用顺序分配改用题干匹配。如果答案表里带了题干关键词就用关键词去questions表里查对应题目再回填答案。如果答案表只有纯字母那只能先保证题目解析数量正确再按顺序分配并在分配后抽样人工核对前 20 题和后 20 题。5.4 现象重复导入后错题记录丢失原因是用了INSERT OR REPLACE或者先DELETE再INSERT导致题目 id 变化wrong_log里的qid成了孤儿记录。解决用UNIQUE(stem)加普通INSERT靠捕获IntegrityError跳过重复题这样已有题目的 id 不变错题记录安全。更新答案用UPDATE而不是删了重插。5.5 现象中文选项显示成乱码或转义字符原因是 JSON 序列化时没加ensure_asciiFalse或者数据库连接没设置正确的编码。解决json.dumps统一加ensure_asciiFalseSQLite 默认就是 UTF-8一般不需要额外设置但如果从其他数据库迁移过来要确认建表时的字符集。读取时用json.loads还原不要手动做字符串替换。6. 进阶把题库接进本地网页做按遗忘曲线排程的复习命令行刷题够用但如果你想长期坚持一个能在浏览器里点选答案的界面体验会好很多。不需要框架一个 HTML 文件加一个轻量后端就够。后端我一般用 Python 的http.server起一个本地服务前端用原生 JavaScript 渲染题目。关键不在于界面而在于排程逻辑。单纯随机抽题的复习效果有限更好的做法是引入间隔重复答错的题隔一天再出现答对的题间隔逐渐拉长。实现上给wrong_log表加两个字段ALTER TABLE wrong_log ADD COLUMN next_review_at TEXT; ALTER TABLE wrong_log ADD COLUMN interval_days INTEGER DEFAULT 1;每次答完题更新排程def schedule_next(conn, qid, correct): cur conn.cursor() cur.execute(SELECT interval_days FROM wrong_log WHERE qid?, (qid,)) row cur.fetchone() if row is None: if correct: return # 首次答对不入错题表 cur.execute(INSERT INTO wrong_log (qid, wrong_count, interval_days, next_review_at) VALUES (?, 1, 1, date(now, 1 day)), (qid,)) else: old_interval row[0] if correct: new_interval min(old_interval * 2, 30) # 答对间隔翻倍上限 30 天 else: new_interval 1 # 答错重置为 1 天 cur.execute(UPDATE wrong_log SET interval_days?, next_review_atdate(now, || ? || day) WHERE qid?, (new_interval, new_interval, qid)) conn.commit()逻辑说明答对时间隔翻倍最长 30 天答错重置为 1 天。next_review_at用 SQLite 的date函数算避免在 Python 里处理日期字符串。抽题时加一个条件WHERE next_review_at date(now)只取到期的题。这样每天打开系统看到的都是该复习的题而不是随机一堆。验证这套排程有没有生效可以查一下未来几天的到期题量分布SELECT next_review_at, COUNT(*) FROM wrong_log WHERE next_review_at IS NOT NULL GROUP BY next_review_at ORDER BY next_review_at;如果发现某天到期题量突然爆炸说明间隔翻倍的上限设太大了把 30 天调小到 14 天通常更合理。这个参数没有标准答案取决于你每天能投入多少时间。我自己用这套东西复习了大半年最大的教训是不要一开始就追求功能全先把「解析入库 随机刷题 错题记录」这三步跑通用起来再根据实际痛点加功能。我最初花了两周写了个带用户系统和云同步的版本结果发现每天真正用到的就是那个命令行脚本。后来把花哨功能全砍了反而坚持下来了。工具的价值在于让你每天都愿意打开它不在于它有多少功能。希望帮到你。本文还有配套的精品资源点击获取