前言

  这个月的文章更新开始慢了起来,因为做这个工具花费了我两个周末ε=(´ο`*)))~
  做字幕工其实是很早就有的想法了,还记的2年前了刚准备去深圳的时候。
  我还专门用 Python 做了 Lynda 和 Pluralsight 两个教学网站的字幕抓取工具。 链接
  现在回过头来看之前的工具真的很初学者,而且已经很久没用,估计 API 更新就用不了了,那个时候还不会用 github ,所以源码只在我的移动硬盘里面有备份。

  至于做 B 站字幕上传工具的想法,其实好几个月前就有了想法(可能是看到别的 Up 主涨粉比较快吧(:з」∠)
  不过我自己真的对视频字幕有洁癖,还是比较喜欢看原汁原味的视频,如果字幕压缩到视频里面无法清除,还真有点受不了。
  特别是以前看教程试过字幕挡住了视频操作,体验很糟糕^(* ̄(oo) ̄)^
  既然 B 站支持了 外挂 字幕功能,我的想法总算是可以实现了 ~

  经过我不懈的努力,最终效果可以参考 B 站的这个教程

演示链接

  选择这个教程是因为比较短,方便我测试。
  实现的原理其实不难,就是直接将 youtube 的 vtt 字幕文件转换为了 B站 的 bcc 后缀的 json 字幕,并且通过了不少时间的调试,实现 逐词 出现的效果。

Tkinter 界面开发

  这次图形界面开发再次使用了 Python 内置的 Tkinter 来制作。
  毕竟工作上一直使用 PySide2 ,偶尔换换口味体验不同的框架还是不错的,而且 Tkinter 比 Qt 小得多,打包的大小也足够小。

alt

  其实看界面也没有多少东西,无非就是几个组件重复套用而已。
  不过 Tkinter 有个比较有意思的点,它的组件可以接受内置的 Variable 类,这个类的执行 set 改变数值的时候可以同步到关联的组件上。
  用这个类就实现了面板的数据绑定了。
  有了数据绑定就可以比较轻松地用统一的规则将数据存储到文件里面了。

  所以我写了一个 Mixin 类可以将组件的数据 存储本地 也可以从本地读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import json
class ConfigDumperMixin(object):
"""ConfigDumperMixin
自动记录 Tkinter Variable 配置
"""

@staticmethod
def auto_load(func):
def wrapper(self, *args, **kwargs):
res = func(self, *args, **kwargs)
self.load_config()
return res
return wrapper

@property
def config_path(self):
return os.path.join(__file__, "..", "config.json")

def get_tkinter_varaible(self):
return [var for var in dir(self) if isinstance(getattr(self, var), tk.Variable)]

def load_config(self, *args, **kwargs):
path = kwargs.get("path", "")
path = path if path else self.config_path
if not os.path.exists(path):
return

with open(path, "r") as f:
config = json.load(f, encoding="utf-8")
[getattr(self, var).set(val) for var, val in config.items()]

def dump_config(self, *args, **kwargs):
path = kwargs.get("path", "")
path = path if path else self.config_path
data = {var: getattr(self, var).get() for var in self.get_tkinter_varaible()}
with open(path, "w") as f:
json.dump(data, f, indent=4, ensure_ascii=False)

  这个类会自动获取类下面的 tk.Variable 进行输入和输出。

  另外组件写法上,这次接入了 contextlib 来用 with 关键字。
  通过缩进让组件写起来更有层次感,不过回过头来,其实写一个组件类封装会更加简洁。

B站 API 调试

  上传字幕没有人测试过它的 API ,不过可以直接从浏览器 F12 的后台里面截取到接口请求。

alt

  在这个面板按下提交按钮,即可获取到请求链接。

alt

  点击之后网页会刷新,会导致之前的链接记录全部刷掉,所以要勾选这里来确保请求信息存留。
  利用右键输出 curl 命令可以在 postman 里面重构请求环境。 参考链接
  这样就不用人工添加 cookie 等等的登陆信息。

请求方式:POST

参数名 类型 内容 必要性 备注
aid num 视频av号 必要
type num 操作方式 必要 一般为 1
csrf str CSRF Token(位于cookie) 必要
data str bcc 字幕数据 必要
bvid num 视频bv号 必要
sign bool 签名 必要 一般为 false
submit bool 提交 必要 一般为 true
oid num 分P编号 必要

  经过我的整理接口信息有如上参数为必填的。
  data 数据为 B 站的字幕数据,本质上就是一个用 json 记录的 srt 文件,额外记录了位置和颜色信息。
  而且目前编辑器还没有修改颜色的功能,可以实现一些 字幕 上的黑科技,不过我还是觉得黑白搭配挺简洁的。

  另外比较坑的地方是 字幕有大小上限的 , 如果视频很长的估计会没法上传完整的字幕。
  我想要 逐词 切分也会占用很多大小空间,随意我设置了字幕如果上传失败就用传统的切分方式上传。

bilili B站 视频下载

  为什么要下载 B站视频呢?
  因为 B站 自动进行画质压缩,我可以下载低画质的视频来方便生成字幕。
  而且有很多老视频的资源我都不知道还有没有(:з」∠)

  我过去下载 B站视频用的是 jijidown 的。
  但是这些图形界面的软件没法用 Python 进行控制。
  于是在 Github 上搜索相关的 bilibili视频下载的仓库,找到了 bilili 这个工具 链接
  简答审阅过 bilili 的源码,我觉得作者的代码水平挺高的。
  用了不少的 Python 骚操作,让代码清晰可读。
  使用起来也比较简单,可以参考 -h 的帮助说明。
  用 pip 安装之后,可以直接在命令行调用 bilili 来进行下载,多线程下载速度很快。

autosub 自动翻译

  github 上有个很多星 autosub 仓库,但是那个已经不再维护了。 链接
  几经周折,最后找到了一个国人维护的 autosub 仓库,而且还加上了一些第三方付费 API 的支持,非常不错 链接
  autosub 的使用可以方法参考作者的文章 链接

  为了方便使用,我直接将 release 版本解压到自己的仓库里面来使用了。
  基于 命令行 来调用,默认的方式使用 Google 的 API 进行白嫖翻译,但是我好容易接入之后,发现这个翻译效果和 youtube 的机翻比起来还是差得远。
  也有可能是 autosub 的音频切分导致识别的时候缺少上下文导致的。
  终止最后的翻译效果让我很失望,所以我后续将开发精力用在了 youtube 上传和下载上。

youtube 视频上传

  youtube 视频上传搜 github 比较高星的是前两个 https://github.com/porjo/youtubeuploaderhttps://github.com/tokland/youtube-upload
  我后面使用了 Go 语言开发的版本,因为 Go 提供了 Release 版本,方便我去调用。
  不过配置上传工具真的挺麻烦的,我在 github 仓库上留有记录 链接
  需要去登陆 Google 控制台创建一个 验证应用,然后开启 Youtube API。
  下载验证应用的 json 文件并放到 运行 exe 的目录下 (或者在 exe 里面用指定 serects)
  然后输入一个视频路径上传,会跳转到网页,从网页里面通过验证,然后获取到一遍神秘代码。
  通过 exe 的 -headlessAuth 指令输入上面的神秘代码会生成一个 token 后缀的文件。
  完成这些操作就可以愉快地上传视频了。

  然而经过如此曲折的折腾之后,上传了 6 个视频, API 就会出现无法上传的问题。
  查了之后才知道 Google Youtube API 一天提供了 10000 Quotas ,然而上传一个视频需要消耗 1600 Quotas
  上传 6 个视频就用完了一天的免费额度了(:з」∠)
  所以经过网上查阅,最终解决方式用 selenium 操控浏览器模拟认为操作。 链接

  通过上面的链接可以定位到写好的 Github 仓库
  链接里面用的是 Firefox 让我有点不爽,但是查了代码感觉不是很多,所以打算自己写一个 Chrome 版本的。
  结果发现 Chrome 经过 selenium 启动之后无法完成 Google 登陆验证,会提示当前浏览器不安全。
  在 StackOverflow 找到解决方案 链接
  通过 Stack Overflow 的 Google 登陆来要开 youtube 的验证,我验证过是可行的,但是最后还是用回了 Firefox 图省事。

  后来我发现 B 站的字幕有大小上限,但是用网页端可以上传我测试用的字幕。
  检查后台发送的接口的数据没有 form data 的数据,Header 里多出了 content-length 字段。
  没有任何的解决想法,只能想到用 selenium 自动化上传来解决问题。

  使用 selenium 自动化需要先用一定的规则匹配到页面上的元素,比较常用的方法是 xpath
  然而 selenium 启动是在是太慢了,我做元素抓取的工作实在是等到黄花菜都凉了还没好。
  所以我去搜了一下 Chrome 的扩展,还发现了非常方便的浏览器扩展来解决问题。 链接
  F12 后台的 element 标签里会多出一个 chropath 标签,选择页面的元素会自动生成相应 xpath ,非常省心。
  我在 B 站的字幕上传页面里面一直无法用 xpath 定位到相关的元素,最后用了 chropath 也无法定位。
  后来再查,发现这个页面居然是 iframe 嵌套进去的。
  B站的字幕编辑器链接要用这个 "https://account.bilibili.com/subtitle/edit/#/editor?bvid={bvid}&cid={cid}"
  这样才可以使用 xpath 选择器来触发相关元素的点击效果。

  好不容易完成了 selenium 控制的自动上传,结果才发现有些更大的字幕用网页端也无法上传。
  网页端的上传速度不如 API 接口快,还是用 API 接口更方便,所以我又改回去了。

youtube 字幕下载

  这里我又踩了很多坑,我记得以前 PLB下载工具整好的时间,我挂机下载了很多 Pluralsight 的教程字幕。
  不过我用的不是 自己开发的垃圾工具,而是用了 youtube-DLG 来实现字幕下载,下载的数量之多以至于我账号都被封了(:з」∠)
  因为短时间内看了访问了大量的教程页面。

  youtube-DLGyoutube-dl 下载神器的图形化界面工具
  震惊! 写文章之际访问 youtube-dl 的主仓库居然因为版权问题被封禁了(:з」∠),前几天还 clone 了源码,可惜 issue 看不到了。
  难怪我发现 youtube-dl 问题越来越多却维护得很不积极。

  我前几天找到了国人维护的魔改版本 链接
  也拿来测试了,然并卵(:з」∠),折腾了我很多时间。

  我来给大家从头捋一捋用 youtube-dl 下载字幕遇到的坑。
  youtube-dl 下载公开视频的字幕是没有问题的,但是一旦涉及到私享字幕就不行了。
  根据 youtube-dl 的说明获取 cookies 也没有用。
  然后查了 Stack Overflow ,通过 –add-header 添加 cookie 也没有用

  后来就没法子了, Github 上关于 youtube 字幕下载比较高星的是这个 链接 浏览器扩展
  我装了这个扩展的确可以下载到 youtube 的字幕。
  于是我又查了这个扩展的源码,看看是怎么获取的,里面的代码不多,比起 youtube-dl 恐怖的代码量好多了。
  作者的 readme 有获取说明 链接
  直接获取通过 js 获取页面的 ytplayer 公共变量居然可以获取到 字幕 的信息。
  信息里面直接有了获取的字幕的直链。
  但是这个方法我没法用 Python 来搞,最后还是的查了 youtube-dl 的源码来实现。

  原来是先访问视频页面,可以从页面里找到下载字幕相关的 https://www.youtube.com/api/timedtext 开头的 API
  然后再通过这个链接加入 fmt=vtt 下载 vtt 字幕 (默认是 xml 格式的字幕信息)
  另外加上 tlang=zh-Hans 可以获取到中文翻译的字幕。
  最后我用 postman 生成代码加上我的 youtube cookie 就可以将我私享的视频字幕拔下来,私享也不用担心视频的版权问题。

  在 postman 上我已经测试能够获取到字幕链接了。
  然而在 Python 里面获取的页面是一堆乱码。
  这个问题也折腾了我好久,没有搞明白。
  我查外网 Stack Overflow 没法将这个问题描述清楚,查不到有用的解决方案。
  后来我直接查国内的 Request 乱码 ,虽然中间也有些不靠谱的,不过最后还真找到解决方案 链接
  原来是压缩导致的乱码。 链接
  根据链接的操作直接注释掉 'accept-Encoding': "gzip, deflate, br" 可以解决问题,主要是 br 的问题。
  后面就比较顺利,后面直接粗暴地通过正则抓 timedtext API链接。

  有了链接加上上面提到的两个参数获取出字幕,再将字幕转换即可。

字幕 转 bcc

  字幕转换主要依赖了 pysrtpyvtt
  这个直接将每一行字幕解析成了一个对象,获取和修改都很方便。
  获取了信息之后需要将时间转换成为 秒 单位即可。
  bcc 字幕没啥特别的,其实就是将 srt 转为 json 格式,并且加多了一些信息进行描述而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"font_size": 0.4,
"font_color": "#FFFFFF",
"background_alpha": 0.5,
"background_color": "#9C27B0",
"Stroke": "none",
"body": [
{
"from": 6.399,
"to": 7.829,
"location": 2,
"content": "hello everyone"
},
{
"from": 7.829,
"to": 7.839,
"location": 2,
"content": "hello everyone"
}
]
}

  就是将 vtt 字幕转成逐字出现的 字幕 效果花了我不少的时间进行调试。
  因为需要我用正则去匹配 vtt 内部的 tag 来生成新的字幕。
  最后有些大视频还因为字幕文件太大上传不了,着实有些遗憾。

总结

  以上就是我花了两周的空余时间做好的字幕上传工具。
  操作逻辑就是先将 B站的视频下载上传到 youtube 上,并且记录下 youtubeid 和 B站视频id 的对应信息。
  等到字幕生成完成之后,在遍历一遍记录好的 json 文件,然后将可以上传的字幕转换上传到 B 站。
  现在虽然已经换成了 selenium 自动化控制浏览器上传视频了,然而单日最多只能上传 50 个视频,这个过程只能慢慢来了。

  另外,这个工具结束之后,还是很希望自己可以静下心来好好地学习 Houdini 了。
  视频教程系列我都准备好了,只等自己开干起来。