ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python解析计算机二级docx试题:构建SQLite题库与刷题闭环

Python解析计算机二级docx试题:构建SQLite题库与刷题闭环 简介面向计算机二级等级考试备考人群的试题与答案汇编时间覆盖2021至2022年聚焦公共基础知识、程序设计、数据库与网络应用等高频考点。资源以1个docx文档形式提供压缩包仅60KB便于在手机、平板或电脑上随时查阅。现有66人学习过该题库可作为考前自测与知识点查漏补缺的参考。题目覆盖VFP日期表达式、C语言特点与标识符规则、Java Swing输入组件、Visual FoxPro表操作、Word段落间距、第一台电子计算机ENIAC、二维数组声明、索引目的、算法复杂度、VBScript逻辑判断、HTTP协议层次、Web浏览器、实体联系类型、存储系统组成、HTML标题标签及数据库表结构等。每道题均附正确答案适合用来检验基础概念掌握程度也可对照错题定位薄弱章节提升选择题与判断题的应试准确率。1. 文件名里的 2021-2022计算机二级试题及答案该怎么组织同一个「2021-2022计算机二级等级考试试题及答案No.15749.docx」在两个备考的人手里会是两种结局一个人用 Word 从头翻到尾翻到第 300 道选择题时早就忘了第 50 道错在哪个选项另一个人把它拆成了带科目、年份、考点标签的本地题库每天按错题召回率抽 40 题十天就能把 2021-2022 的真题过三轮。差别不在勤奋在于有没有把计算机二级试题及答案当成数据而不是当成一份要读的文档。这类文档的真实形态通常很粗糙选择题连排四个选项挤在同一段里答案有的跟在题干后面有的集中在文末C语言、Python、MS Office、WPS、公共基础五个科目的题混在一起同一道题在两套卷子里重复出现答案偶尔还互相打架操作题带截图和步骤描述纯文本根本读不出结构。手工整理不现实只靠记性刷题效率又太低。下面这套流程做的事很具体解析 docx写进 SQLite按科目打标签抽题判分错题回归最后加上题干检索和答案冲突检测。全程本地跑不依赖任何在线题库软件。适合正在备考二级、手里已经攒了历年真题的人也适合想拿一份真实脏数据合并单元格、全角标点、跨段题干练文本解析的开发者。2. 先看懂版式计算机二级试题及答案 docx 的四种排版与解析选型动手写解析器之前我一般会先把文档的排版摸一遍。同一批流传的二级真题来源不同排版可能完全不同有人是从网页复制粘贴进 Word 的有人是扫描后 OCR 转出来的还有人是把 Excel 答题卡直接贴成了表格。对着不认识的版式硬写正则十有八九要推倒重来。2.1 四种典型排版决定了解析器要写成什么样排版类型典型特征解析难点建议方案连排式1下列关于栈的叙述中…… A B C D全在一个段落选项和题干无换行边界靠标点判断段内正则切分先切选项再切题干题号分节式题号独占一行选项各占一行答案另起一行需要状态机跟踪当前题逐段扫描 状态变量表格答题卡式题目在单元格里答案在另一列正文与表格混排顺序会乱单独遍历doc.tables按行列重建图文混排操作题题干带截图、步骤编号、如图所示图片不可结构化文字步骤零散只保留文字步骤和图片锚点图片单独导出判断版式最快的办法不是肉眼翻而是让脚本先统计一遍。下面这段代码只做一件事看看有多少段落是题号开头其中又有多少段落内部还带选项两类数量一比版式基本就清楚了。import re from docx import Document doc Document(2021-2022计算机二级等级考试试题及答案No.15749.docx) # 题号开头兼容 1. / 1、/1/【1】 等写法 Q_HEAD re.compile(r^\s*[(【\[]?\s*\d{1,4}\s*[)】\]]?\s*[、.]) OPT_IN re.compile(r[A-D][、.:]) # 段内出现 A. B. 这类选项标记 inline, standalone, total 0, 0, 0 for p in doc.paragraphs: t p.text.strip() if not t: continue total 1 if Q_HEAD.match(t): if OPT_IN.search(t): inline 1 # 连排式题干和选项同段 else: standalone 1 # 题号可能独占一行 print(f段落总数{total} 连排式{inline} 疑似独占行{standalone} 表格数{len(doc.tables)})参数说明上只有两处值得调Q_HEAD里的\d{1,4}是题号宽度超过 9999 题的文档才需要改OPT_IN里把全角顿号、半角点、冒号都列进了字符集是因为 OCR 出来的文档里这三种标点混用得非常随意。跑完的结果如果inline远大于standalone说明主结构是连排式解析重点要放在段内切分上。2.2 为什么从段落级解析入手而不是一口气正则全文一个常见的错误做法是.join([p.text for p in doc.paragraphs])拼成一个大字符串然后上一条大正则。这样做有两个坑一是段落边界丢掉之后A这种选项标记和题干里正常的选项 A就分不开了二是跨段题干第一段是题干、第二段接下列说法正确的是会被粘成一句切分时反而更难还原。正确姿势是保留doc.paragraphs的顺序把它当成一条流用状态机往前走。段落是 Word 里最稳定的结构单位格式再怎么变题干、选项、答案基本都会落在独立的段落上或者至少落在能靠标点识别的段落内部。2.3 解析前必须统一的三件事第一件是题号归一。1、【1】、1、、1都是同一道题的不同写法先统一成整数题号后面按题号排序、按题号比对答案回填才会稳定。第二件是标点归一。全角顿号、全角句点、全角括号在题干和选项里的含义完全不同但选择题选项标记必须能被同一条正则匹配到。稳妥的做法是先把。、按位置做一次映射而不是简单粗暴地全角转半角——题干里的中文标点要留着。第三件是科目标注。不要想着先入库以后再分类。解析阶段就把科目写进结构里后面按科目抽题、按科目看正确率都从这里来事后补标等于把成本翻倍。3. 用 python-docx 抽取题干、选项与答案写进 SQLite版式确认之后真正的工作量在解析器本身。这一章给一个可以直接改路径就跑的版本覆盖单选、多选、判断三种选择题形态操作题只留文字步骤。3.1 依赖与环境三个库就够# 读取 docx、操作 SQLite、中文分词可选 pip install python-docx # sqlite3 是标准库不需要单独安装 # 验证版本python-docx 0.8 以上都支持 doc.tables 遍历 python -c import docx, sqlite3; print(docx.__version__)python-docx负责读sqlite3负责存jieba只有在做中文分词检索时才需要。如果只做解析和抽题前两个足够了。注意python-docx只能读.docx遇到老式.doc文件要先转存一次格式这一步在 Word 或 LibreOffice 里做不要试图用脚本硬读。3.2 一个可复用的解析器状态机加三类正则解析器的核心思路是维护一个cur变量表示当前正在处理的题遇到题号就结算上一题、开一道新题遇到选项就塞进选项字典遇到答案就写上答案字段遇到解析就存进解析字段其余段落按有没有答案决定是补题干还是补解析。import re, sqlite3, hashlib from docx import Document QNUM re.compile(r^\s*[(【\[]?\s*(\d{1,4})\s*[)】\]]?\s*[、.]\s*(.)$) OPT re.compile(r^\s*([ABCD])\s*[、.:]\s*(.)$) ANS re.compile(r^\s*(?:答案|参考答案|正确答案)\s*[:]?\s*([ABCD]{1,4}|[对错√×])) ELAB re.compile(r^\s*(?:解析|分析|考点)\s*[:]\s*(.)$) def norm(s: str) - str: # 只压缩空白不动中文标点避免破坏题干语义 return re.sub(r[\u3000\s], , s).strip() def parse_docx(path: str) - list[dict]: doc Document(path) rows, cur [], None for p in doc.paragraphs: t norm(p.text) if not t: continue if (m : QNUM.match(t)): # 新题开始 if cur and cur[stem]: rows.append(cur) cur {qno: int(m.group(1)), stem: m.group(2), opts: {}, answer: , analysis: } continue if cur is None: # 题干之前的水印、页眉、标题 continue if (m : OPT.match(t)): # 选项行 cur[opts][m.group(1)] m.group(2) elif (m : ANS.match(t)): # 答案行 cur[answer] m.group(1).upper() elif (m : ELAB.match(t)): # 解析行 cur[analysis] m.group(1) elif cur[answer]: # 答案之后的散句多半是解析续行 cur[analysis] t else: # 答案之前的散句是跨段题干 cur[stem] t if cur and cur[stem]: rows.append(cur) return rows逻辑上有三处需要留意。QNUM的捕获组只认整数题号是为了防止把2021年这类年份误判成题号ANS同时接受ABCD组合和对/错/√/×是因为判断题在二级题库里这两种写法都常见cur[answer]是否为空被当成判断依据用来决定后续散句是补题干还是补解析——这个启发式规则在 95% 的情况下成立剩下 5% 的题靠人工抽查兜底。norm()只压缩空白不转标点原因是二级题干里有大量3.141.5倍这样的数字全角半角一转容易出歧义。3.3 题库表设计与导入脚本字段类型说明idINTEGER PK自增主键qnoINTEGER原文档题号仅作溯源subjectTEXTC语言 / Python / MS Office / WPS / 公共基础yearINTEGER2021 或 2022从卷名或文件头提取qtypeTEXT单选 / 多选 / 判断 / 操作stemTEXT题干正文opt_a~opt_dTEXT四个选项判断题留空answerTEXT标准答案analysisTEXT解析与考点说明stem_hashTEXT UNIQUE归一化题干的哈希用于去重和冲突检测CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, qno INTEGER, subject TEXT NOT NULL DEFAULT 待定, year INTEGER, qtype TEXT NOT NULL DEFAULT 单选, stem TEXT NOT NULL, opt_a TEXT, opt_b TEXT, opt_c TEXT, opt_d TEXT, answer TEXT, analysis TEXT, stem_hash TEXT UNIQUE ); CREATE INDEX IF NOT EXISTS idx_q_subject ON questions(subject, year);def stem_hash(stem: str) - str: # 去掉标点和空白再哈希让下列/以下这类改写也能撞到一起 key re.sub(r[\W_], , stem) return hashlib.md5(key.encode(utf-8)).hexdigest() def save(rows, dbncre.db): conn sqlite3.connect(db) ok dup 0 for r in rows: try: conn.execute( INSERT INTO questions(qno,stem,opt_a,opt_b,opt_c,opt_d,answer,analysis,stem_hash) VALUES(?,?,?,?,?,?,?,?,?), (r[qno], r[stem], r[opts].get(A), r[opts].get(B), r[opts].get(C), r[opts].get(D), r[answer], r[analysis].strip(), stem_hash(r[stem]))) ok 1 except sqlite3.IntegrityError: dup 1 # stem_hash 撞了说明是重复题 conn.commit() print(f入库 {ok} 题跳过重复 {dup} 题) return connstem_hash建 UNIQUE 索引有两个作用一是导入时自动去重同一道题在 2021 和 2022 两套卷子里重复出现时只留一条二是后面查同一道题答案不一致时能直接按这个字段分组。哈希前用\W去掉标点和空白是为了让下列关于栈的叙述中正确的是和下列关于栈的叙述中正确的是能正确合并。3.4 跑一遍并校验导入结果python - PY from parse import parse_docx, save # 把上面两段存成 parse.py rows parse_docx(2021-2022计算机二级等级考试试题及答案No.15749.docx) print(解析到题数:, len(rows)) print(缺答案题数:, sum(1 for r in rows if not r[answer])) save(rows) PY # 入库后三个必查指标 sqlite3 ncre.db SELECT COUNT(*) FROM questions; sqlite3 ncre.db SELECT COUNT(*) FROM questions WHERE answer IS NULL OR answer; sqlite3 ncre.db SELECT qtype, COUNT(*) FROM questions GROUP BY qtype;缺答案题数是最值得盯的指标。它偏高一般意味着两件事之一文档的答案集中在文末需要额外走一遍答案区回填或者答案用的是参考答案B之外的写法需要往ANS正则里补模式。后一种情况不要改主正则去兼容所有变体单独写一个补丁正则跑第二轮主流程保持干净。4. 给 2021-2022 二级真题打标签C语言、Python、MS Office、WPS、公共基础怎么分流题库入库只是第一步真正决定刷题体验的是科目标签。计算机二级的五个科目混在一份文档里按科目抽题、按科目看正确率是后面一切统计的基础。4.1 关键词表法用特征词打分而不是单一 if科目高区分度特征词典型题干片段C语言#include、printf、scanf、malloc、指针、数组、struct以下程序段的输出结果是Pythondef、import、range(、列表、字典、元组、print(执行以下代码后输出结果是MS OfficeExcel、Word、PowerPoint、幻灯片、数据透视表、单元格在Excel中若要……WPSWPS、稻壳、WPS文字、WPS表格、WPS演示在WPS表格中以下操作正确的是公共基础栈、队列、二叉树、算法复杂度、E-R、软件工程、数据库系统下列关于栈的叙述中正确的是import re, sqlite3 RULES { 计算机二级c语言题库: (r#include|printf|scanf|malloc|\bstruct\b|指针|数组|函数调用, 3), 计算机二级python题库: (r\bdef\b|\bimport\b|range\(|列表|字典|元组|print\(, 3), 计算机二级wps题库: (rWPS|稻壳, 5), 计算机二级ms office题库: (rExcel|Word|PowerPoint|幻灯片|数据透视表|单元格, 2), 计算机二级公共基础: (r栈|队列|二叉树|算法复杂度|E-R|软件工程|数据库系统, 3), } def tag(stem: str): score {k: len(re.findall(p, stem, flagsre.I)) * w for k, (p, w) in RULES.items()} best max(score, keyscore.get) # 得分太低说明特征词没命中标成待定交给人工 return (best, 高) if score[best] 3 else (待定, 低)打分而不是命中即归类解决的是题干里同时出现 Excel 和数据的这类交叉题。WPS的权重给到 5是因为真题里只要出现WPS三个字母科目就基本确定而单元格这种词在 WPS 和 MS Office 里都会出现。4.2 权重与优先级WPS 和 MS Office 为什么必须先判WPS 和 MS Office 两个科目的题干高度相似都是在表格中选中单元格后这类描述。区别只在软件名。如果两条规则用同一个分值交叉题就会被随机分到两边。稳妥的顺序是先跑 WPS 规则命中就直接定科目剩下没命中的再走 MS Office 规则。这样 WPS 题库里那些没写WPS但确实考 WPS 的题会落到 MS Office 里属于可接受的误差反过来的误差更大。C语言和 Python 之间基本不会互相污染printf和print(是硬区分。公共基础是唯一会跨科目的数据结构、算法、软件工程这些内容在 C语言和 Python 的选择题里也会出现但公共基础考的是概念题干里不会出现代码。判断依据可以再加一条题干里出现#include或def就不算公共基础。4.3 标签体系科目之外还要打什么标签维度取值用途科目五科 待定按科目抽题、按科目统计正确率年份2021 / 2022按年度做整卷模拟题型单选 / 多选 / 判断 / 操作操作题单独练不参与随机抽题考点指针、循环、函数、E-R 图……错题按考点聚类定位薄弱环节频次同一stem_hash出现次数高频题优先复习考点标签不建议一开始就穷举。先按科目跑两周把错题集中出现的题干导出来看一眼再决定往哪个科目加细分标签。上来就列 50 个考点标签最后大概率是大部分题落在其他里。4.4 误判兜底置信度阈值与人工复核清单conn sqlite3.connect(ncre.db) rows conn.execute(SELECT id, stem FROM questions WHERE subject待定).fetchall() with open(need_review.txt, w, encodingutf-8) as f: for qid, s in rows: f.write(f{qid}\t{s[:60]}\n) print(待人工复核:, len(rows))阈值定成 3 分是试出来的低于这个值命中的基本都是噪声词高于这个值待定题会多到懒得看。导出到need_review.txt之后大部分是题干被截断或者 OCR 出错的坏数据几百条里真正需要人工判断的通常不到 30 条。把复核结果直接UPDATE questions SET subject? WHERE id?写回去就完成一轮校准。5. 刷题闭环抽题、判分、错题回归的本地实现题库和标签都有了接下来是每天真正会用的部分抽题、判分、记录、回归。这部分用 SQL 加一个小命令行脚本就够了不需要写界面。5.1 抽题 SQL随机抽题、高频优先与没做对过的过滤-- 抽 20 道指定科目、最近 30 天内没做对过的题 SELECT q.id, q.stem, q.opt_a, q.opt_b, q.opt_c, q.opt_d FROM questions q WHERE q.subject :subject AND q.qtype IN (单选,判断) AND q.id NOT IN ( SELECT p.qid FROM practice p WHERE p.correct 1 AND p.ts datetime(now,localtime,-30 day) ) ORDER BY RANDOM() LIMIT :n;这个查询的两个设计点值得说明。NOT IN子查询过滤的是做对过的题不是做过的题——做错的题必须能被再次抽到否则错题永远不会重现。时间窗口给 30 天是为了让做对过的题在一个月后重新进入抽题池起到复习作用。如果只想过一遍真题把子查询整个删掉ORDER BY qno就是按原卷顺序出题。高频题优先只要换一个排序-- 同一 stem_hash 出现次数越多越靠前适合考前突击 SELECT id, stem, COUNT(*) OVER (PARTITION BY stem_hash) AS freq FROM questions WHERE subject :subject ORDER BY freq DESC, RANDOM() LIMIT :n;5.2 判分与练习记录表字段说明qid对应questions.idchosen用户选择的答案多选按AC这种形式存correct1 为对0 为错判分后写入cost_ms作答耗时用于识别蒙对的题ts作答时间默认取本地时间CREATE TABLE IF NOT EXISTS practice ( id INTEGER PRIMARY KEY AUTOINCREMENT, qid INTEGER NOT NULL REFERENCES questions(id), chosen TEXT, correct INTEGER NOT NULL DEFAULT 0, cost_ms INTEGER, ts TEXT DEFAULT (datetime(now,localtime)) ); CREATE INDEX IF NOT EXISTS idx_practice_qid ON practice(qid, ts);import sqlite3, time def judge(conn, qid: int, chosen: str, cost_ms: int) - bool: std conn.execute(SELECT answer FROM questions WHERE id?, (qid,)).fetchone()[0] or # 多选与单选统一去重、按字母排序后比较避免 AB 与 BA 判错 ok int(.join(sorted(set(chosen.upper()))) .join(sorted(set(std.upper())))) conn.execute(INSERT INTO practice(qid,chosen,correct,cost_ms) VALUES(?,?,?,?), (qid, chosen, ok, cost_ms)) conn.commit() return bool(ok) def drill(subject: str, n: int 20): conn sqlite3.connect(ncre.db) qs conn.execute(SELECT id,stem,opt_a,opt_b,opt_c,opt_d FROM questions WHERE subject? AND qtype IN (单选,判断) ORDER BY RANDOM() LIMIT ?, (subject, n)).fetchall() right 0 for qid, stem, a, b, c, d in qs: print(f\n[{qid}] {stem}) for k, v in zip(ABCD, (a, b, c, d)): if v: print(f {k}. {v}) t0 time.time() chosen input(作答 ).strip() or if judge(conn, qid, chosen, int((time.time() - t0) * 1000)): right 1 print(f\n本轮 {len(qs)} 题正确 {right}正确率 {right/len(qs):.0%})判分时对答案做集合排序比较是踩过的坑多选题库里答案可能是BA用户输入AB字符串直接比会判错。cost_ms那一列在第一轮刷题时看着没用等你要分辨真会和蒙对的时候就有用了——三秒内选对的多选题多半是运气。5.3 错题回归按间隔重做正确率按科目看错题状态触发条件下次出现新错首次答错次日巩固中答对 1 次3 天后基本掌握连续答对 2 次7 天后已掌握连续答对 3 次30 天后-- 各科目近期正确率直接看出该补哪科 SELECT q.subject, COUNT(*) AS 作答次数, ROUND(AVG(p.correct) * 100, 1) AS 正确率 FROM practice p JOIN questions q ON q.id p.qid GROUP BY q.subject ORDER BY 正确率; -- 按考点看错误集中的地方 SELECT q.subject, p.qid, COUNT(*) c FROM practice p JOIN questions q ON q.id p.qid WHERE p.correct 0 GROUP BY q.subject, p.qid HAVING c 2 ORDER BY c DESC LIMIT 30;间隔重做的实现不需要额外的调度器用practice表里最后一次作答时间和连续正确次数算出来就行取每题最近一次ts和最近的正确序列符合上表条件就进抽题池。这个做法比把所有错题堆一起重做更接近记忆规律也不会让错题池越滚越大——连续答对三次之后题目会自动退出高频抽题范围。6. 进阶题干模糊检索与答案冲突检测前五章跑通之后题库已经能用了。剩下两件小事做过一次就会一直用考前几分钟查一个考点以及把题库里答案打架的题揪出来。6.1 用 FTS5 给题干建索引考前 3 分钟查一个考点SQLite 自带 FTS5不用装任何东西。中文默认按字切分对短题干检索足够用想按词切可以先用 jieba 把题干分词后存进一个单独的列再对该列建索引。CREATE VIRTUAL TABLE IF NOT EXISTS q_fts USING fts5(stem, contentquestions, content_rowidid); INSERT INTO q_fts(rowid, stem) SELECT id, stem FROM questions; -- 查所有考指针的题命中部分用 b 标出来 SELECT q.id, q.subject, snippet(q_fts, 0, b, /b, …, 12) FROM q_fts JOIN questions q ON q.id q_fts.rowid WHERE q_fts MATCH 指针 LIMIT 20;snippet()的五个参数依次是列号、命中前缀、命中后缀、省略号、片段长度。片段长度给 12 个 token 比较合适太短看不到上下文太长一屏装不下。注意 FTS5 的MATCH语法里指针 OR 数组这种布尔组合是支持的但中文短语要加引号写成数据透视表才会被当成一个整体匹配。6.2 同一道题两处答案不一致怎么批量揪出来这是真题文档里一定会出现的问题同一道题在 2021 卷里答案是 B在 2022 卷里变成了 C。之前建的stem_hash就是干这个的。SELECT stem_hash, COUNT(*) AS 出现次数, GROUP_CONCAT(DISTINCT answer) AS 答案集合, GROUP_CONCAT(id) AS 题号列表 FROM questions GROUP BY stem_hash HAVING COUNT(*) 1 AND COUNT(DISTINCT answer) 1 ORDER BY 出现次数 DESC;COUNT(DISTINCT answer) 1是关键条件它筛掉的是重复但答案一致的题只留下真正冲突的那些。这类题在几千题的库里通常只有个位数到几十条导出来人工核对一下即可。# 把冲突题连同选项导出成一份便于核对的文本 sqlite3 ncre.db -separator $\t \ SELECT id, subject, stem, opt_a, opt_b, opt_c, opt_d, answer FROM questions WHERE stem_hash IN ( SELECT stem_hash FROM questions GROUP BY stem_hash HAVING COUNT(*)1 AND COUNT(DISTINCT answer)1); conflicts.tsv核对完之后建议的做法不是改答案而是把冲突题标一个需要复核标记在抽题环节默认排除。理由很实在真题流传过程中本身就有错漏与其赌哪个答案对不如让这些题暂时不进入你的复习池。真要保留也只留一条另外几条用UPDATE questions SET qtype作废 WHERE id IN (...)标记掉抽题 SQL 里加一句AND qtype IN (单选,判断)自然就过滤了。最后补一个参数上的经验snippet()的片段长度和HAVING里的冲突阈值都不要一开始就调细先用默认值跑一遍全库看冲突清单的实际长度再决定要不要收窄条件。多数情况下冲突题的数量远比你预期的少。本文还有配套的精品资源点击获取
返回列表