Featured image of post 避免使用 Python ass 库处理 ASS/SSA 格式的字幕文件

避免使用 Python ass 库处理 ASS/SSA 格式的字幕文件

Python ass 库对 ASS/SSA 格式的解析比较严格,而实际的字幕播放器则非常宽容

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 库报错的场景:

  1. 字段数量不匹配Dialogue 行的字段数与 Format 行定义的不一致(多了或少了逗号)。播放器会截断或补空,但 parser 直接报错。
  2. 编码问题 — 文件实际是 GBK/GB2312/Shift-JIS 但没有 BOM 或声明,库默认按 UTF-8 读取就会炸。播放器通常会自动探测编码。
  3. 非标准的 section 或字段 — 比如 [Aegisub Project Garbage]、自定义的 [Fonts][Graphics] 等扩展段,以及第一行漏写了 [Script Info] ,或者某些字段名拼写不标准,库不认识就报错。(大部分情况应该属于这种)
  4. Style 定义中的非法值 — 比如颜色值格式不规范(&H00FFFFFF 写成 &HFFFFFF)、布尔值用 True/False 而不是 -1/0、字体大小为空等。
  5. 行尾/换行符问题 — 混合的 \r\n\n,或者文件末尾缺少换行。
  6. 注释和空行 — 文件中夹杂着 ; 开头的注释或意外的空行,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 比正则解析更快。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy