这事儿得从那个下雨的周末说起,我窝在沙发上刷手机,突然想找点老片子看,结果打开Club365,发现中文字幕少得可怜,不是没有,但很多不匹配,或者干脆放了个机翻的,我寻思着:能不能自己写个工具,把那些字幕拽下来重新整理?
说干就干,我选了Golang,为啥?因为它快,编译出来一个小小的二进制文件,扔哪都能跑,而且我喜欢它那种“吊儿郎当”但干活利索的劲儿。
第一步:摸清Club365的字幕藏在哪儿
打开Chrome的开发者工具,Network一栏,刷新页面,找啊找,找到了一个叫 subtitles.json 的文件,里头长这样:
{
"video_id": "club365_zh_12345",
"subtitles": [
{"start": 0.0, "end": 3.5, "text": "嘿,好久不见。"},
{"start": 3.5, "end": 7.2, "text": "你最近在忙什么呢?"}
]
}
关键点:这个JSON是一段一段的,包含每个字幕的起始时间、结束时间和文本,更骚的是,有些视频有多个语言版本的字幕——URL参数里带 ?lang=zh 就是中文,?lang=en 是英文。
我写了段Golang代码,模拟浏览器发请求:

resp, err := http.Get("https://api.club365.com/videos/12345/subtitles?lang=zh")
if err != nil {
log.Fatal("拉取字幕失败:", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
第一次跑,报错403,哦,忘了加User-Agent,加上之后,妥了。
第二步:把JSON解析成干净的字幕文件
拿到这个JSON之后,我想着转成标准的 SRT格式,这样任何播放器都能用,SRT格式长这样:
1
00:00:00,000 --> 00:00:03,500
嘿,好久不见。
2
00:00:03,500 --> 00:00:07,200
你最近在忙什么呢?
我用Golang的结构体映射JSON里的字段:
type Subtitle struct {
Start float64 `json:"start"`
End float64 `json:"end"`
Text string `json:"text"`
}
type SubtitleResponse struct {
Subtitles []Subtitle `json:"subtitles"`
}
然后写了个转换函数,把秒数转成 HH:MM:SS,mmm 的格式,这里有个小坑——Golang的 time.Duration 好用是好用,但精度控制得手动搞定。
func secondsToSRTTime(seconds float64) string {
d := time.Duration(seconds * float64(time.Second))
return fmt.Sprintf("%02d:%02d:%02d,%03d",
int(d.Hours()),
int(d.Minutes())%60,
int(d.Seconds())%60,
int(d.Milliseconds())%1000,
)
}
最后把字幕按序号、时间线、文本一行行写进 .srt 文件里。跑一下,完美。
第三步:批量处理——俱乐部里上百个视频的字幕
但问题来了:Club365可不是只有一两个视频,它有几百上千个啊,一个一个手动下载,手会断的。
于是我写了个循环,遍历视频ID列表,视频ID从哪里来?我发现它们的主页面里有个隐藏的JSON接口,返回所有视频的元信息,包括ID、标题、语言资源列表。
type Video struct {
ID string `json:"id"` string `json:"title"`
HasSubsZH bool `json:"has_subs_zh"`
}
只下载那些 HasSubsZH 为 true 的视频,然后并行处理——Golang的goroutine就是干这个的。
var wg sync.WaitGroup
for _, v := range videos {
if v.HasSubsZH {
wg.Add(1)
go func(videoID string) {
defer wg.Done()
downloadAndSaveSubtitle(videoID)
}(v.ID)
}
}
wg.Wait()
并行一下,速度唰唰的。 原本逐个下载要半小时,现在3分钟搞定,但有个坑——并发太快,被服务器限流了,我加了个 time.Sleep(200 * time.Millisecond) 在每次请求之间,平衡了一下。
第四步:最大的问题——字幕是机翻的
下载下来的字幕,有些根本没法看,Hello, how are you”被翻成“你好怎么样你”,一看就是谷歌翻译的结果,而且还没调教好。
我寻思着能不能用更好的翻译API重新翻译?于是我接入了百度翻译API(做中文翻译公认不错的),因为我已经从Club365拿到的原文(英文)和机翻中文,现在只需要把原文再翻译一次,覆盖掉原来的中文。
type TranslationRequest struct {
Q string `json:"q"`
From string `json:"from"`
To string `json:"to"`
AppID string `json:"appid"`
Salt string `json:"salt"`
Sign string `json:"sign"`
}
注意:在请求API时必须拼接签名,格式是 appid+q+salt+密钥 的MD5,这个搞错了就403,我调试了半小时才发现少了个参数。
用新翻译的文本替换原来的中文字幕,再写回文件。这一下子质量好多了,至少是通顺的自然语言了。
第五步:组织整理——让字幕能直接用在播放器里
下载好的SRT文件,我按视频标题归类:
| 视频ID | 字幕文件路径 | |
|---|---|---|
| club365_123 | 星际迷航·原初 | ./subtitles/星际迷航·原初.srt |
| club365_456 | 黑客帝国 | ./subtitles/黑客帝国.srt |
| club365_789 | 生活大爆炸 S01E01 | ./subtitles/生活大爆炸_S01E01.srt |
我做了一个小脚本,扫描本地视频文件,自动匹配同名的SRT字幕,只要视频文件名和字幕文件名一致(不含扩展名),PotPlayer、MPC-BE等播放器就能自动加载。
# 示例:重命名字幕为与视频匹配 mv 黑客帝国.srt "黑客帝国.BluRay.1080p.x264.mkv.srt"
这样视频目录里的字幕就直接可用,中文用户打开直接看,不用再手动拖拽加载。
意外发现——字幕的时间轴还能自动对齐
有些字幕下下来之后,发现时间轴偏移了,比如第一句字幕出来慢了一秒,我又写了个小功能,自动批量调整时间轴偏移量。
func shiftSubtitle(subs []Subtitle, offset time.Duration) []Subtitle {
for i := range subs {
subs[i].Start += offset.Seconds()
subs[i].End += offset.Seconds()
}
return subs
}
可以根据第一个字幕的起始时间自动计算偏移,或者手动输入偏移量(毫秒级)。这个功能虽然小,但很常用——尤其是那些来自不同源的字幕。
关于Club365中文字幕的现状
说实话,Club365上的中文字幕质量参差不齐,有的显然是人工翻译的,用词地道,断句合理;有的就是机器瞎翻的,卡得让人想摔键盘,我的这些工具,能解决一部分问题——但不是全部。
有些字幕连对应的英文原文都没有,只有一个未知语言的声道,那就彻底没法子。遇到这种情况,我的代码只能跳过,然后默默把视频URL记录下来,等以后有资源了再补。
最后想说的
这个工具我从头到尾用Golang写,大概花了两个晚上,现在它躺在我电脑里,每次想看电影就直接跑一下,拉最新的中文字幕,挺好用的。
其实我写这篇文章不是为了炫耀什么,而是想告诉你——很多所谓的“麻烦”,用一点点代码和耐心,就能变得很顺手。 你不需要是全栈工程师,也不需要懂复杂的算法,只要你愿意折腾,边查文档边敲代码,慢慢就能搞出来。
Club365里还有很多好片子等着看,我也才整理完一半,剩下的一半,周末再说吧——不完美的工具也比没有强。
(完)
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/jiankang/384.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《从零开始,我用Golang写了一个在线视频Club365中文字幕爬虫,结果发现…》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:这事儿得从那个下雨的周末说起,我窝在沙发上刷手机,突然想找点老片子看,结果打开Club365,发现中文字幕少得可怜,不是没有,但很多不匹...