用快捷指令下载 YouTube 视频的纯文字字幕

2022-08-10

A version of this article appears on Aug. 10, 2022 on SSPAI as a member-only post. Learn more or subscribe

The article is permitted to be self-archived in the version as originally submitted for publication on the author’s personal website under CC BY-NC 4.0 pursuant to § 5.2(b) of the SSPAI Fellowship Contributor Agreement.

背景

视频如今已经成为获取信息的重要渠道,YouTube 上大量科普和教学视频也是优质的学习资源。但在观看这些视频的同时,怎样将其中的信息记录下来是个问题。

实际上,YouTube 会自动为大多数视频生成字幕,并提供各种语言的翻译版本; 有些比较用心的播主还会手动编制字幕并上传。以这些字幕为基础,很容易就能整理出完整的笔记。

问题在于,YouTube 本身并没有提供下载字幕的方式。虽然使用开源视频下载工具 yt-dlp 可以解决,但其命令行选项比较冗长,难记也难用。此外,yt-dlp 下载到的是包含时间轴信息的字幕格式文件,也不利于浏览和后续处理。

好在,快捷指令来到 Mac 上后,也入乡随俗支持了执行终端命令(我去年曾撰文介绍过)。因此,可以用快捷指令给 yt-dlp 做一个图形界面的包装,更方便直观地完成字幕下载操作。

使用方法

本文用到的快捷指令依赖于 yt-dlp,因此需要先安装该工具。对于 macOS 用户,最简单的方法是使用 Homebrew:

brew install yt-dlp/taps/yt-dlp

注: 你可能还知道一个类似的工具 youtube-dl,它实际上是 yt-dlp 的前身,但命运多舛,多次成为一些版权团体的攻击对象—— 最有名的一次就是 2020 年受到美国唱片业协会 RIAA 的版权投诉,代码仓库一度下线。后来虽然恢复,但开发和更新也基本停滞了。yt-dlp 正是 youtube-dl 的分叉版本,除了保持快速更新,还重点解决了限速等问题;因此我们在这里优先选择它。)

安装完成后,运行如下命令:

which yt-dlp

如果返回了 yt-dlp 的安装路径,表示安装成功。复制这个路径供下一步使用。

接着,下载本文用到的快捷指令。

导入过程中,会提示输入 yt-dlp 的安装路径,在其中填入上一步输出的结果即可。

注: 之所以需要这个步骤,是因为快捷指令中的终端有别于通过「终端」等应用打开的交互式终端,不能识别 Homebrew 软件包的搜索路径,因此需要给出一个绝对路径。但不同平台会将 yt-dlp 安装在不同位置,无法事先预判,因此需要根据实际情况手动填写。)

这样就完成了准备工作。

现在,运行快捷指令,填入要下载字幕的视频地址,就会调用 yt-dlp 查询可用的字幕。如果视频同时包括自动字幕和人工字幕,则会弹出菜单要求选择要下载的类型。(如果有人工字幕可选,建议优先选择,因为比自动字幕更准确。)

最后选择要下载的语言(默认只保留中文和英文的选项),快捷指令就会下载好字幕,提取其中的文本,并保存为「下载」文件夹中的 subs.txt 文件。

原理介绍

如上所述,本文用到的快捷指令本质上就是对 yt-dlp 的包装。因此,要做出这样一个快捷指令,就要先了解 yt-dlp 在命令行下是怎样工作的。

通过运行 yt-dlp --help 查询帮助,可以知道:

要查询一个视频的可用字幕,命令为 yt-dlp --list-subs URL,其中 URL 为视频地址。该命令的输出结果类似于:

[...]
[info] Available automatic captions for xxxxxxxxxxx:
Language   Name                               Formats
af-en      Afrikaans from English             vtt, ttml, srv3, srv2, srv1, json3
[...]
zu         Zulu                               vtt, ttml, srv3, srv2, srv1, json3
[info] Available subtitles for xxxxxxxxxxx:
Language  Name    Formats
en        English vtt, ttml, srv3, srv2, srv1, json3
[...]

要下载一个视频的字幕,命令为:yt-dlp --write-subs --sub-langs LANGS --skip-download URL

其中,LANGS 为上一步输出表格的第一列中的语言代码。如果是自动字幕,则将 --write-subs 改为 --write-auto-subs。此外,可以使用 --output 调整下载的文件名。

因此,如果在命令行下使用 yt-dlp 下载字幕,主要步骤就是:

  1. 使用 --list-subs 选项查询可用字幕;
  2. 浏览输出结果,记下所需字幕的语言代码;
  3. 使用 --write-subs--write-auto-subs 选项下载字幕;
  4. 对字幕文件做后续处理(如制成纯文字版)。

我们的快捷指令正是按照这样的思路编写的。

获取可用字幕列表

开头几个步骤中,快捷指令询问要下载字幕的地址,将其传递给 yt-dlp 来查询字幕。

注意到在 yt-dlp 的输出中,自动字幕和人工字幕各有一个表格:第一行为表头,每行分为三列:语言代码、字幕描述和字幕格式列表(总会包含默认的 vtt 格式)。

因此,我们使用正则表达式:

(?m)(?<=Formats\n)(.*vtt.*$\n?)*

就可以匹配到除了表头之外的内容。这里,(?m) 的作用是启用跨行匹配,否则默认只能匹配到单行内部的字符。

根据匹配到的结果数量,就能判断字幕的提供情况,进而提供不同选项供用户选择:

  • 如果视频既有自动字幕也有人工字幕,上述正则表达式将匹配到两个结果,此时提供一个菜单供选择要下载的类型;
  • 如果视频只有自动字幕,上述正则表达式只能匹配到一个结果,也不需要手动选择;
  • 如果视频没有字幕,上述正则表达式不能匹配到任何结果,此时应终止继续执行。

选择字幕语言

在选择了要下载的字幕类型后,还需要选择语言。为此,进一步使用正则表达式匹配

(?m)^([A-z-]+)\s+(.+?)\s+vtt.*

并替换为

$1 ($2)

这里,$1 匹配的是每行开头的语言代码,$2 匹配的是每行第二列的字幕描述,其余内容,包括多余的空格、字幕格式等,则丢弃不用。

又考虑到 YouTube 的自动字幕中很多对中文用户意义不大(例如因错误识别原视频语言而产生的选项、以及翻译为小语种的选项),再进一步通过

(?m)^(zh|en).*` 

过滤出最常用的中文和英文字幕。

经过这样的处理,原本较为繁琐的输出就会变成如下这样的简洁格式:

[...]
zh-Hans-en (Chinese (Simplified) from English)
zh-Hant-en (Chinese (Traditional) from English)
en-en (English from English)
[...]

将其依次传输给「拆分文本」和「从菜单中选择」动作,就能得到一个语言选单。然后,从用户选择结果中用 ^[A-z-]+ 来提取最终选择的语言代码。

至此,我们就获得了 yt-dlp 字幕下载所需的全部信息,将其组合成完整命令,通过「执行终端脚本」步骤运行即可。(图中的条件判断步骤是为了根据之前步骤选择的字幕类型,确定要使用哪种下载选项。)

将时间轴字幕转为纯文本

前面提到,yt-dlp 下载的字幕文件是带时间轴信息的专用格式,并不利于阅读和后续处理。因此,我们将字幕文件暂存到临时文件夹 /tmp 等待进一步清理。

怎样清理呢?我们先来观察一下 WebVTT 字幕的一般格式:

WEBVTT
Kind: captions
Language: zh-Hans

00:00:00.333 --> 00:00:02.202 position:63% line:0%
第一句话

00:00:02.202 --> 00:00:06.940 position:63% line:0%
每行<00:00:04.892><c>内部</c>也可以标注时间

[...]

可以看到,其语法并不复杂。大体而言:

  • 第一行固定为 WebVTT 字样;
  • 之后为一些可选的元信息行,YouTube 总是包含 Kind 和 Language 两行元信息,其后用一个空行和主体隔开;
  • 主体部分均为一行时间轴(cue)和一行文本内容(payload)的组合(还可以有一行编号,但 YouTube 字幕没有),每行字幕之间用一个空行隔开;
  • 每行文本的内部也可以标注时间。

(更详细的语法标准可以参阅 WebVTT 格式的标准。)

要将这样的格式还原为文本,方法当然很多,包括使用正则表达式。但本文用的正则表达式已经够多了,这里就偷个懒,用用 Unix 风格的工具:

这里,我们用管道串起了多个「基本款」命令,它们的作用分别如下:

需求 命令 含义
去掉主体部分之前的元信息行 tail +4 只输出从第 4 行以后的内容
去掉各行时间轴 grep -v " --> " 排除(-v)所有包含 --> (时间轴里连接起止时间的箭头)的行
去掉行内的时间轴信息 `sed -E 's!<([0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3} c
去掉所有空行 grep . 输出有任何内容(.)的行
去掉连续的重复行 uniq 这个命令的老本行

最后删除临时文件,打开「下载」文件夹中的成品即可。

扩展延伸

使用 iOS 设备遥控

本文快捷指令依赖于电脑端,但并不意味着就不能在 iOS 上运行。例如,如果你有一台经常开机的局域网内电脑或远程服务器(可以是任何 yt-dlp 支持的平台,包括 Linux 和 Windows),那么就可以通过 iOS 遥控上述机器下载、处理字幕,然后输出结果到 iOS 端。

为此,只需做两个小调整即可:

  • 将原版快捷指令中的「执行终端命令」动作换为「通过 SSH 运行脚本」动作;
  • 将最后输出结果的方式从写入文件改为直接传递给快捷指令展示。

这里,我们也提供了一个修改好的模板,下载后在 SSH 动作中填写自己服务器的登录信息即可使用。

纯终端版本

不难看出,尽管我们用快捷指令实现了自动构造 yt-dlp 命令,但实现步骤是比较麻烦的,快捷指令有限的处理能力在这里起到了拖后腿的作用。

如果你更习惯跟终端打交道,其实也可以将整个流程做成一个终端脚本。这里,我也提供自己写的一个比较粗略的版本(下载),运行时将视频地址作为参数传给这个脚本即可,同样可以实现快捷指令版本中的各项交互功能,包括选择字幕类型和语言、导出纯文字版等。限于篇幅,这里就不多着墨了,可以使用 --help 选项具体了解,也欢迎更有经验的朋友指正。