Subtitle Renamer 是我日常使用的字幕重命名工具,非常好的解决了 Jellyfin / mpv 等视频播放软件仅识别与视频文件同名的字幕文件的痛点。
在使用中发现一个问题,经常会出现无法识别 .ass 和 .ssa 格式字幕的问题,而其他格式的字幕(如 .srt )则从来没有遇到过这种情况。而这些无法识别的字幕,在我所用的所有视频播放软件中都能正常渲染。同时,去仓库的 Issue 中查看,发现已经有人遇到过这个问题。
拉取源码后,找到了报错的代码:
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
|
def assSubtitle(file_name):
# 检测文本编码
encoding = subEncoding(file_name)
with open(file_name, "r", encoding=encoding) as file:
result = ass.parse(file).events
# 提取正文内容
subtitle = []
for item in result:
# 转义样式中的斜杠
new_item = item.text.replace("\\", "\\\\")
# 匹配 {} 之外内容
ass_pattern = r'\{[^{}]*\}'
matches = re.sub(ass_pattern, '', new_item)
# 排除单字的特效字幕
if len(matches) == 1:
continue
# 排除空内容
if not matches:
continue
subtitle.append(matches)
return subtitle
|
问题在于 result = ass.parse(file).events 这一行。 ASS/SSA 格式没有一个严格的官方标准,Python 的 ass 库对 .ass 和 .ssa 文件的格式解析比较严格,其本质是一个 parser ,遇到不符合预期的内容就会抛异常,而实际播放器则非常宽容,普遍采取"尽力解析、忽略错误"的策略。
让 AI 总结了以下可能会造成 ass 库报错的场景:
- 字段数量不匹配 —
Dialogue 行的字段数与 Format 行定义的不一致(多了或少了逗号)。播放器会截断或补空,但 parser 直接报错。
- 编码问题 — 文件实际是 GBK/GB2312/Shift-JIS 但没有 BOM 或声明,库默认按 UTF-8 读取就会炸。播放器通常会自动探测编码。
- 非标准的 section 或字段 — 比如
[Aegisub Project Garbage]、自定义的 [Fonts]、[Graphics] 等扩展段,以及第一行漏写了 [Script Info] ,或者某些字段名拼写不标准,库不认识就报错。(大部分情况应该属于这种)
- Style 定义中的非法值 — 比如颜色值格式不规范(
&H00FFFFFF 写成 &HFFFFFF)、布尔值用 True/False 而不是 -1/0、字体大小为空等。
- 行尾/换行符问题 — 混合的
\r\n 和 \n,或者文件末尾缺少换行。
- 注释和空行 — 文件中夹杂着
; 开头的注释或意外的空行,parser 不处理就崩了。
其中第三种情况在各个字幕组制作的字幕文件中非常普遍,毕竟字幕组数量庞大,字幕制作者也可能没有这方面的意识,主打一个“能用就行”。为了兼容这么一部分数量庞大的非标字幕文件,尝试在 ass 报错后尝试使用正则来解析字幕文件:
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
def assSubtitle(file_name):
# 检测文本编码
encoding = subEncoding(file_name)
try:
# 原 ass.parse 逻辑
except Exception as e:
# 如果ass.parse失败,使用正则表达式解析
print(f"ass.parse解析失败,使用正则表达式解析: {e}")
subtitle = []
try:
with open(file_name, "r", encoding=encoding) as file:
in_events_section = False
for line in file:
line = line.strip()
# 检查是否进入了[Events]部分
if line == "[Events]":
in_events_section = True
continue
# 检查是否进入了其他部分
if in_events_section and line.startswith("["):
in_events_section = False
# 只处理Events部分的Dialogue行
if in_events_section and line.startswith("Dialogue:"):
# 分割并获取最后一个部分(对话内容)
parts = line.split(',', 9) # 最多分割9次,确保最后一个元素包含全部对话内容
if len(parts) >= 10:
text = parts[9]
# 转义样式中的斜杠
text = text.replace("\\", "\\\\")
# 移除花括号内的ASS样式代码
clean_text = re.sub(r'\{[^{}]*\}', '', text)
# 移除多余空白
clean_text = clean_text.strip()
# 排除单字和空内容
if len(clean_text) > 1 and clean_text:
subtitle.append(clean_text)
return subtitle
except Exception as e:
print(f"正则解析失败: {e}")
raise e
|
搞定。这里保持原来的 ass.parse 逻辑是因为经过实际测试,处理同样的标准字幕文件,使用 ass.parse 比正则解析更快。