
简介这份《2025年公共基础知识试题题库及答案通用版》面向事业单位、公务员、社区工作者及“三支一扶”等公共基础知识备考人群可用于日常刷题、章节自测与考前查漏补缺。资源包内共1个docx文档压缩后约44KB内容以试题、参考答案和解析为主便于在电脑或手机上直接打开检索与打印。题目涉及国情省情、公文文种、声学常识、儒学思想、哲学原理、行政法与宪法、生产关系、书信格式等高频考点并配有选项辨析与解析能帮助使用者理解命题角度、巩固易错知识点。目前已有85人学习下载适合需要快速建立公共基础知识知识框架、利用碎片时间练习客观题的考生也可作为培训机构整理随堂练习的参考材料。1. 拿到一份公共基础知识题库 docx真正的活儿是把版式变成字段考试季前后做企业内训、做小程序题库、做知识竞赛系统的人多半会从别处拿到同一类文件《2025年公共基础知识试题题库及答案通用版.docx》。几百道到几千道题挤在一个文档里单选、多选、判断、填空混排答案要么跟在题后一行要么按题号缩写成「1-5 BCDAB」扔在文末解析有时独立成段有时和题干粘成一段。人的眼睛能读程序读不了。真正要做的不是打开它读而是把它变成一张有题型、有答案、有章节归属、能随机抽、能关键词检索的表。难点从来不在读文档的库本身——读段落只要三行代码——而在版式规则不统一题号可能是自动编号选项缩进可能是空格答案区可能被分页切断。适合打算自建刷题服务、要做题库数据清洗、或者需要跨年份比对题库差异的开发者。2. 拆开《2025年公共基础知识试题题库及答案通用版.docx》段落结构、题型识别与正则边界同一份 docx用 Word 打开看是「一页一页的题」用程序打开看是一串没有语义的段落。中间这层翻译做得好不好直接决定后面要不要手工改几百道题。2.1 先用 zipfile 看清 .docx 里到底存了什么docx 本质是一个 zip 包正文是 XML。动手写解析逻辑之前先列一遍包里的文件能省掉后面很多猜测。import zipfile path 2025年公共基础知识试题题库及答案通用版.docx with zipfile.ZipFile(path) as z: for info in z.infolist(): # file_size 是压缩后大小看体量能判断哪几个文件是主力 print(f{info.filename:45} {info.file_size:10})输出里通常有这几个关键成员word/document.xml是正文word/styles.xml定义样式名word/numbering.xml定义自动编号规则。最后这个是重点——如果题库的题号是 Word 的自动编号那么段落的text里根本看不到「1.」「2.」题号只存在于document.xml的w:numPr节点里。很多人第一次解析就卡在这里正文抓出来一片没有题号的句子正则当然匹配不上。对应的读取方式是把document.xml拉出来扫w:numPr建立numId → 编号格式的映射再按段落出现顺序自己累加序号。对一份题库来说编号格式基本是固定的阿拉伯数字所以更省事的做法是按段落顺序直接重排题号只在解析出的题号和文档内嵌题号冲突时才回退去查 numbering.xml。2.2 python-docx 读段落样式、缩进和那个被吃掉的题号接下来才是常规读取。这里不要只打印文本样式名和缩进量才是分类的依据。from docx import Document doc Document(path) for i, p in enumerate(doc.paragraphs[:40]): # style.name 常见值Normal、Heading 1、List Paragraph # left_indent 能区分「题干」和「选项」选项通常有缩进或首行缩进 indent p.paragraph_format.left_indent print(i, repr(p.style.name), indent, repr(p.text[:60]))几个参数的实际意义style.name为Heading 1/2的段落一般是章节标题正好可以用来给题目打上chapter标签这比事后按题号区间切分靠谱得多left_indent为None或 0 的多半是题干非零的多半是选项段落文本为空但样式特殊的往往是分页符或空行直接跳过。如果你发现某些题的题干被拆成了两段检查一下是不是中间夹了一个空的Normal段落——Word 里的手动换行和段落换行在 XML 层是不同的东西视觉上却一样。2.3 四类题型的判定规则与可用正则公共基础知识题库的题型就那么几类规则可以先写死后面再用数据回测修正。题型题干特征选项特征建议正则单选末尾带 或含「下列」「以下」A. B. C. D. 四项^\s*(\d{1,4})\s*[.、]\s*(.*)$多选含「正确的是」「包括」「属于」四项以上或明确标注多选同上靠选项数判定判断以 结尾无选项无[(]\s*[)]\s*$填空题干含___或独立括号无_{2,}落到代码上先定正则常量再写一个分类函数。import re RE_Q_NO re.compile(r^\s*(\d{1,4})\s*[.、]\s*(.*)$) RE_OPT re.compile(r^\s*([A-H])\s*[.、)]\s*(.*)$) RE_ANSWER re.compile(r^\s*(?:参考答案|答案|【答案】)\s*[:]?\s*(.*)$) RE_EXPL re.compile(r^\s*(?:解析|【解析】|答案解析)\s*[:]?\s*(.*)$) def classify(stem: str, options: dict) - str: if not options: # 判断题的括号在题干末尾填空题一般是连续下划线 return judge if re.search(r[(]\s*[)]\s*$, stem) else blank # 选项超过四个基本是多选注意有些文档多选只有四个选项 return multi if len(options) 5 else singleRE_Q_NO里限定 1 到 4 位数字是为了避开「2025 年」这种年份开头的段落被误判成题号——这是最常见的一次误匹配。RE_OPT的字符类只取 A 到 H是因为题库里选项最多到 H再往后放会把「A 类人员」这种正文误吞成选项。分类函数不依赖题干关键词只依赖选项数量是因为多选和单选的题干措辞在不同版本的题库里差别很大而选项数量相对稳定。2.4 识别失败的三种版式与兜底策略第一类是答案集中区。文档末尾经常出现「1-5 BCDAB 6-10 ACBDA」这种连续答案串题干里完全没有答案字段。写个展开函数把它还原成{题号: 答案}字典即可。def expand_answer_blob(blob: str) - dict: # 支持 1-5 BCDAB 和 1~5 BCDAB 两种分隔写法 pairs re.findall(r(\d)\s*[-–~至]\s*(\d)\s*([A-D]), blob) result {} for start, end, letters in pairs: start, end int(start), int(end) for offset, ch in enumerate(letters): # 答案串长度与题号区间不一致时多出的部分直接丢弃并记录告警 if start offset end: result[start offset] ch return result第二类是跨页断行。题干太长时 Word 会把它拆成两个段落表现为上一段没有结束标点、下一段以中文开头。加一条合并规则当前段不以。结尾且不是选项也不是答案行就把它拼到上一段末尾。第三类是选项折行。选项文字很长时会换行成两段第二段没有任何字母前缀。处理方式是记住「最后一个被识别的选项字母」把无前缀的续行追加到它后面。提示这三条兜底规则都会引入新的误判建议先跑一遍把合并前后不一致的样本导出来人工扫一眼再决定是否保留。3. 公共基础知识题库的字段设计与落库答案解析分离、去重、可回溯解析出题目对象只是中间态真正要长期用的是数据库。字段设计如果一开始就漏了回溯信息后面发现某道题解析错了你连它在原文档哪儿都找不到。3.1 一道题最少要存哪些列列名类型说明idINTEGER PK自增主键source_fileTEXT来源文件名跨版本比对必需source_indexINTEGER在原文档中的段落序号用于回溯定位qtypeTEXTsingle / multi / judge / blankstemTEXT题干已剥离选项和答案options_jsonTEXT形如{A: ..., B: ...}answerTEXT规范化后的答案多选为ABDexplanationTEXT解析正文chapterTEXT章节或模块名从 Heading 样式取stem_hashTEXT去重指纹created_atTEXTISO 格式时间source_index这一列很多人会省掉代价是后期排查时只能靠题干关键词在 Word 里搜几百道题里搜十几次就崩溃了。answer统一成大写无分隔符的字符串是为了让判分逻辑只有一个分支多选、单选、判断共用同一段比较代码。3.2 答案与解析的三种排布与状态机解析答案的排布方式大致三种紧跟在题干下方、集中在每章末尾、集中在文档末尾。前两种可以用同一个状态机处理第三种走 2.4 的答案串回填。def parse_paragraphs(paras) - list: items, cur [], None for idx, p in enumerate(paras): text p.text.strip() if not text: continue m RE_Q_NO.match(text) # 先排除选项行因为 A. xxx 不会以数字开头但 1. A 类 会 if m and not RE_OPT.match(text): if cur: items.append(cur) cur {source_index: idx, no: int(m.group(1)), stem: m.group(2), options: {}, answer: , explanation: } continue if cur is None: continue m RE_OPT.match(text) if m: cur[options][m.group(1)] m.group(2).strip() continue m RE_ANSWER.match(text) if m: cur[answer] m.group(1).strip().upper() continue m RE_EXPL.match(text) if m: cur[explanation] m.group(1).strip() continue # 续行归属有答案未解析 → 归解析已有选项 → 归最后一个选项否则归题干 if cur[answer] and not cur[explanation]: cur[explanation] text elif cur[options]: last max(cur[options]) cur[options][last] text else: cur[stem] text if cur: items.append(cur) return items状态机的核心是「续行归属」那三段判断顺序不能换。如果先判断cur[options]再判断答案那些「答案B」后面紧跟的解析文字就会被错接到最后一个选项上导致 D 选项变成「D. 以上都对解析本题考查……」这种鬼样子。反过来如果题干本身就有选项cur[options]非空续行永远进不了题干分支所以题干分支必须放在最后。3.3 建库、批量写入与索引CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_file TEXT NOT NULL, source_index INTEGER NOT NULL, qtype TEXT NOT NULL, stem TEXT NOT NULL, options_json TEXT NOT NULL DEFAULT {}, answer TEXT NOT NULL DEFAULT , explanation TEXT NOT NULL DEFAULT , chapter TEXT NOT NULL DEFAULT , stem_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now,localtime)) ); CREATE INDEX idx_q_stem_hash ON questions(stem_hash); CREATE INDEX idx_q_chapter ON questions(chapter); CREATE INDEX idx_q_qtype ON questions(qtype); CREATE UNIQUE INDEX uq_q_dedup ON questions(stem_hash, answer);写入时用一次事务包住全部插入别一条一条 commit。import sqlite3, json conn sqlite3.connect(gk_2025.db) rows [(r[source_file], r[source_index], r[qtype], r[stem], json.dumps(r[options], ensure_asciiFalse), r[answer], r[explanation], r[chapter], stem_hash(r[stem])) for r in parsed] with conn: # with 块结束时自动 commit异常时自动 rollback conn.executemany( INSERT OR IGNORE INTO questions (source_file, source_index, qtype, stem, options_json, answer, explanation, chapter, stem_hash) VALUES (?,?,?,?,?,?,?,?,?), rows ) conn.close()INSERT OR IGNORE配合uq_q_dedup唯一索引重复题会被静默跳过。批量插入时如果把executemany放在循环里逐条执行几千道题的耗时能从毫秒级涨到十几秒差别全在事务边界上。3.4 去重规范化哈希够用相似度算法留给改写题去重分两层。第一层是完全重复做法是先把题干规范化再去算哈希。import hashlib, re def normalize(stem: str) - str: s re.sub(r\s, , stem) s re.sub(r[(]\s*[)], , s) # 去掉空括号 s re.sub(r[。、,.;:?!], , s) # 去掉标点 return s def stem_hash(stem: str) - str: # 取前 16 位足够碰撞概率在百万量级题库下可忽略 return hashlib.md5(normalize(stem).encode(utf-8)).hexdigest()[:16]第二层是改写题比如把「下列说法正确的是」改成「下列表述正确的有」选项顺序打乱。这类题用哈希抓不到得用序列相似度。from difflib import SequenceMatcher def find_near_duplicates(items, threshold0.92): hits [] for i in range(len(items)): for j in range(i 1, len(items)): # 先用长度差做粗筛能砍掉大部分无用比较 a, b normalize(items[i][stem]), normalize(items[j][stem]) if abs(len(a) - len(b)) / max(len(a), len(b)) 0.2: continue ratio SequenceMatcher(None, a, b).ratio() if ratio threshold: hits.append((items[i][id], items[j][id], round(ratio, 3))) return hitsthreshold设成 0.92 是经验值低于这个数容易把同一知识点的不同题目误判成重复高于 0.96 又会漏掉改动几个字的改写题。长度差粗筛的 0.2 阈值同样是经验值题干长度差超过 20% 基本不可能是同一道题。4. 让题库能查、能抽、能组卷FTS5 索引加一层最小服务数据落了库接下来的需求通常有两个按关键词搜题按条件抽题组卷。这两件事用 SQLite 就能做完不必上搜索引擎。4.1 中文检索FTS5 该选 unicode61 还是 trigramSQLite 的 FTS5 默认分词器按空格切词中文会被整段当成一个 token搜「行政复议」匹配不到「对行政复议决定不服的」。所以要么换 trigram 分词器要么自己在代码侧做切分。CREATE VIRTUAL TABLE questions_fts USING fts5( stem, explanation, contentquestions, -- 外部内容表不重复存正文 content_rowidid, tokenizetrigram -- 三字符滑窗支持中文子串匹配 ); INSERT INTO questions_fts(rowid, stem, explanation) SELECT id, stem, explanation FROM questions;contentquestions让 FTS 表只存索引不存正文磁盘占用能省一半以上。tokenizetrigram的代价是索引体积约为 unicode61 的两到三倍且查询词长度至少要三个字符才有意义。题库这个量级几千到几万条完全可以接受。查的时候用 MATCH配合 bm25 排序。SELECT q.id, q.stem, q.answer, bm25(questions_fts) AS score FROM questions_fts JOIN questions q ON q.id questions_fts.rowid WHERE questions_fts MATCH 行政处罚 ORDER BY score LIMIT 20;bm25()返回的是负值越小越相关所以直接ORDER BY score升序就是相关性从高到低。4.2 三个接口抽题、检索、错题回写用 FastAPI 搭最小服务三个接口就够内部用。from fastapi import FastAPI, Query import sqlite3, random app FastAPI() DB gk_2025.db def conn(): c sqlite3.connect(DB) c.row_factory sqlite3.Row return c app.get(/quiz/random) def random_quiz(n: int Query(10, ge1, le100), qtype: str | None None): sql, args SELECT * FROM questions, [] if qtype: sql WHERE qtype ? args.append(qtype) sql ORDER BY RANDOM() LIMIT ? args.append(n) return [dict(r) for r in conn().execute(sql, args).fetchall()]n的上限锁在 100是防止前端传个 99999 把整库拉出来。ORDER BY RANDOM()在几万行以内响应稳定数据量再上一个数量级改成WHERE id ?加随机起点取连续块性能差别很明显。4.3 组卷参数配比用权重表别写成硬编码 if组卷的需求几乎一定会变所以参数要能配。参数含义默认值调整建议single_ratio单选占比0.6总分值固定时改这个最直接影响难度multi_ratio多选占比0.25超过 0.35 及格率会明显掉judge_ratio判断占比0.15适合放在卷首做热身chapter_whitelist限定章节空全库按模块出专项卷时用avoid_days排除近期做过的题7错题本场景设为 0def build_paper(ratios: dict, total: int, whitelistNone, avoid_days7): paper, used [], set() for qtype, ratio in ratios.items(): need round(total * ratio) sql SELECT * FROM questions WHERE qtype ? AND id NOT IN ( SELECT question_id FROM answer_log WHERE created_at datetime(now, ?)) ORDER BY RANDOM() args [qtype, f-{avoid_days} days] if whitelist: marks ,.join(? * len(whitelist)) sql sql.replace(ORDER BY, fAND chapter IN ({marks}) ORDER BY) args [qtype] whitelist args[1:] for row in conn().execute(sql, args).fetchall(): if len([p for p in paper if p[qtype] qtype]) need: break if row[id] in used: continue used.add(row[id]) paper.append(dict(row)) return paper按题型分别取而不是取一批再筛是因为后者的配比会随题库分布漂移。avoid_days为 0 时datetime(now, -0 days)仍然成立条件不会失效这点比用字符串拼接更安全。4.4 导出成 Anki 与 Markdown注意分隔符import csv, json with open(anki_import.csv, w, newline, encodingutf-8) as f: # 用制表符而不是逗号避免选项里自带的逗号把字段切碎 w csv.writer(f, delimiter\t) for r in conn().execute(SELECT * FROM questions): opts json.loads(r[options_json]) front r[stem] br br.join( f{k}. {v} for k, v in sorted(opts.items())) back r[answer] br (r[explanation] or ) w.writerow([front, back, r[chapter]])三个细节制表符做分隔符是为了绕开选项文本里的逗号br是 Anki 卡片模板能识别的换行标签用\n在正面不会换行sorted(opts.items())保证选项按 A、B、C 顺序输出字典本身在旧版本 Python 里不保证顺序。5. 进阶排错扫描版题库的 OCR 校验、答案错位定位与跨版本差异比对5.1 扫描件题库OCR 之后必须做数量对账有些题库不是原生 docx而是扫描件转出来的段落结构完全靠不住。OCR 之后先别急着解析题干先做两个对账。第一个是题号连续性把识别出的题号排成序列检查是否存在跳号或重复跳号说明有整段被漏识别重复说明分栏排版导致同一区域的文字被读了两次。第二个是选项完整性统计每道题的选项数分布如果集中在 3 和 5 上说明C.被识别成了C,或者D.被识别成了O.需要针对 OCR 常见的字符混淆补一组替换规则。OCR_FIX {,: ., O.: D., 0.: D., ;: ., |: I} def fix_ocr_option(line: str) - str: for bad, good in OCR_FIX.items(): if line.startswith(bad): return good line[len(bad):] return line替换规则要按前缀位置判断不能全文替换否则题干里的逗号会被改成句号严重影响后续检索。5.2 答案错位的定位方法答案错位最常见的成因是题干被漏解析了一道导致后续答案整体平移一位。定位方法不是逐题看而是做数量对账统计解析出的题目总数和答案区给出的答案总数数量对不上就说明有漏题对得上但得分率异常低就说明是位置错配。def audit_alignment(questions, answer_map): missing [n for n in answer_map if n not in {q[no] for q in questions}] extra [q[no] for q in questions if q[no] not in answer_map] return {答案区有但题库没有: missing, 题库有但答案区没有: extra}把这两组编号打出来通常能一眼看出断点在哪——连续的一段编号整体偏移就是中间漏了一道题。修的方式是回到原文找到那一处补一条解析规则。5.3 跨版本比对找出两年题库的差异题手里同时有 2024 和 2025 两版题库时最有价值的输出是三个集合新增题、删除题、答案被改过的题。前两个靠stem_hash做集合差就行第三个需要按规范化题干连接后再比答案。SELECT a.stem, a.answer AS old_answer, b.answer AS new_answer FROM questions a JOIN questions b ON a.stem_hash b.stem_hash WHERE a.source_file LIKE %2024% AND b.source_file LIKE %2025% AND a.answer b.answer;这条查询能在秒级返回所有答案变动的题。拿到结果后别急着信先抽十条去原文核对——答案改动里有一部分其实是解析阶段把「答案B」和「答案BD」的多选漏读了 D属于解析 bug 而不是真的改题。把这类误报的题干特征记下来反哺到正则里下一版题库解析的准确率会明显高出一截。本文还有配套的精品资源点击获取