☰
Word行测题库转本地刷题系统:解析、SQLite存储与错题统计
2026/10/10 20:28:34 网站建设 项目流程

简介:这份《(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 / A.xxx 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_path="quiz.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, source="2025常识题库"): 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_ascii=False), 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_ascii=False保证中文正常存储,不加这个参数查出来会是转义字符。

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_ascii=False), 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_ascii=False), qid) ) print(f"题目 {qid} 答案或解析有更新") conn.commit()

这段逻辑先查题干是否存在,存在就比对答案和解析,有变化才更新。这样既保住了错题记录,又能同步修订内容。实际用的时候,我会在更新前先备份一份quiz.db,万一更新逻辑写错了还能回滚,这是血泪经验——有次没备份,一个字段映射写反,几百道题的答案全被覆盖成空。

4. 刷题端:随机抽题、错题重刷与正确率统计

4.1 命令行刷题脚本

数据准备好了,刷题端可以很简单。一个命令行脚本就能实现「随机抽题、即时判分、错题入库」的闭环:

import sqlite3 import json import random from datetime import datetime def quiz(conn, category=None, only_wrong=False, n=20): 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_wrong=True时从错题表里按错误次数降序取题,保证高频错题优先出现。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_ascii=False,或者数据库连接没设置正确的编码。

解决:json.dumps统一加ensure_ascii=False;SQLite 默认就是 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_at=date('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 天通常更合理。这个参数没有标准答案,取决于你每天能投入多少时间。

我自己用这套东西复习了大半年,最大的教训是:不要一开始就追求功能全,先把「解析入库 + 随机刷题 + 错题记录」这三步跑通,用起来,再根据实际痛点加功能。我最初花了两周写了个带用户系统和云同步的版本,结果发现每天真正用到的就是那个命令行脚本。后来把花哨功能全砍了,反而坚持下来了。工具的价值在于让你每天都愿意打开它,不在于它有多少功能。希望帮到你。

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

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

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

立即咨询