学习英语听力一直是很多同学的痛点材料听了、文本也看了但最后留在脑子里的内容却很少。其实问题往往不在于“听”得不够多而在于缺少一套可复用的整理与复习流程。这篇文章不准备讲英语本身而是带你用 Python 实现一个“慢速英语精听素材处理”工作流。我会用一篇标题为《Mom And Dad Split Home: Can This Family Heal?》的慢速英语内容作为示例把字幕清洗、句子切分、生词提取、Anki 卡片生成和音频切片串成一套完整工具。读完以后你拿到任意一篇听力文本或 SRT 字幕都可以按同一套流程快速生成自己的精听学习包。慢速英语听力材料有一个特点播报速度慢、词汇相对基础、句子结构比较完整非常适合做精听和跟读训练。但原始文本往往带着字幕时间轴、空行、换行和大量重复标记直接拿来阅读并不舒服如果想把其中的生词整理成 Anki 单词卡还需要手动复制粘贴。整个过程既重复又容易出错而这正是 Python 脚本的价值所在。我建议读者具备基础的 Python 语法能力比如函数、文件读写、正则表达式如果你还没系统学过这些也能通过本文看懂整体设计先把脚本体复制运行起来。下面进入正题。1. 这个案例要解决什么问题1.1 听力学习中的“素材处理负担”当我们拿到一篇慢速英语材料时完整学习链路通常包括泛听、逐句精听、对照文本、查生词、做跟读、记录复习卡片。麻烦的地方在于从第二步开始我们就需要频繁地在文本、词典、Anki 之间来回切换。比如一段听力原文中出现了句子Mom and Dad split home. Can this family heal?你打算把这句话做成卡片记录单词split和heal的新含义。手动操作只需要两分钟但如果你想积累一万句、五千个生词那么这个工作量的放大效应就很明显了。更现实的问题是原文本中通常不只包含干净句子它可能是这样的 SRT 字幕文件1 00:00:00,000 -- 00:00:04,080 Mom and Dad split home. 2 00:00:04,080 -- 00:00:08,126 Can this family heal?这段内容里序号和时间轴是给播放器用的对学习者阅读并不友好。如果能把它们自动清洗掉再按句子结构重新组织就会得到一份更清爽的学习文本。1.2 为什么用 Python 而不是纯手工整理纯手工整理不是不行而是很难坚持。每天精听一篇材料每篇可能有两三百个词手动去标题、去时间轴、逐句复制到单词软件很消耗意志力。Python 脚本可以用几秒钟完成重复劳动让你把精力放到真正重要的“听、说、读、记”上。下面用一个简单场景说明假设你有 10 篇慢速英语文本每篇 300 个词。手工清洗 10 篇文本大概需要 30 分钟写脚本的初始成本可能也是 30 分钟但脚本只要维护一次后面新增 100 篇都不需要再花额外时间。这就是把学习工具化的意义。本文用《Mom And Dad Split Home: Can This Family Heal?》这个主题作为处理样例既能覆盖文本清洗的典型难点又能演示如何把“句子级学习数据”提取出来。1.3 本文会实现的最终效果整套脚本包含四部分能力第一读取 SRT 字幕或普通 TXT 文本第二清洗时间轴、序号并切分句子第三提取生词并生成带上下文的 CSV 学习表第四可选地根据字幕时间轴切分音频片段。最终输出结构是output/ ├── clean_text.txt # 清洗后的纯文本 ├── sentences.csv # 句子编号、句子原文、单词数 └── vocabulary.csv # 单词、词频、上下文例句、候选备注字段有了这些文件你可以导入自己的单词本也可以配合 Anki 制卡。2. 准备工作与运行环境在开始写代码前先说明本文使用的环境。我采用 Windows 11 系统 Python 3.11 演示但脚本本身只依赖标准库不强制要求特定操作系统。macOS 和 Linux 同样可以运行区别只在 Python 安装方式和路径写法。主要环境清单如下依赖项说明操作系统Windows 11 / macOS / Ubuntu 均可Python 版本3.9 及以上3.11 更佳三方依赖无全部使用标准库文本编辑器VS Code 或任意支持 UTF-8 的编辑器示例素材自建 SRT 字幕文件用于演示非原文全文本文刻意不引入第三方依赖原因是这类工具脚本需要在不同电脑间复制。如果依赖 pandas、lxml安装步骤就会变复杂。标准库里的re、csv、pathlib、subprocess已经足够完成核心功能。建议先把项目目录建立成下面这样fast-english-tools/ ├── data/ │ ├── example.srt │ └── example.txt ├── scripts/ │ ├── preprocess.py │ ├── sentences.py │ └── vocabulary.py ├── output/ └── README.md在终端中依次创建目录。output目录可以一开始就存在便于脚本运行时写入也可以让脚本用mkdir自动创建。下面我们进入目录结构说明。2.1 为什么需要固定的项目结构很多人写工具脚本习惯把所有文件放在一个目录里。一旦处理材料多了目录会变得混乱。固定结构的好处有三个第一输入、脚本、输出互不干扰避免误删第二后续接入新素材时只需放文件到data目录第三脚本内部用相对路径定位文件换机器时也能快速迁移。为了让代码更健壮我在脚本开头统一从Path(__file__)推导项目根目录而不是使用容易出错的绝对路径。比如在scripts/preprocess.py中这样定位目录from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent DATA_DIR BASE_DIR / data OUTPUT_DIR BASE_DIR / output if not OUTPUT_DIR.exists(): OUTPUT_DIR.mkdir(parentsTrue, exist_okTrue)这段代码会自动找到项目根目录然后把output目录创建出来。这里用Path(__file__).resolve()是为了避免脚本从别的目录被调用时找不到路径。2.2 示例素材的准备由于版权原因我不会在文中给出完整听力原文。制作示例文件data/example.srt时可以手动输入下面这样的测试内容1 00:00:00,000 -- 00:00:04,080 Mom and Dad split home. 2 00:00:04,080 -- 00:00:08,126 Can this family heal? 3 00:00:08,126 -- 00:00:12,505 They sit together at the kitchen table.请注意这只是一个格式样例用来验证脚本是否能正确去掉序号和时间轴。真实学习时替换为你自己的合法听力材料即可。如果你手上只有普通 TXT 文本那么直接准备一份纯文本就行脚本也能处理。2.3 验证 Python 环境在终端输入python --version如果可以输出版本号说明 Python 已经安装。如果提示“python 不是内部或外部命令”在 Windows 上可以尝试py --version确认环境正常后下面先安装不需要任何依赖直接进入代码阶段。3. 模块设计分步骤理解处理流程整套程序的本质可以概括为一个流水线原始文本 - 清洗 - 句子切分 - 单词统计 - 学习文件输出如果还需要切音频则在清洗阶段保留字幕时间轴等句子切分后再根据句子的时间范围调用 FFmpeg 切分音频。我们不需要把它们写成一个巨大的 Python 文件。拆成多个模块每个模块只负责一个阶段能显著降低维护成本。下面分别介绍三个脚本的职责脚本核心职责输出preprocess.py清洗 SRT / TXT干净文本sentences.py切分句子并统计句子 CSVvocabulary.py提取生词、生成语境例句生词 CSV下面按次序实现。4. 实现文本清洗模块4.1 编写 preprocess.py先写一个最小但完整的文本清洗脚本。它的任务是读取data目录中的文件把 SRT 时间轴、序号、空行以及无意义的标签全部清理掉。这里需要用到正则表达式。SRT 字幕时间轴一般长这样00:00:00,000 -- 00:00:04,080注意时间中的毫秒是用逗号分隔的。VTT 字幕则可能使用点号例如00:00:00.000 -- 00:00:04.080为了兼容两种格式匹配时间轴的正则要写成TIME_RE re.compile( r\d{2}:\d{2}:\d{2}[,.]\d{3}\s*--\s*\d{2}:\d{2}:\d{2}[,.]\d{3} )其中[,.]表示逗号或点号都可以\s*表示时间轴箭头前后可能有空格。完整代码如下# 文件路径scripts/preprocess.py import re from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent DATA_DIR BASE_DIR / data OUTPUT_DIR BASE_DIR / output TIME_RE re.compile( r\d{2}:\d{2}:\d{2}[,.]\d{3}\s*--\s*\d{2}:\d{2}:\d{2}[,.]\d{3} ) def clean_srt_text(text: str) - str: 清理 SRT/VTT 字幕只保留字幕文本内容。 cleaned_lines [] for raw_line in text.splitlines(): line raw_line.strip() if not line: continue if line.isdigit(): # 字幕序号例如 1、2、3 continue if TIME_RE.search(line): # 时间轴 continue if line.startswith(WEBVTT): continue if line.startswith(Kind:) or line.startswith(Language:): continue cleaned_lines.append(line) return \n.join(cleaned_lines) def clean_txt_text(text: str) - str: 普通文本清洗去掉多余空行和行尾空格。 cleaned_lines [] for raw_line in text.splitlines(): line raw_line.strip() if line: cleaned_lines.append(line) return \n.join(cleaned_lines) def read_text_file(file_path: Path) - str: 以 UTF-8 读取文件。 return file_path.read_text(encodingutf-8) def save_text_file(file_path: Path, content: str) - None: 以 UTF-8 写入文件。 file_path.write_text(content, encodingutf-8) def main(): # 尝试读取 SRT如果文件不存在则读取 TXT srt_path DATA_DIR / example.srt txt_path DATA_DIR / example.txt if srt_path.exists(): raw_content read_text_file(srt_path) clean_content clean_srt_text(raw_content) source_name example.srt elif txt_path.exists(): raw_content read_text_file(txt_path) clean_content clean_txt_text(raw_content) source_name example.txt else: raise FileNotFoundError(data 目录下没有 example.srt 或 example.txt) # 输出清洗后的文本 out_path OUTPUT_DIR / clean_text.txt save_text_file(out_path, clean_content) print(f[OK] 已从 {source_name} 清洗文本输出到 {out_path}) print(clean_content) if __name__ __main__: main()这段脚本最关键的是“过滤”逻辑。有人可能想问为什么不用一句.replace()把所有--删除因为时间轴在一行中可能前后还有文本如果只删除--残留的毫秒时间和数字仍然会混进文本里。按行判断再跳过整行逻辑更稳定。4.2 运行清洗脚本在项目根目录打开终端运行python scripts/preprocess.py如果data/example.srt中的内容是我上面给的样例文本那么输出文件output/clean_text.txt内容应为Mom and Dad split home. Can this family heal? They sit together at the kitchen table.这里要注意真实听力材料有时会出现对话跨行的情况例如一行只有Can this下一行才是family heal?。清洗脚本只负责去除格式不会强行合并断行。合并句子逻辑放在下一个模块中处理。5. 实现句子切分与统计模块5.1 句子切分的基本思路清洗后的文本依然可能是多行结构我们需要把它转换成“一行一句”的结构。慢速英语文本中句子边界通常有句号、问号或感叹号但英文缩写也有点号比如Dr. Smith lives in New York.这里的Dr.不是句子结束。因此简单按.切分会出错。本文作为入门版采用一个折中方案先用空行或换行把字幕块分开如果整段没有明显分隔再按“标点 空格”的规则切分。更精确的方案是使用 NLTK 或 spaCy但这会增加环境安装成本。对于慢速英语材料Dr.、Mr.这类缩写出现频率不高我们可以在一开始把常见缩写替换为占位符避免破坏句子边界。下面的示例先处理“按句号、问号、感叹号后跟空格切分”的情况# 文件路径scripts/sentences.py import re import csv from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent DATA_DIR BASE_DIR / data OUTPUT_DIR BASE_DIR / output # 常见缩写占位替换防止被错误切分 COMMON_ABBREVIATIONS { Dr.: Dr, Mr.: Mr, Mrs.: Mrs, Ms.: Ms, St.: St, } def protect_abbreviations(text: str) - str: 将常见缩写中的点替换为占位符句子切分后再还原。 for abbr, placeholder in COMMON_ABBREVIATIONS.items(): text text.replace(abbr, placeholder) return text def restore_abbreviations(text: str) - str: 把占位符还原为缩写。 for abbr, placeholder in COMMON_ABBREVIATIONS.items(): text text.replace(placeholder, abbr) return text def split_sentences(text: str) - list[str]: 将文本切分为句子。 这里以 . ! ? 后跟空白作为边界。为了保留缩写 在真正切分之前先替换常见缩写。 text protect_abbreviations(text) # 合并换行把多个空白压成一个空格 text re.sub(r\s, , text) # 按句子结束标点切分 parts re.split(r(?[.!?])\s, text) sentences [] for part in parts: part restore_abbreviations(part.strip()) if part: sentences.append(part) return sentences def count_words(sentence: str) - int: 统计句子中的英文单词数。 return len(re.findall(r[A-Za-z], sentence)) def load_clean_text() - str: clean_path OUTPUT_DIR / clean_text.txt if not clean_path.exists(): raise FileNotFoundError(请先运行 preprocess.py, 生成 clean_text.txt) return clean_path.read_text(encodingutf-8) def save_sentences_csv(sentences: list[str]) - None: out_path OUTPUT_DIR / sentences.csv with open(out_path, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([sentence_id, sentence, word_count]) for idx, sentence in enumerate(sentences, start1): writer.writerow([idx, sentence, count_words(sentence)]) print(f[OK] 已生成句子统计文件{out_path}) def main(): text load_clean_text() sentences split_sentences(text) save_sentences_csv(sentences) print(切分后的句子) for idx, sentence in enumerate(sentences, start1): print(f{idx}: {sentence}单词数 {count_words(sentence)}) if __name__ __main__: main()5.2 为什么把句子输出成 CSVCSV 是跨软件交换数据时使用最广泛的表格格式之一。Excel、Numbers、Python 的csv库都能直接处理。输出 CSV 后后续不仅可以用脚本提取单词还可以直接用 Excel 对句子做筛选、统计和人工校对。csv.writer在写入时需要设置newline这是 Python 官方文档推荐的做法不然在 Windows 上可能出现多一个空行的情况。这是一个很容易被忽略的细节。运行下面命令python scripts/sentences.py预期输出切分后的句子 1: Mom and Dad split home.单词数 5 2: Can this family heal?单词数 4 3: They sit together at the kitchen table.单词数 7关于单词数统计我目前用的是[A-Za-z]这个正则会把它当作一个单词。对于its这类缩写它会被统计为 1 个单词如果想拆成it和is两个词需要更复杂的词法处理暂不展开。6. 实现生词提取与学习卡片模块6.1 文本到词频统计的流程学习听力材料时生词通常具备两个特征第一它不在你的常用词汇表里第二它在当前文本中对理解句子产生阻碍。计算机无法真正理解“阻碍”但我们可以用词频来辅助筛选全文中反复出现、且不属于停用词的单词很值得优先掌握。处理流程如下句子切分结果 - 单词小写化 - 去标点 - 过滤停用词 - 词频统计 - 生成 CSV在组织数据时最有价值的字段不是孤立的单词而是单词出现的上下文句子。因此最终生成的 CSV 应有两类文件word_frequency.csv词频表。vocabulary.csv生词 首次出现位置 上下文句子可直接用于制卡。下面是scripts/vocabulary.py的实现# 文件路径scripts/vocabulary.py import csv import re from collections import Counter, defaultdict from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent OUTPUT_DIR BASE_DIR / output STOP_WORDS { the, a, an, and, or, but, of, in, on, at, to, for, with, by, as, is, are, was, were, be, been, being, do, does, did, have, has, had, it, this, that, these, those, i, you, he, she, we, they, them, his, her, their, our, your, my, from, about, into, over, after, before, can, could, will, would, should, may, might, must, not, no, so, if, then, than, when, where, which, who, whom, because, but, also, there, here, what, } def tokenize(text: str) - list[str]: 提取英文单词保留撇号其他标点不作为单词内容。 return re.findall(r[A-Za-z], text.lower()) def read_sentences_csv(): path OUTPUT_DIR / sentences.csv if not path.exists(): raise FileNotFoundError(请先运行 sentences.py) rows [] with open(path, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: rows.append(row) return rows def is_basic_word(word: str) - bool: 过滤停用词和过短词。 if word in STOP_WORDS: return True if len(word) 2: return True return False def build_vocabulary_data(sentence_rows: list[dict]) - None: word_counter: Counter[str] Counter() word_first_sentence: dict[str, str] {} word_sentence_id: dict[str, str] {} for row in sentence_rows: sentence row[sentence] sentence_id row[sentence_id] words tokenize(sentence) for word in words: if is_basic_word(word): continue word_counter[word] 1 if word not in word_first_sentence: word_first_sentence[word] sentence word_sentence_id[word] sentence_id frequency_path OUTPUT_DIR / word_frequency.csv with open(frequency_path, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([word, frequency]) for word, count in word_counter.most_common(): writer.writerow([word, count]) vocab_path OUTPUT_DIR / vocabulary.csv with open(vocab_path, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([word, frequency, sentence_id, context_sentence]) for word, count in word_counter.most_common(): writer.writerow([ word, count, word_sentence_id.get(word, ), word_first_sentence.get(word, ), ]) print(f[OK] 单词统计文件{frequency_path}) print(f[OK] 生词学习文件{vocab_path}) def main(): rows read_sentences_csv() build_vocabulary_data(rows) print(提取到的非停用词) vocab_path OUTPUT_DIR / vocabulary.csv with open(vocab_path, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for idx, row in enumerate(reader): if idx 20: break print(f{row[word]}: {row[context_sentence]}) if __name__ __main__: main()这段脚本的默认STOP_WORDS并不完整。它只是一个基础集合。真实学习中你会发现自己的常用词表与别人不同比如你非常熟悉kitchen就不需要把它列为生词但对另一个学习者来说它可能是生词。所以脚本里没有删除任何词只是把它筛选出来。你可以在生成 CSV 后在 Excel 里按自己的实际水平删掉不认识的常用词或者把不需要的词从表里删除。用户自定义生词表也是一种必要的人工干预。6.2 运行生词提取模块在终端运行python scripts/vocabulary.py输出结果中会看到类似下面的单词列表kitchen: They sit together at the kitchen table. family: Can this family heal?parent、split等词如果出现在文本中也会被提取出来。由于这篇示例文本很短词频表中的词数不会很多。真实新闻材料通常在 300 到 500 词之间输出会更丰富。6.3 为什么要保留“上下文例句”而不是只给单词很多背单词软件的卡片只显示“单词 中文释义”。这样做效率不高因为脱离语境后用户很难记住单词在不同句子中的真实用法。把单词放进原句例如单词split 原句Mom and Dad split home.当你在 Anki 中复习时看到split会先想“它在这句话里是什么意思”再回想“home 为什么和 split 搭配”。这个过程比孤立记忆更接近语言习得的真实场景。如果需要扩展中文释义你可以把vocabulary.csv导入 Excel手动增加一列meaning或者在生成卡片后使用词典 API 自动补全。为保持脚本简单本文不引入在线词典请求。7. 进阶功能根据字幕时间轴切分音频7.1 切分音频的适用场景在精听训练中很多人会想把每一句单独截成一小段音频循环播放。这样就不用来回拖拽播放器进度条。如果字幕文件里有时间轴我们就能自动把每一句对应的音频切出来。前提是你已经拥有该音频文件的合法副本并且只用于个人学习。不要对未经授权的受版权保护内容进行二次分发。切分音频使用的是 FFmpeg 命令。它不是 Python 模块而是一个独立的命令行工具。安装方式因系统而异Ubuntusudo apt update sudo apt install ffmpegmacOSbrew install ffmpegWindows从 FFmpeg 官网下载压缩包并配置 PATH。安装后在终端验证ffmpeg -version看到版本信息后就可以在 Python 中通过subprocess调用。7.2 解析 SRT 时间轴并生成切片为了得到每个句子的起始时间和结束时间需要先把 SRT 文件解析成结构化数据。上一节中的preprocess.py把时间轴丢掉了所以在音频切片场景下我们应该直接从原始 SRT 文件读取。下面给出一个简易解析函数它把字幕块拆成录音时间区间和文本内容# 文件路径scripts/split_audio.py import re import subprocess from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent DATA_DIR BASE_DIR / data AUDIO_DIR BASE_DIR / audio SRT_TIME_LINE_RE re.compile( r(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*--\s*(\d{2}:\d{2}:\d{2}[,.]\d{3}) ) def srt_time_to_seconds(time_str: str) - float: 把 00:00:01,500 或 00:00:01.500 转成秒。 time_str time_str.replace(,, .) hours_str, minutes_str, seconds_str time_str.split(:) hours int(hours_str) minutes int(minutes_str) seconds float(seconds_str) return hours * 3600 minutes * 60 seconds def parse_srt_blocks(srt_content: str) - list[dict]: 解析 SRT返回带 start/end/text 的字典列表。 blocks re.split(r\n\s*\n, srt_content.strip()) items [] for block in blocks: lines block.strip().splitlines() if not lines: continue timestamp_line None text_lines [] for line in lines: if -- in line: timestamp_line line elif line.strip().isdigit(): continue else: text_lines.append(line.strip()) if timestamp_line is None or not text_lines: continue match SRT_TIME_LINE_RE.search(timestamp_line) if not match: continue start_text, end_text match.groups() start_sec srt_time_to_seconds(start_text) end_sec srt_time_to_seconds(end_text) items.append({ start: start_sec, end: end_sec, text: .join(text_lines), }) return items def find_audio_file() - Path: audio_candidates list(AUDIO_DIR.glob(*.mp3)) list(AUDIO_DIR.glob(*.m4a)) if not audio_candidates: raise FileNotFoundError(audio 目录中没有 mp3/m4a 文件) return audio_candidates[0] def split_single_audio(audio_path: Path, start: float, end: float, out_path: Path) - None: cmd [ ffmpeg, -y, -i, str(audio_path), -ss, str(start), -to, str(end), -c, copy, str(out_path), ] subprocess.run(cmd, checkTrue, capture_outputTrue, textTrue)实际执行时还需要保证输出文件名不包含特殊字符。建议使用句子编号作为文件名例如def build_output_filename(index: int, sentence: str) - str: short_sentence re.sub(r[^A-Za-z], _, sentence)[:40].strip(_) if not short_sentence: short_sentence fsentence_{index} return f{index:03d}_{short_sentence}.mp3这样生成的文件像001_Mom_and_Dad_split_home_.mp3直观且不容易冲突。需要提醒的是用-c copy可以避免重新编码速度很快但它对音频时间点的切割精度取决于音频编码格式某些场景下可能出现前后几十毫秒的偏差。如果对精度要求高可以去掉-c copy改为重新编码ffmpeg -i input.mp3 -ss 0.0 -to 4.08 -c libmp3lame output.mp3但重新编码更慢也会造成音质损耗。对于日常精听练习-c copy通常足够用了。7.3 音频切片与字幕文本结合为了让后续操作中能同时拿到“音频文件名”和“字幕文本”最方便的方式是生成一份segments.csv包含以下字段segment_id,start_seconds,end_seconds,audio_file,sentence很多移动端单词卡工具支持“文本 音频”组合。把audio_file字段加入卡片就可以在例句播放时同步听到原声音频真正把听力训练和词汇复习结合起来。这里不继续扩展完整 GUI 或数据库部分。你可以先用少量音频测试切分效果确认起点和终点没有明显偏移后再批量处理整篇材料。8. 用示例材料完整跑一遍8.1 从 SRT 到学习卡片假设项目目录已经建立data/example.srt内容如下1 00:00:00,000 -- 00:00:04,080 Mom and Dad split home. 2 00:00:04,080 -- 00:00:08,126 Can this family heal? 3 00:00:08,126 -- 00:00:12,505 They sit together at the kitchen table.依次执行三行命令python scripts/preprocess.py python scripts/sentences.py python scripts/vocabulary.py第一次执行后建议把三个脚本的输出都打开检查。clean_text.txt应没有时间轴和序号sentences.csv里的每一行应是一句完整的话vocabulary.csv中提取的词表应基本合理。如果有不该出现的单词进入词表比如the说明STOP_WORDS没有覆盖它。你可以在集合里补充也可以直接忽略因为高频停用词并不全是生词。8.2 遇到的问题排查这里先列几个运行过程中可能出现的问题。问题现象常见原因解决思路运行脚本提示找不到文件终端不在项目根目录先cd到项目根目录再执行中文路径报错Windows 控制台编码问题代码中读写文件统一指定encodingutf-8CSV 用 Excel 打开中文乱码CSV 默认用 UTF-8 编码Excel 需要使用 UTF-8-SIG写入 CSV 时可用encodingutf-8-sig正则匹配不到时间轴字幕格式是 WebVTT毫秒用点号时间轴正则中已兼容[,.]如果仍失败需检查原始格式句子单词数统计偏少连字符或撇号处理规则不一致根据需求调整tokenize正则这里专门说一下中文乱码问题。Python 的open()默认编码在 Windows 上可能是gbk而脚本中如果用encodingutf-8写入Excel 打开时又可能猜成gbk于是出现乱码。为了避免这个问题可以把 CSV 文件的编码从utf-8改成utf-8-sig。utf-8-sig会在文件开头加入 BOM 标记Excel 更容易识别。如果你使用 WPS 或 macOS 的 Numbers通常不需要这个设置但写成utf-8-sig也不影响正常使用。修改sentences.py中写入 CSV 的那一行即可with open(out_path, w, encodingutf-8-sig, newline) as f:同理vocabulary.py中写入 CSV 的地方也改成utf-8-sig。9. 数据处理与工程化建议9.1 不要在原始文件上直接修改处理听力素材时最大的风险是破坏原始文件。如果你在一个脚本中同时完成“读取 - 清洗 - 覆盖源文件”这三件事一旦清洗逻辑有 bug原始文本就没了。即使不怕内容丢失也需要重新下载而有些下载来源不一定能保证长期有效。更好的方式是坚持输入和输出分离。原始文件永远放在data目录处理结果统一输出到output目录。如果某一步输出结果不理想重新调整脚本后再次运行即可不需要担心破坏素材。9.2 函数职责单一化从本文看到每个脚本都很短每个函数只做一件事清洗、切分、生成 CSV。这样的代码即使有缺陷定位也很快。相反如果写成一个 500 行的单文件阅读和测试成本都会增加。在后续扩展时建议为纯函数添加简单测试。例如split_sentences可以这样验证def test_split_sentences(): text Mom and Dad split home. Can this family heal? result split_sentences(text) assert len(result) 2 assert result[0] Mom and Dad split home. assert result[1] Can this family heal? print(test passed)如果你没用过pytest也可以只在if __name__ __main__:里调用这种验证函数。无论项目大小核心逻辑有自动化验证都是防止重构时引入回归的好习惯。9.3 版权与合规慢速英语类材料通常有正式发布的文字稿与音频。自己做学习笔记没有问题但要注意不要批量抓取未经授权的站点内容。不要把下载的音频和文本重新打包上传到公开平台。生成的学习卡片不要包含第三方付费课程或未授权复制的讲义。如果在公司或组织内部分享这类工具也应该确保输入的素材来源有合法授权。这一点与写代码本身同样重要。9.4 从“手动查词”到“更自动化的流程”本文的生词提取只做了“机器初筛”没有自动获取词性和中文释义。这是因为自动补全释义往往依赖第三方词典 API每家 API 的字段、限额和授权方式都不同写死会造成文章失效。你可以继续扩展的方向有两个。第一个方向是调用本机词典软件把单词写入剪贴板或生成一个.txt文件再配合欧路词典、GoldenDict 的批量查词功能。第二个方向是接入公开词典 API解析 JSON 返回结果并更新到 CSV 中。实现前记得查看服务条款与配额限制。无论选择哪个方向人工校对仍然是最后一道关。机器会把数据给你整理好但哪些词值得放进复习队列最终由你的学习目标决定。10. 常见问题与排查思路为了减少调试时间这里集中列一批实践中容易遇到的问题。10.1 正则切分失败如果原文中的句子结尾没有空格例如Mom and Dad split home.Can this family heal?那么(?[.!?])\s这个正则无法切分因为home.和Can之间没有空白。现实中这类格式很少但不排除复制文本时丢失空格。解决方案先做“标点后补空格”的预处理再切分。例如text re.sub(r([.!?])(?[A-Z]), r\1 , text)这个正则会匹配.或?後紧跟大写字母的场景并在中间补一个空格。慢速英语字幕文本里如果每行是一句完整句子这个问题就不严重。10.2 SRT 文件名编码错误读取 SRT 时少数文件不是 UTF-8 编码而是UTF-8 with BOM或ANSI。直接read_text(encodingutf-8)可能会抛异常或者读出的文本首行包含\ufeff字符。解决办法是使用utf-8-sig读取content Path(example.srt).read_text(encodingutf-8-sig)utf-8-sig可以自动去掉开头的 BOM。你可以把read_text_file函数的编码从utf-8改为utf-8-sig这样兼容性更好。10.3 脚本输出正常但 Excel 打开 CSV 出现大量逗号错位原因通常是句子中包含英文逗号而 CSV 写入时没有正确加引号。Python 的csv.writer会默认处理这种情况会把包含逗号的字段用引号包裹。如果你是用字符串拼接去写 CSV则很可能出现问题。所以统一使用csv.writer不要自己用write(f{sentence}, {word}\n)拼接。这个坑非常常见值得在代码注释中提醒。10.4 FFmpeg 不在系统 PATH 中如果subprocess.run(cmd)抛出了FileNotFoundError说明系统找不到ffmpeg命令。验证方式是在终端单独运行ffmpeg -version如果命令不存在需要先安装或在 Python 中传入 FFmpeg 的绝对路径。例如FFMPEG_BIN rD:\ffmpeg\bin\ffmpeg.exe建议不要用绝对路径写死在公共脚本里因为换一台电脑就会失效。可以把FFMPEG_BIN提成一个配置变量放在脚本顶部让使用者自行维护。10.5 文本行数很多导致程序崩溃如果你的语料库非常大比如一次性处理几十万字利用read_text一次读入仍然可行但要注意 Python 的内存占用通常不会很大几十万字文本只有不到 10 MB普通电脑可以承受。如果是处理上 GB 的音频切片那就不要把所有音频一次性读入内存而是逐句调用 FFmpeg 输出。本文提供的方法就是逐句执行占用内存很少。11. 最佳实践与后续扩展11.1 给脚本增加配置开关当前脚本把输入文件名直接写死在代码中。最佳实践是把“输入材料”和“处理逻辑”解耦。例如新增一个config.pySRT_FILE data/example.srt AUDIO_FILE audio/example.mp3 OUTPUT_ENCODING utf-8-sig CLEAN_OUTPUT output/clean_text.txt这样当你要换一篇材料时不需要阅读整个脚本找硬编码路径只改配置文件即可。为了降低本文复杂度我没有把配置单独拆出来但在实际项目中强烈推荐。11.2 结合间隔重复制卡vocabulary.csv可以直接导入 Anki。Anki 是开源的间隔重复记忆软件导入前需要先设置模板字段。模板可以设计成正面和反面两个字段。正面显示地道的原声句子或单词反面显示翻译或例句播放按钮。在 Anki 中新建笔记类型时字段大致设置为Word Context Meaning Audio然后从 Anki 的“文件 - 导入”中选择vocabulary.csv。注意修改分隔符选项默认是 Tab 还是逗号取决于你的文件。按本文生成的是逗号分隔 CSV导入时需要指定对应符号。如果你不想让单词出现重复可以在导入前按字段word做去重或者直接沿用生成文件里的词频字段把频率大于 1 的单词优先处理。11.3 加入词形归并之后再统计词频目前程序把heal和heals当成两个单词把family和families也当成两个单词。从背单词角度看它们属于同一个词族一次性掌握更高效。要实现这一点可以引入词形还原工具例如 NLTK 的WordNetLemmatizer或spaCy。安装 NLTK 后可以这样处理from nltk.stem import WordNetLemmatizer lemmatizer WordNetLemmatizer() print(lemmatizer.lemmatize(families)) # family print(lemmatizer.lemmatize(heals)) # heal但 NLTK 需要额外下载语料数据会让脚本从“零依赖”变成“五步安装”。文中的基础版足够日常使用如果你已经熟悉 Python 包管理再自行扩展词形归并也不迟。11.4 输出“精听句子包”供移动端使用移动端背单词工具通常支持通过 CSV 或 Excel 导入自定义卡片。你可以把本文生成的sentences.csv中每个句子作为一条听力例句卡左声道保留完整句子必要时可切分句首和句尾。对慢速英语学习者来说最好的操作路径其实是“听一句 - 暂停 - 复述 - 对照文本 - 复习生词”。整个过程需要音频和文本严格对应。本文的segments.csv方案已经为你准备好了数据基础下一步就是选择一个顺手的前端工具。11.5 建议的使用节奏如果你每天精听一篇材料不建议每天把整套流程都跑一遍。更好的方案是每天跑preprocess.py和sentences.py生成当天文本每周日集中跑vocabulary.py把一周的生词统一导出到 Anki。这样既能避免被工具制作占用过多时间又能保证复习材料及时更新。当前这套脚本非常适合个人电脑上的小规模使用。如果你计划运营一个英语学习社群并要把工具分享给更多人使用建议加上 Web 界面并通过表单上传音频和字幕文件。不过这样一来就涉及服务器、用户数据上传和内容合规等更复杂的问题超出了本文范围。整套代码的核心价值在于让重复劳动自动化而不是让学习本身被自动化。慢速英语材料依然需要你认真听、反复读脚本只是帮你把素材整理成更适合高效学习的形态。你可以把示例代码复制下来用自己手头的合法听力材料试验一次然后根据习惯调整停用词表、输出编码或文件目录结构最终形成属于你自己的精听工作流。