ARTICLE DETAIL

资讯详情

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

旧版 .doc 题库解析:Python 正则清洗与错题加权练习

旧版 .doc 题库解析:Python 正则清洗与错题加权练习 简介面向江西省高校教师岗前培训备考需求的《高等教育心理学》题库文档紧扣岗前培训考核范围适合准备高校教师资格考试、岗前培训结业测试的教师与教研人员使用。资料以单一 doc 文档形式提供共 1 个文件压缩包约 100KB篇幅短小便于打印或在电脑、手机上随时查阅。文档按章节组织选择题与填空题覆盖心理学概论、教育心理学与高等教育心理学等模块如心理动力、心理过程、心理状态与心理特征四分法笛卡尔与唯理论、构造主义、经验论等流派辨析以及观察法、实验法、问卷法、个案法等研究方法并涉及桑代克理论与我国教育心理学改造期、繁荣期等发展阶段。题目均附参考答案可用于自测、背诵与考点查漏补缺。目前已有 65 人学习适合作为岗前培训冲刺阶段的精简题库使用。1. 22 页 .doc 题库为什么直接搜答案效率最低江西省高校教师岗前培训的这套高等教育心理学题库原始形态是一份 22 页的 .doc 文档八个章节、选择题与填空题混排、答案用括号里的单个字母直接写在题干当中。多数人拿到它的第一反应是 CtrlF 搜关键词翻两页就发现行不通——同一道「学习动机」的题在第二、五、六章各出现一次答案位置时而句末、时而句首。更麻烦的是这份文档在流转中掉过字。「冯特」只剩一个「特」「格式塔」被写成「格塔式」「接受学习」变成「承受学习」「1879」里的数字 1 被识别成小写字母 l。这些坑让「搜原文」这条路的命中率大打折扣也让直接背诵变得不可靠。换一条路把它当成一份半结构化数据来处理。先用 LibreOffice 无头模式把 .doc 转成 UTF-8 纯文本再用正则把「章节—题型—题干—选项—答案」五层结构抽出来接着修掉丢字、合并重复题最后导出成可检索、可抽样、可记错题的练习集。整套流程在一台装好 Python 的普通电脑上就能跑完需要的只是一份能落地的文档解析脚本。2. .doc 二进制解析LibreOffice、antiword 与 Python 的三条路线2.1 为什么 python-docx 打不开这份文件.doc是 Word 97-2003 的复合二进制格式OLE2/CFB文件头固定是D0 CF 11 E0 A1 B1 1A E1内部靠 FAT 表把WordDocument、1Table这些流串起来。python-docx 走的是 OOXML 路线底层是 zip 包直接Document(题库1.doc)会抛PackageNotFoundError。动手前先确认格式别在错误的工具上浪费半小时# 看文件真实类型.docx 会显示 Microsoft Word 2007.doc 显示 Composite Document File file 高等教育心理学题库1.doc # 只看前 8 个字节OLE2 的魔数一眼可辨 xxd -l 8 高等教育心理学题库1.doc输出的第二列如果是d0cf 11e0 a1b1 1ae1就说明这份 22 页的文档是旧格式必须换工具链。反过来如果拿到的是.docxfile会显示Microsoft Word 2007那就是 zip 结构unzip -l能直接列目录。这一步决定了后面走哪条路值得先花三十秒。2.2 三条转换路线的对比与选型同一份 .doc不同工具的保真度差别很大。判断标准只有一个转换后的纯文本里题干行、选项行、页码行、章节标题行是否还能分得开。如果选项和题干被合并成一大段后面所有正则都要重写。方案依赖文本保真度批量能力适用判断LibreOffice headless需装 LibreOffice高保留段落与分页强支持通配符首选跨平台antiword单个二进制中段落边界会丢弱需逐个调用只要纯文本时够用catdoc单个二进制中低中文偶有乱码弱应急win32comWindows 已装 Word最高中进程开销大有 Office 授权时python-docx纯 Python 包不支持 .doc—仅处理 .docx选型上有两个容易踩的坑。一是 antiword 默认按 80 列折行输出题干和选项会被硬切成两行正则里所有^锚点都会失效得加-w 0关掉折行。二是 win32com 每次调用都会拉起一个 WINWORD.EXE 进程几百份文档跑完会把机器内存吃满记得在finally里doc.Close(False)并app.Quit()。2.3 LibreOffice 无头转换与 UTF-8 参数无头转换是这套流程里最稳的一环但命令里的 filter 名必须写全否则中文会按系统 locale 落成 GBK# 创建输出目录LibreOffice 不会自动建 mkdir -p ./txt # 关键点filter 名写全 txt:Text (encoded):UTF8只写 txt 会走默认编码 soffice \ -env:UserInstallationfile:///tmp/lo_$$ \ --headless \ --convert-to txt:Text (encoded):UTF8 \ --outdir ./txt \ ./docs/高等教育心理学题库1.doc三个参数值得单独说明。-env:UserInstallation指定一个临时配置目录避免已有 LibreOffice 实例在运行时代码复用它、命令返回成功但文件没落盘--headless表示不弹窗--convert-to的 filter 字符串里冒号后面的Text (encoded):UTF8才是真正的输出编码开关。批量场景把源文件路径换成./docs/*.doc即可LibreOffice 会自己循环。转换完成后先做一次编码体检读出来的每个字节都应该是合法的 UTF-8# 检查转换结果是否为合法 UTF-8乱码在早期发现比后期省事 from pathlib import Path for p in Path(./txt).glob(*.txt): raw p.read_bytes() try: text raw.decode(utf-8) print(f{p.name}: OK, {len(text)} 字符) except UnicodeDecodeError as e: print(f{p.name}: 编码异常 - {e})如果这里报错说明 filter 没生效回到上一步补上:Text (encoded):UTF8不要靠iconv事后补救——GBK 转 UTF-8 遇到半个汉字会直接截断丢的是内容而不是编码。2.4 页码行与全角空白的预处理转换出来的文本里每一页开头或结尾都嵌着一行- 1 - / 22这种页码。它长得像题目会污染后面的正则匹配必须在入口处清掉。同时原文大量使用全角空格\u3000做选项对齐统一换成半角后正则里只需要处理一种空白。import re PAGE_NO re.compile(r^\s*-\s*\d\s*-\s*/\s*\d\s*$, re.M) def preprocess(text: str) - str: # 1) 删掉整行的页码标记 text PAGE_NO.sub(, text) # 2) 全角空格统一成半角方便后续用 \s 匹配 text text.replace(\u3000, ) # 3) 行内多余空格压缩但保留两空格以上的分隔语义 text re.sub(r[ \t]{2,}, , text) # 4) 去掉行尾空白避免 \n 前留空格影响锚点 text \n.join(line.rstrip() for line in text.splitlines()) return text第 3 步压缩到固定两个空格是有意为之原文的选项分隔恰好是两个以上空格压缩后既消除了排版噪声又保留了「这里是分隔」的信号后面的选项切分正则会用到这个特征。处理完这一步文本就进入了可以被正则稳定识别的状态。3. 四类模式的正则抽取题干、选项、答案与填空题3.1 先归一化再匹配原始文本里混着全角括号、全角字母、全角句点和顿号、直接写正则要枚举十几种组合。做法是先把这些字符映射到半角再做一次匹配。用str.translate建表比链式replace快一个数量级也更不容易漏项。# 全角 - 半角映射表两边长度必须一致 FULL2HALF str.maketrans( 、, ()ABCD.,,:; ) def normalize(s: str) - str: return s.translate(FULL2HALF)映射完成后所有正则里只需要写一种括号()和一种句点.。注意不要顺手把全角句号。也映射成.因为.在正则里是通配符映射之后必须转义才能当字面量用凭空多出一类转义负担。3.2 选择题题干与答案的分离这份题库的选择题有个反直觉的地方——答案括号不固定在句末。第一章第 1 题是「……属于( A )。」第 3 题却是「( B )是一种对事物进行深入认知的需要」答案跑到了句首第三章第 12 题更有意思题干里本身还带着一对不含字母的括号。所以匹配规则不能依赖位置只能靠「括号内是不是单个 A-D 字母」来判断。目标正则说明章节标题^第[一二三四五六七八九十]章\s*(.)$捕获章名用于分组题型标题^[一二三四]、\s*(选择题填空题)新题目起始^(\d)[.]\s*(.)$捕获题号与题干首行答案括号[(]\s*([A-D])\s*[)]全行扫描取第一个命中选项簇([A-D])[.、]\s*([^A-D]*)一行内可能有四个选项选项在这份文档里是挤在同一行的A心理动力 B心理过程 C心理状态 D心理特征。切分时不能用^...$要用前瞻切分import re Q_START re.compile(r^(\d)[.]\s*(.)$) ANS_IN re.compile(r[(]\s*([A-D])\s*[)]) OPT_ITEM re.compile(r^([A-D])[.、]\s*(.*)$) def parse_choice_block(lines): 把一道选择题的多行文本拆成 题干 选项 答案 stem_parts, options, answer [], {}, None for line in lines: # 答案括号可能出现在任意一行先全行找 if answer is None: m ANS_IN.search(line) if m: answer m.group(1) # 一行里可能有多个选项用前瞻在 A-D 前切开 chunks re.split(r(?[A-D][.、]), line) hit False for ch in chunks: mo OPT_ITEM.match(ch.strip()) if mo: options[mo.group(1)] mo.group(2).strip() hit True if not hit: stem_parts.append(line.strip()) return { stem: .join(p for p in stem_parts if p), options: options, answer: answer, }re.split(r(?[A-D][.、]), line)里的前瞻是零宽断言不会吃掉字母本身所以切出来的每一块都以选项字母开头。OPT_ITEM再把这块的字母和内容分开。hit这个标志位是必要的如果一行既没有选项也没被识别成题干说明它可能是跨行的续行要拼回题干而不是丢弃。3.3 填空题括号即答案位填空题的结构更简单也更危险。原文长这样11967 年美国心理学家奈塞尔发表了(《认知心理学》)一书标志着现代认知心理学的正式诞生。括号里的内容就是答案而且一道题可能有多个空顺序必须与空位顺序一致。抽取规则是把题干里所有(…)或…的内容按出现顺序提取成列表同时把括号替换成占位符___生成题目版本。BLANK re.compile(r[(]\s*([^()]{1,30}?)\s*[)]) def parse_fill_blank(stem: str): answers BLANK.findall(stem) # 按出现顺序拿答案 question BLANK.sub(______, stem) # 题干版留空 return {question: question, answers: answers}{1,30}这个长度上限不是随便定的题库里出现过「(以人为本)」「(学习的方法)」这类不超过 8 字的答案也出现过带说明的长括号。设成 30 字符可以挡住题干中夹带的解释性括号避免把「(如第三章所述)」误当成答案。真正的兜底还是人工抽检脚本只负责把可疑项标出来。3.4 用状态机串起章节与题型单条正则解决不了「这道题属于哪一章哪一节」的问题需要一个自上而下的状态机。状态只有三个当前章节、当前题型、当前题目的行缓冲。遇到章节标题就切换章节并重置题号计数遇到题型标题就切换解析模式遇到^\d[.]就把上一题 flush 出去、开一道新题。import json def build_records(text: str): records, buf [], [] chapter, section, qid None, None, None def flush(): if buf and section 选择题: rec parse_choice_block(buf) rec.update(chapterchapter, qidqid, typechoice) records.append(rec) elif buf and section 填空题: rec parse_fill_blank( .join(buf)) rec.update(chapterchapter, qidqid, typeblank) records.append(rec) buf.clear() for line in text.splitlines(): line normalize(line).strip() if not line: continue if line.startswith(第) and 章 in line[:4]: flush(); chapter line; continue if line.startswith((一、, 二、)) and 题 in line: flush(); section line.split(、)[1].strip(); continue m Q_START.match(line) if m: flush(); qid int(m.group(1)); buf.append(m.group(2)); continue buf.append(line) flush() return records if __name__ __main__: text preprocess(open(./txt/题库1.txt, encodingutf-8).read()) with open(./out/questions.jsonl, w, encodingutf-8) as f: for r in build_records(text): f.write(json.dumps(r, ensure_asciiFalse) \n)写成 JSONL 而不是一个大 JSON是为了后面能逐行流式处理、增量修补。每行一条记录字段固定为chapter / section / qid / type / stem / options / answer清洗和导出环节都基于这个 schema改一处不用动全局。4. 数据清洗丢字修复、答案校验与重复题合并4.1 丢字与替换的成因判断把抽取结果通读一遍会发现错误有明显的规律性。「冯特」只剩「特」、「内」字大面积消失「部动机」「在规律」「容」、「接受」被写成「承受」、「以及」被写成「以与」、「1879」里的 1 被写成小写 l。前两类是字符丢失后两类是同义替换和形近混淆。原始文本应为类型德国著名心理学家( 特 )冯特首字丢失格塔式心理学格式塔字序错乱承受学习 / 承受事物意义的学习接受学习同义替换部动机 / 部归因内部动机 / 内部归因字符丢失在规律 / 在需要内在规律 / 内在需要字符丢失l879 年1879 年形近混淆这类问题几乎不可能靠规则全自动修完但可以把高频错误做成词典覆盖掉 90% 以上的影响面剩下的靠本章 4.3 的校验环节暴露出来。4.2 术语纠错词典与误伤防范朴素的做法是把错误串换成正确串但这里有个必须注意的边界「部动机」要修正成「内部动机」可「外部动机」里同样含有「部动机」这三个字。直接全局替换会把「外部动机」改写成「外内部动机」制造出一批新的错题。import re FIXES [ # 用负向后顾断言排除「外部动机」这种合法词避免误伤 (r(?!外)(?!内)部动机, 内部动机), (r(?!外)(?!内)部归因, 内部归因), (r格塔式, 格式塔), (r承受学习, 接受学习), (r以与, 以及), (rl879, 1879), (r\( 特 \), 冯特), ] COMPILED [(re.compile(p), r) for p, r in FIXES] def apply_fixes(s: str) - str: for pat, rep in COMPILED: s pat.sub(rep, s) return s顺序上要把长模式放在短模式前面否则短模式先吃掉一部分字符长模式就再也匹配不到了。另外每次新增一条修正规则都要拿「外部」「内部」这组词做一次回归确认没有互相污染。4.3 答案分布与题数完整性校验清洗完不能直接拿来背得先验证。最有效的两个指标是每章题数和答案字母分布。这份文档里第一章选择题应该是 10 题、第三章 15 题、第五章 20 题如果抽取结果对不上说明状态机在某处漏掉了题号行。答案分布则能暴露括号识别的错位正常的题库 A/B/C/D 大致均衡如果某一字母占比超过 40%几乎可以确定有大量答案被错配。from collections import Counter def audit(records): by_chapter {} for r in records: by_chapter.setdefault(r[chapter], Counter())[r[type]] 1 print(章节.ljust(24), 选择题, 填空题) for ch, c in by_chapter.items(): print(ch[:22].ljust(24), str(c[choice]).rjust(4), str(c[blank]).rjust(5)) # 答案字母分布异常时人工回看原文档对应行 letters Counter(r[answer] for r in records if r[type] choice) total sum(letters.values()) or 1 for k in ABCD: print(f{k}: {letters[k]:3} ({letters[k] / total:.1%})) # 没有答案或选项不全的题单独列出来 bad [r for r in records if r[type] choice and (not r[answer] or len(r[options]) 4)] print(f\n待人工确认{len(bad)} 题) for r in bad[:10]: print( -, r[chapter], r[qid], r[stem][:40])这份文档还有个必须知道的前提它是节选第八章的选择题只列到第 4 题就被截断了填空题也没收全。审计脚本要能把「末章题数明显偏少」这件事打印出来而不是静默通过——静默通过意味着你永远不知道自己漏了多少内容。4.4 重复题合并归一化哈希加相似度兜底题库里跨章节的重复题不少比如「自我效能感」的定义在第三、五章各出现一次。去重要分两层完全相同的用归一化哈希直接判等措辞略有差异的用相似度兜底。import hashlib from difflib import SequenceMatcher def fp(stem: str) - str: # 去掉所有空白和标点后取哈希忽略排版差异 clean re.sub(r[\s。、().,], , stem) return hashlib.md5(clean.encode(utf-8)).hexdigest() def dedup(records, threshold0.92): seen, kept {}, [] for r in records: h fp(r[stem]) if h in seen: continue dup_of None for k in kept: ratio SequenceMatcher(None, r[stem], k[stem]).ratio() if ratio threshold: dup_of k break if dup_of is None: seen[h] r kept.append(r) return kept几百道题的规模下SequenceMatcher的两两比较完全跑得动一千题以内耗时在秒级。阈值设 0.92 是个折中低于它会把「正迁移」和「负迁移」这种一字之差的题目误合并高于它则漏掉「学习动机是……的一种内部过程或内部心理状态」这类同义改写。合并时保留题号最小的那条把其它来源记进sources字段方便回溯。5. 从静态题库到可练习系统间隔重复与错题加权抽样5.1 导出 Anki 的字段设计抽干净之后最省力的复用方式是导出成 Anki 能吃的 TSV。制表符分列字段内部换行用br代替第三列当牌组名或标签def to_anki_tsv(records, path./out/deck.tsv): with open(path, w, encodingutf-8) as f: for r in records: if r[type] choice: opts br.join(f{k}. {v} for k, v in sorted(r[options].items())) front f{r[stem]}br{opts} back f答案{r[answer]}br来源{r[chapter]} 第 {r[qid]} 题 else: front r[question] back 答案 .join(r[answers]) # 第三列作为标签便于按章节刷题 tag re.sub(r\s, _, r[chapter]) f.write(f{front}\t{back}\t{tag}\n)选项用sorted()后再输出是因为解析阶段字典的插入顺序受行内切分结果影响排序能保证同一个字母永远在同一行视觉上更稳定。导入 Anki 时记得勾选「允许 HTML」否则br会被当成字面文本显示出来。5.2 错题加权抽样脚本如果不想装 Anki一个几十行的命令行练习器也够用。核心是加权抽样——错过的题权重更高答对的题权重衰减不必引入完整的 SM-2 算法。import json, random def pick(pool, n10, alpha1.6): weight (错误次数 1) ** alpha / (答对次数 1) weights [(s[wrong] 1) ** alpha / (s[right] 1) for s in pool] chosen, idxs [], list(range(len(pool))) for _ in range(min(n, len(pool))): total sum(weights[i] for i in idxs) r random.uniform(0, total) acc 0.0 for i in idxs: acc weights[i] if acc r: chosen.append(pool[i]); idxs.remove(i); break return chosenalpha控制惩罚强度设成 1.0 是线性加权错三次和错一次只差 3 倍调到 1.6 之后错三次的题被抽中的概率约是错一次的 2.1 倍既压得住老错题也不至于让某几道题反复出现刷屏。状态用一个小 JSON 文件落盘字段只有wrong、right和last_seen。盒位触发条件下次出现间隔0答错本次会话内重新入池1首次答对1 天后2连续答对 2 次3 天后3连续答对 3 次以上7 天后验证环节不能省。抽 5% 的题目回原文档逐行比对重点看填空题的答案顺序是否与括号出现顺序一致——这是最隐蔽的一类错误答案本身没错位置错了一格背下来反而有害。章节标题用grep -c ^第.*章 ./txt/题库1.txt数一遍和审计报告里的章节数对上整条链路才算闭合。本文还有配套的精品资源点击获取
返回列表