简介《现代大学英语听力2》原文及答案文档面向高校英语专业学生及需要强化听力理解的备考者用于解决听力材料难以核对原文、答案缺失、文化背景理解不足等问题。整包仅含1个docx文档约161KB包含Unit 1 Task 1与Task 2的听力答案及完整原文前者讲述Gretel访问伦敦、参观圣保罗大教堂并对深色西装、圆顶硬礼帽、雨伞和报纸等英国人形象感到惊讶引出“It takes all kinds to make a world”的多样性观点后者通过John与Etsuko的对话比较英日生活在工作节奏、气候、地形、住房等方面的差异。借助这些材料学习者可对照原文精听、核对答案、积累关键词句并理解英国社会文化差异提升跨文化交际能力。目前已有74人学习。1. 一份听力教材 docx 到底能拆出什么很多人拿到「现代大学英语听力2原文及答案.docx」用法就是打开、对照、关掉。但把它拽进编辑器里逐段看一下就会发现Unit 1 底下 Task 1 到 Task 8每个 Task 都严格由「【答案】」和「【原文】」两块组成答案块是压缩过的要点原文块是完整的口语对话或短文。Gretel 在伦敦街头看到一片圆顶硬礼帽、Mr. Clark 用 It takes all kinds to make a world 回应她那段答案只给六条短句原文却有完整的对话节奏和口语重复。这两块之间的落差正好是一份天然对齐的平行语料摘要和全文成对出现还带单元号和任务号。对做听力训练系统、TTS 语料准备、题库结构化的人来说这比一份散装 txt 有用得多。它适合两类人一类想把这本教材做成可检索、可自测的本地库另一类想拿它练手 docx 结构化抽取和 PDF 转档后的脏文本清洗。2. docx 解析实战用 python-docx 定位【答案】与【原文】块2.1 先搞清楚 docx 里装的是什么docx 本质是一个 zip 包正文在word/document.xml正文段落是w:p段内的文字被切成若干w:rrun。常见的坑是直接zipfile.read()加正则去标签这样能拿到文字但会丢失样式、会把分页符当空格、还会把页眉页脚一起混进来。更麻烦的是表格——Task 7 那张 Men/Women/Both 打勾表在 XML 里是w:tbldoc.paragraphs根本读不到它直接漏掉一整题。2.2 段落级抽取与结构摸底先不写解析逻辑把文档丢进解释器看一眼段落分布比对着 Word 界面猜结构靠谱得多。from docx import Document doc Document(现代大学英语听力2原文及答案.docx) # 只打印前 60 个字符避免刷屏重点看样式名和序号 for i, p in enumerate(doc.paragraphs[:400]): text p.text.strip() if not text: continue print(f{i:04d} | {p.style.name:12} | {text[:60]})p.style.name通常是Normal、Heading 1之类如果原文块被排成了独立样式这里能直接看出来后面就可以按样式切而不是按正则切。p.text只返回段内可见文字不会带 XML 标签但会保留全角引号和 U201B 这类字符先原样打印别急着替换。2.3 用状态机切出 Task 与两个块文档的结构信号很明确Task N开新任务【答案】和【原文】开新块1 / 140这种页码行是噪声。用一个三状态的小状态机就能切干净。import re TASK_RE re.compile(r^Task\s*(\d), re.I) BLOCK_RE re.compile(r^[【\[]\s*(答案|原文|Answer|Transcript)\s*[】\]]) PAGE_RE re.compile(r^\d\s*/\s*\d$) # 形如 3 / 140 的页脚残渣 def split_tasks(paragraphs): tasks, cur, block [], None, None for p in paragraphs: t p.text.strip() if not t or PAGE_RE.match(t): continue # 空段与页码直接跳过 m TASK_RE.match(t) if m: # 命中 Task 头开新任务 cur {task_no: int(m.group(1)), answer: [], transcript: []} tasks.append(cur) block None continue m BLOCK_RE.match(t) if m and cur is not None: # 命中块头切换写入目标 block answer if m.group(1) in (答案, Answer) else transcript continue if cur is not None and block: cur[block].append(t) return tasksTASK_RE用re.I兼容大小写BLOCK_RE同时吞掉全角【】和半角[]因为转档过程经常把两种括号混用。PAGE_RE只匹配「纯数字 / 数字」的整行避免误伤正文里 Task 2 提到的 minus thirty degrees 或日期。block变量取三个值None / answer / transcript任何块外的散句都不会被误收进去这比先切全文再回填安全。2.4 把段落和表格按文档顺序一起遍历Task 7 的答案主体在表格里必须按 body 的真实顺序走一遍否则段落顺序会错乱。from docx.oxml.ns import qn from docx.table import Table from docx.text.paragraph import Paragraph def iter_block_items(doc): 按文档流顺序产出 Paragraph 或 Table for child in doc.element.body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, doc) elif child.tag qn(w:tbl): yield Table(child, doc) for item in iter_block_items(doc): if isinstance(item, Table): # 表格按行拍平成 单元格1 | 单元格2 的文本行 for row in item.rows: print( | .join(c.text.strip() for c in row.cells)) else: print(item.text.strip())qn(w:p)做的是命名空间限定写死字符串{http://...}p也能跑但换个库版本就容易断。表格拍平后Task 7 那三段Men / Women / Both就变成普通文本行可以复用同一套状态机逻辑不必为表格单独写一条分支。3. 脏文本清洗U201B 引号、com 吞字与页脚残渣3.1 那份原料里的四类典型噪声把 Task 1 到 Task 8 的文本拉出来做一遍字符统计噪声分成四类规律非常明显。第一类是全篇的撇号用了 U201BSINGLE HIGH-REVERSED-9 QUOTATION MARKdidn‟t、Paul‟s、wasn‟t全是这个字符直接做字符串匹配会全部 miss。第二类是词间空格丢失出现as iftheywere、what agreattree这种连写。第三类是页脚1 / 140、5 / 140被混进正文流。第四类最隐蔽comparing life变成paring life、compact变成pact、combine变成bine、company变成pany丢失的全是com这个前缀。噪声类型原始片段目标形态处理方式U201B 撇号didn‟tdidnt字符级替换词间粘连as iftheywereas if they were词典切分页脚残渣3 / 140删除整行正则前缀吞字a big panya big company词表回补3.2 标点与空格的归一化字符级问题放在流水线最前面代价最低。U201B 和 U2019 都归一到 ASCII 单引号破折号—、–归一到-连续空白压成一个空格。粘连词的修复要谨慎——不能对整串无空格文本盲目插空格那会把St. Pauls也拆坏。import re, unicodedata PUNCT_MAP { \u201b: , \u2019: , \u2018: , # 各种单引号 \u201c: , \u201d: , # 各种双引号 \u2013: -, \u2014: -, \u2212: -, # 各种短横 } def normalize(text: str) - str: for src, dst in PUNCT_MAP.items(): text text.replace(src, dst) text unicodedata.normalize(NFKC, text) # 全角转半角 text re.sub(r[ \t\u00a0], , text) # 压缩空白 return text.strip()NFKC会把全角字母数字、部分全角标点折叠成半角但不会动中文汉字所以对中英混排的教材文本是安全的。PUNCT_MAP只处理确定的等价字符不做模糊映射——把–和—都归一成-是工程上的取舍因为这份语料里没有需要区分长短破折号的场景。3.3 用词频表回补被吞掉的前缀pany、pact、bine这些 residual token 有一个共同点加回常见前缀后就能命中高频英文词。用词频数据做判断比维护一张手工纠错表可扩展得多。from wordfreq import zipf_frequency import string PREFIXES [com, con, ex, in, un, re, pre, pro] def repair_token(tok: str) - str: core tok.strip(string.punctuation).lower() if len(core) 3: return tok if zipf_frequency(core, en) 2.5: return tok # 本身是常见词原样返回 for pre in PREFIXES: cand pre core if zipf_frequency(cand, en) 3.0: # 候选词足够常见才认 return tok.replace(core, cand, 1) return tok def repair_line(line: str) - str: return .join(repair_token(w) for w in line.split())zipf_frequency(word, en)返回 0 到 7 之间的对数频率7 表示最高频the一类2.5 大致对应「中等常见」3.0 对应「比较确定是真实词汇」。阈值不是死的如果语料里有大量专业术语把 2.5 调到 2.0 会更保守宁可漏修也不误改。PREFIXES只放英文里高频的构词前缀不放anti、semi这种容易撞车的避免把pact修成impact。注意前缀回补必须整篇批量跑并输出变更日志逐条人工确认后再落库。专有名词、人名、地名一律加白名单跳过。3.4 把清洗串成可回溯的流水线清洗最怕不可回溯。每一步都记下改了什么后面查错才有依据。def clean_pipeline(text: str, log: list) - str: before text text normalize(text) if text ! before: log.append((normalize, before[:40], text[:40])) before text text repair_line(text) if text ! before: log.append((prefix_repair, before[:40], text[:40])) return textlog用一个列表按顺序追加(阶段名, 修改前, 修改后)三元组跑完整个文档后按阶段名统计条数哪一步改动量异常大基本就是那一步的规则写宽了。这套结构比在每个函数里print更好用因为统计可以脱离运行环境单独做。4. Task 级结构化建模JSON 落盘与 SQLite 检索4.1 数据模型怎么切切分粒度决定后面能做什么。按两层建模最够用task表存单元号、任务号、答案块字符数、原文块字符数segment表存每个任务下的子条目用kind区分答案还是原文用seq保序。答案块内部的A.B.C.和1) 2)编号单独抽出来做item_no这样 Task 5 那种 A 是判断题、B 是对照表的混排结构不会丢层级。字段类型说明unitINTEGER单元号从Unit N提取task_noINTEGER任务号从Task N提取kindTEXTanswer或transcriptitem_noTEXT子题编号如A、1)无编号留空seqINTEGER同任务同 kind 下的顺序textTEXT清洗后的正文4.2 子条目切分答案和原文的切分规则不一样。答案块用编号开头的行切原文块用说话人标签或者自然段切。import re # 答案块A. / B. / C. 或 1) / 2) 开头 ANS_ITEM re.compile(r^([A-Z]\.|\d\))\s*) # 原文块John: / Etsuko: / Matthew: 这类说话人标签 SPK_ITEM re.compile(r^([A-Z][a-zA-Z\-]{1,15}):\s*) def split_items(lines, kind): pat ANS_ITEM if kind answer else SPK_ITEM items, cur_no, buf [], , [] for line in lines: m pat.match(line) if m: if buf: items.append((cur_no, .join(buf))) cur_no, buf m.group(1).rstrip(.)), [line[m.end():]] else: buf.append(line) if buf: items.append((cur_no, .join(buf))) return itemsANS_ITEM里\d\)用的是右括号而不是点号因为答案里的编号是1)而不是1.点号会和正文里的缩写句点撞车。SPK_ITEM限制了说话人标签长度在 2 到 16 个字符之间避免把句首的Yes:或Oh:误判成说话人——真出现这种误判加进一份说话人白名单过滤掉即可。4.3 落 SQLite 并做抽取质量校验CREATE TABLE task ( id INTEGER PRIMARY KEY, unit INTEGER NOT NULL, task_no INTEGER NOT NULL, UNIQUE (unit, task_no) ); CREATE TABLE segment ( id INTEGER PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES task(id), kind TEXT NOT NULL CHECK (kind IN (answer, transcript)), item_no TEXT DEFAULT , seq INTEGER NOT NULL, text TEXT NOT NULL ); CREATE INDEX idx_seg_task_kind ON segment(task_id, kind, seq);UNIQUE (unit, task_no)是防重跑的关键抽取脚本写错了重跑一次不会插出重复任务。CHECK约束把 kind 锁死成两个值后面做查询时不用再考虑第三个取值。索引建在(task_id, kind, seq)上按任务取答案或按任务取原文都是走索引的等值查找。抽取质量的第一个校验查询很朴素但能立刻发现异常SELECT t.unit, t.task_no, SUM(CASE WHEN s.kind answer THEN 1 ELSE 0 END) AS ans_segs, SUM(CASE WHEN s.kind transcript THEN 1 ELSE 0 END) AS trs_segs FROM task t LEFT JOIN segment s ON s.task_id t.id GROUP BY t.id ORDER BY t.unit, t.task_no;正常情况下每个 Task 的两列都应该大于 0。Task 5 的答案块含一张优缺点对照表段数会明显高于 Task 1如果某个 Task 的trs_segs是 0基本可以断定块头识别失败回查那一行的括号形态就行。5. 从静态文档到可检索听力库切句、对齐与召回5.1 句子级切分与 TTS 对齐原文块是口语材料句号问号分号混用还有Oh, just like the poem...这类引号内嵌。切句用轻量规则就够在.?!后跟空格加大写字母处断开但要排除Mr.St.St. Pauls这类缩写。切完后每条句子带一个全局递增序号落进sentence表后面 TTS 合成时按序号命名音频文件天然完成句级对齐。5.2 答案要点与原文句的相似度对齐答案块的要点是对原文的抽象比如答案写「They were all wearing dark suits and bowler hats」原文是同样内容加上 umbrellas 和 newspapers。这种句子用词重叠度做对齐比向量更稳也更便宜。文本向量化之后把句子切到 40 到 120 词一段再喂模型短句向量化后语义太稀召回会飘。5.3 用检索定位易错句把sentence表建成 FTS5 全文索引用bm25排序出题时按关键词反查原文句最方便。CREATE VIRTUAL TABLE sentence_fts USING fts5( text, task_no UNINDEXED, seq UNINDEXED, tokenize porter unicode61 ); SELECT task_no, seq, bm25(sentence_fts) AS score FROM sentence_fts WHERE sentence_fts MATCH bowler AND hat ORDER BY score LIMIT 5;tokenize porter unicode61里的 porter 做英文词干还原hats也能命中hatunicode61保证非 ASCII 字符不会被切碎。bm25()返回负值越小越相关所以按升序排。两个UNINDEXED字段是为了查询能直接回指到原始任务和句序不参与打分检索结果拿回去就能播放对应音频。这套结构跑通之后同一份 docx 既能当课堂材料也能当本地题库和 TTS 语料的来源加个单元筛选就是一个能用的自测页面。本文还有配套的精品资源点击获取