简介:这份《新视野大学英语(第三版)视听说教程2听力原文.pdf》面向使用该教材的大学英语学习者及备考四六级、需要强化听力训练的学生,解决视听说课堂中听力材料难以逐句核对、课后复习缺少文本参照的问题。资源包内含1个PDF文件,整体约320KB,轻量易存,可直接在手机或电脑上查阅,方便与教材音频对照使用。内容覆盖Unit 1“Life is a learning curve”等单元的Sharing、Listening等板块完整脚本,包含Hina与多位受访者关于学习西班牙语、法语、阿拉伯语、吉他、绘画、网站设计等经历的对话原文,并附有“What's the most difficult thing you've ever learned?”等讨论环节的逐字稿,便于精听、跟读与笔记整理。目前已有2523人学习下载,适合希望借助原文文本提升听力辨音能力、积累口语表达与话题素材的读者。
1. 一份听力原文 PDF 为什么值得做成可检索语料库
带过视听说课程的老师多半遇到过这种场景:学生课下想复盘一段对话,手里只有音频,没有文本,只能反复倒带听写,效率极低。新视野大学英语(第三版)视听说教程2听力原文.pdf 这类材料,本质上是把教材配套音频里的对话、短文、新闻片段转成可读文本,再按单元和任务编号整理成册。它的价值不在“多了一份文档”,而在于把不可检索的音频变成了可搜索、可标注、可二次加工的语言数据。适合三类人:需要做听写对照的任课教师、想把教材语料接入自己学习系统的开发者、以及做语料库语言学或教材分析的研究者。难点也很直接——PDF 里的文本往往混着页码、题号、音标符号和排版噪声,直接复制粘贴会带出一堆乱码,必须先做结构化清洗才能用。
2. 从 PDF 到结构化文本:抽取、清洗与分单元切分
2.1 先判断 PDF 是文本层还是扫描件
动手之前必须确认一件事:这份 PDF 是原生文本层,还是扫描图片。判断方法很简单,用命令行工具看一眼就能定性。
# 查看 PDF 前两页的文本抽取结果,有内容说明是文本层 pdftotext -f 1 -l 2 "听力原文.pdf" - | head -50 # 如果输出为空或只有零星字符,基本是扫描件,需要走 OCR pdfinfo "听力原文.pdf" | grep -i pages如果pdftotext能吐出成段英文,说明是文本层,后续用 Python 的 pdfplumber 或 PyMuPDF 直接抽取即可。如果输出为空,就得先渲染成图片再走 OCR,常见做法是用 pdftoppm 转图,再交给 Tesseract 或 PaddleOCR。这里有个血泪经验:扫描件的 OCR 结果里,数字 1 和字母 l、数字 0 和字母 O 经常混淆,听力原文里出现时间、编号时尤其明显,必须留人工校对环节。
2.2 用 pdfplumber 做逐页抽取并保留坐标
文本层 PDF 的抽取,我一般不用简单的extract_text(),因为听力原文常有分栏排版,直接抽会把左右栏串在一起。用 pdfplumber 拿到每个字符的坐标,再按 x 坐标聚类分栏,能大幅降低串行概率。
import pdfplumber def extract_by_column(pdf_path, page_no): with pdfplumber.open(pdf_path) as pdf: page = pdf.pages[page_no] # 按字符的 x0 坐标聚类,区分左右栏 words = page.extract_words(extra_attrs=["x0", "top"]) if not words: return "" # 以页面中线为界,简单二分左右栏 mid = page.width / 2 left = [w for w in words if w["x0"] < mid] right = [w for w in words if w["x0"] >= mid] # 各自按 top 排序后拼接 left_text = " ".join(w["text"] for w in sorted(left, key=lambda x: x["top"])) right_text = " ".join(w["text"] for w in sorted(right, key=lambda x: x["top"])) return left_text + "\n" + right_text print(extract_by_column("听力原文.pdf", 5))这段代码的关键参数是extra_attrs=["x0", "top"],它让每个词带上横纵坐标。mid取页面宽度一半作为分栏阈值,适合标准双栏教材排版。如果实际排版是左窄右宽,需要手动调整这个阈值。抽取结果里会混入页眉页脚的页码和单元号,下一步要过滤。
2.3 用正则清洗题号、页码与音标噪声
抽取出来的原始文本通常长这样:Unit 1 3 Conversation 1 /t/ 12。需要把单元标记、题号、音标符号剥离,只留对话正文。清洗规则要按实际噪声逐条加,不能一把梭。
import re def clean_transcript(raw): text = raw # 去掉页眉页脚里的页码(单独成行的数字) text = re.sub(r"^\s*\d{1,3}\s*$", "", text, flags=re.M) # 去掉 Unit X 标记 text = re.sub(r"Unit\s*\d+", "", text, flags=re.I) # 去掉题号如 1. 2. 3) 等行首编号 text = re.sub(r"^\s*\d+[\.\)]\s*", "", text, flags=re.M) # 去掉音标斜杠内容(简单匹配,复杂音标需更细规则) text = re.sub(r"/[^/\n]{1,20}/", "", text) # 合并多余空白 text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() sample = "Unit 1 3 Conversation 1 /t/ 12\n Hello, welcome to the campus." print(clean_transcript(sample))正则的顺序有讲究:先删独立页码,再删单元标记,最后删题号。如果把题号规则放前面,可能误伤正文里的数字。音标匹配用/[^/\n]{1,20}/是保守写法,只处理短音标,遇到长音标串需要放宽长度或改用更精确的字符集。清洗完必须抽 5 到 10 页人工过一遍,确认没有把正文误删。
2.4 按单元和任务编号切分成 JSON 语料
清洗后的整篇文本要切成结构化记录,方便后续检索和对照音频。切分依据是单元号和任务类型(Conversation、Passage、News 等)。
import json def split_units(full_text): # 以 Unit 数字为分隔符切分 pattern = re.compile(r"Unit\s*(\d+)", re.I) parts = pattern.split(full_text) units = [] # parts 结构:[前言, 单元号, 内容, 单元号, 内容...] for i in range(1, len(parts), 2): unit_no = int(parts[i]) content = parts[i + 1].strip() units.append({"unit": unit_no, "content": content}) return units with open("cleaned.txt", encoding="utf-8") as f: text = f.read() units = split_units(text) with open("transcripts.json", "w", encoding="utf-8") as f: json.dump(units, f, ensure_ascii=False, indent=2) print(f"共切分 {len(units)} 个单元")pattern.split会把单元号作为捕获组保留在结果里,所以步长取 2 正好拿到单元号和内容。如果原文单元标记不统一(有的写 Unit,有的写 UNIT),正则的re.I能兜住大小写。切分后建议打印每个单元的前 100 字做抽查,确认没有把两个单元粘在一起。
3. 把听力原文接进检索与对照系统
3.1 用 SQLite FTS5 建全文索引
结构化 JSON 只是中间态,要真正能搜,得进全文索引。SQLite 自带的 FTS5 扩展足够轻量,不需要额外服务。
import sqlite3, json conn = sqlite3.connect("listening.db") conn.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS transcripts USING fts5( unit, content, tokenize='porter unicode61' ) """) with open("transcripts.json", encoding="utf-8") as f: units = json.load(f) for u in units: conn.execute( "INSERT INTO transcripts(unit, content) VALUES (?, ?)", (u["unit"], u["content"]) ) conn.commit() # 测试检索 for row in conn.execute( "SELECT unit, snippet(transcripts, 1, '[', ']', '...', 10) " "FROM transcripts WHERE transcripts MATCH 'campus' LIMIT 3" ): print(row) conn.close()tokenize='porter unicode61'里的 porter 是英文词干还原,搜studies能命中study,对英语语料很实用。snippet函数返回带高亮标记的片段,第五个参数 10 表示片段最多 10 个词。如果要做中文检索,tokenizer 要换成unicode61并配合分词预处理,因为 FTS5 默认不切中文词。
3.2 音频时间戳与文本对齐的可行路径
如果手里同时有音频,可以把文本按句切分后做强制对齐,生成带时间戳的对照文件。常见做法是用轻量对齐工具跑一遍,输出每句的起止时间。
# 假设已有分句文本 sentences.txt 和音频 audio.mp3 # 用对齐工具生成 TextGrid 或 JSON 时间戳(工具名按实际环境替换) aligner --audio audio.mp3 --text sentences.txt --output aligned.json对齐的准确率取决于音频清晰度和文本是否逐字一致。听力原文里如果有省略、重复、口语填充词,对齐会漂移。我的习惯是先用前 30 秒做小样验证,确认偏移在可接受范围再跑全量。对齐结果存成{start, end, text}的列表,前端就能做点击句子跳转播放。
3.3 检索接口的最小实现
有了索引,包一层简单查询函数就能对外提供服务。参数上要控制返回条数和片段长度,避免一次吐太多。
def search(keyword, limit=10, snippet_len=15): conn = sqlite3.connect("listening.db") sql = """ SELECT unit, snippet(transcripts, 1, '<b>', '</b>', '...', ?) FROM transcripts WHERE transcripts MATCH ? LIMIT ? """ rows = conn.execute(sql, (snippet_len, keyword, limit)).fetchall() conn.close() return [{"unit": r[0], "snippet": r[1]} for r in rows] for item in search("library"): print(item["unit"], item["snippet"])snippet_len控制高亮片段长度,太小会截断上下文,太大返回臃肿,15 到 20 个词比较平衡。limit默认 10 条,前端做分页时再调大。注意 FTS5 的 MATCH 语法对特殊字符敏感,用户输入里带引号或星号要先转义,否则会抛语法错误。
4. 避坑与排查:抽取清洗中最容易翻车的五件事
4.1 现象:抽取结果左右栏交错,句子读不通
原因:PDF 是双栏排版,extract_text()按阅读顺序从上到下扫,把左栏底部和右栏顶部接在了一起。解决:改用坐标聚类分栏,如 2.2 节所示,以页面中线为界分别抽取再拼接。如果分栏不规则,用 pdfplumber 的page.crop()按区域裁切后逐块抽取。
4.2 现象:清洗后正文里的数字被误删
原因:题号正则^\s*\d+[\.\)]\s*加了行首锚定,但有些正文句子恰好以数字开头,比如2 people were talking。解决:把题号规则收紧为「数字加点或括号且后面紧跟大写字母或空格再大写」,或者先按行判断该行是否只有编号没有正文,再决定删不删。清洗后务必抽查含数字的段落。
4.3 现象:OCR 结果里 l 和 1、O 和 0 混淆
原因:扫描件字体较细或分辨率不足,OCR 模型区分度下降。解决:提高渲染分辨率到 300 DPI 以上再 OCR;对已知易混字符做上下文替换规则,比如单词中间出现的独立1大概率是l;最终保留人工校对环节,尤其是人名、地名和数字。
4.4 现象:FTS5 检索报fts5: syntax error near
原因:用户输入里带了 FTS5 的保留字符,如"、*、(、-。解决:查询前对关键词做转义,把特殊字符用双引号包起来,或者只保留字母数字和空格。更稳妥的做法是用参数化查询加预处理函数,把AND、OR、NOT等操作符也纳入白名单管理。
4.5 现象:单元切分后内容错位,第 3 单元混进第 2 单元
原因:原文里Unit标记出现多次,比如目录页也有 Unit 列表,正则把目录也当成了正文分隔符。解决:先定位正文起始页,只对正文范围做切分;或者在切分前删掉目录页内容。另一个办法是要求单元号严格递增,发现回退就合并处理。
5. 让语料真正好用的三个进阶技巧
第一个技巧是给每条记录补上任务类型标签。听力原文里 Conversation、Passage、News 的语体差异很大,检索时如果能按类型过滤,教学场景下会方便很多。做法是在切分阶段用正则识别任务标题,写入 JSON 的type字段,建索引时单独建一列。这样搜library可以限定只看 Conversation,避免短文里的同词干扰。
第二个技巧是做词频与难度分析。把清洗后的文本按单元统计词频,对照常见词表标注难度等级,能快速看出哪个单元的词汇负担偏重。实现上用一个 Counter 统计 token,再和词表做交集即可。参数上要注意先做小写化和词形还原,否则run和running会被算成两个词。这个分析结果可以直接用于备课时的重点词筛选。
第三个技巧是保留原始页码映射。清洗和切分过程中,把每条记录对应的 PDF 页码存下来,检索结果里带上页码,用户就能快速定位到原文档核对。实现方式是在抽取阶段把page.page_number一并写入,切分时按位置继承。这个字段平时不显眼,但一旦需要人工复核,能省下大量翻页时间。
| 字段 | 类型 | 说明 |
|---|---|---|
| unit | int | 单元编号 |
| type | str | 任务类型,如 Conversation |
| content | str | 清洗后的正文 |
| page | int | 对应 PDF 页码 |
| start | float | 音频起始秒,可选 |
| end | float | 音频结束秒,可选 |
这张表就是最终语料的最小字段集,多出来的字段按需加,但 unit、content、page 这三个建议从一开始就保留。我自己做这类项目时,最早图省事只存了正文,后来要核对原文只能靠记忆翻页,返工成本很高。现在养成的习惯是:抽取阶段能拿到的元数据,全部落库,用不用得上以后再说。希望帮到你。
本文还有配套的精品资源,点击获取