小酷|AI辅助编写与审阅|2026-10-26公开整理;代码与测试版本日期为2026-10-25。本篇公开一个本地字幕结构检查器及使用方法,供有合法字幕处理需求的人自行试用。源码使用MIT许可,文章不提供视频、影视字幕或客户材料。
先分清:结构问题与字幕内容问题
字幕出现时间倒置、漏空行或序号不连续时,可以先用程序定位到具体字幕块和文件行。程序没有听声音、看画面或判断翻译,也不会自动改字幕。即使报告没有error,仍可能有warning,不能由此判断字幕准确、阅读舒适或符合某个平台要求。
能检查什么
- 严格时间码格式、结束早于开始、空正文、从1开始的连续序号,以及疑似漏掉字幕块分隔空行。
- 零时长、开始时间乱序及重叠列为警告。多人对白有时会故意重叠,应结合自己的素材决定。
- 这些是本工具选定的结构规则,不能当作所有SRT方言或播放器的共同硬性要求。
在本机运行
需要已经安装的Python 3.10或以上;本地历史验证环境为Python 3.10。程序仅使用标准库。把文末源码复制为UTF-8文本文件srt_check.py;另准备自己有权处理的UTF-8或UTF-8 BOM字幕副本input.srt。报告父文件夹须已存在,报告文件名必须是新的。
python -X utf8 srt_check.py "input.srt" "report-new.json"
脚本不联网、不上传字幕、不修改输入,不覆盖已有报告;输入和报告路径相同会拒绝。报告包含输入文件名与SHA-256,分享报告前请使用不含姓名或客户信息的文件名,并检查报告里的片段是否可公开。
退出码0:报告生成且未发现本工具定义的error,仍须查看warning。退出码1:报告生成并包含error。退出码2:路径、编码或其他已捕获的处理失败;不要把目录里较早的报告误当成本次成功输出。
怎样看重叠警告
以下是自造示例,没有来自真实客户的内容。第1块持续到10秒,第2块和第3块都落在这段时间内,因此都需要复核。是否修改由你的具体字幕设计决定。保存为demo-overlap.srt后,可以用上面的命令替换输入文件名运行。
1
00:00:00,000 --> 00:00:10,000
第一句持续十秒。
2
00:00:02,000 --> 00:00:03,000
第二句有意重叠。
3
00:00:04,000 --> 00:00:05,000
第三句仍与第一句重叠。
findings内的severity表示错误或警告,code是规则编号,block是按空行划分的块序号,line是原文件中的物理行号。W_OVERLAP提示时间交叠,不表示程序已替你确认它不该存在。
验证和使用范围
2026-10-25的当前数字解析修复版本有15项本地回归通过证据,覆盖基本格式、文件保护、超长数字比较等;这些是自造样本的技术检查,不是用户验收。旧版本曾检查2,000,000字节和1,000块的容量样本,那次耗时不能当作当前源码的新性能测量。工具本身不强制容量上限且一次读取整个文件,大文件的时间和内存需要另行评估。
本工具不检查拼写、翻译、阅读速度、音画同步或平台准入,不保证发现所有SRT变体的问题。工具和文章免费提供。本页不接单、不收集字幕文件、不提供自动交付;页面底部的可选赞助与下载和使用无关。公开技术示例不代表已有付费客户。
当前源码(srt_check.py)
复制时保留缩进;代码块中的引号应为源码原样。源文件本次未改动,仅以文本形式发布。
"""XM-20261025-05:离线只读 SRT 结构检查。日期:2026-10-25,负责人:小执。"""
import argparse
import hashlib
import json
from pathlib import Path
import re
import sys
STAMP = r"([0-9]{2,}):([0-5][0-9]):([0-5][0-9]),([0-9]{3})"
TIMING = re.compile(rf"^{STAMP} --> {STAMP}$")
def timestamp_key(parts):
# 时间仅用于排序;归一化小时文本避免超长数字触发整数转换限制。
hours, minutes, seconds, millis = parts
hours = hours.lstrip("0") or "0"
return len(hours), hours, minutes, seconds, millis
def check_bytes(raw, source_name):
"""返回结构报告,不改动输入,不推断字幕语义或声音同步。"""
try:
text = raw.decode("utf-8-sig")
except UnicodeDecodeError as exc:
raise ValueError("输入不是有效 UTF-8/UTF-8 BOM;请先另存授权副本,不猜测编码。") from exc
if "\x00" in text:
raise ValueError("输入含 NUL 字符,可能是 UTF-16 等不支持编码,拒绝处理。")
blocks, current = [], []
for line_no, line in enumerate(text.splitlines(), 1):
if line.strip():
current.append((line_no, line))
elif current:
blocks.append(current)
current = []
if current:
blocks.append(current)
findings, cues = [], []
def add(severity, code, block, line, message):
findings.append(dict(severity=severity, code=code, block=block, line=line, message=message))
if not blocks:
add("error", "E_EMPTY", 0, 1, "文件没有字幕块。")
for number, block in enumerate(blocks, 1):
line_no, index = block[0]
if not re.fullmatch(r"[0-9]+", index):
add("error", "E_INDEX", number, line_no, "字幕块首行应为 ASCII 数字序号。")
elif (index.lstrip("0") or "0") != str(number):
add("error", "E_INDEX_SEQUENCE", number, line_no, f"按本检查规则序号应为 {number},实际为 {index}。")
if len(block) < 2:
add("error", "E_TIMING", number, line_no, "缺少时间码行。")
else:
match = TIMING.fullmatch(block[1][1])
if not match:
add("error", "E_TIMING", number, block[1][0], "时间码须为 HH:MM:SS,mmm --> HH:MM:SS,mmm,分秒为 00–59。")
else:
start, end = timestamp_key(match.groups()[:4]), timestamp_key(match.groups()[4:])
if end < start:
add("error", "E_TIME_ORDER", number, block[1][0], "结束时间早于开始时间。")
elif end == start:
add("warning", "W_ZERO_DURATION", number, block[1][0], "字幕时长为零,需人工确认是否应显示。")
else:
cues.append((start, end, number, block[1][0]))
if len(block) < 3:
add("error", "E_EMPTY_TEXT", number, block[-1][0], "字幕正文为空。")
for offset in range(2, len(block) - 1):
if re.fullmatch(r"[0-9]+", block[offset][1]) and TIMING.fullmatch(block[offset + 1][1]):
add("error", "E_SEPARATOR", number, block[offset][0], "正文中出现新的序号与时间码,疑似字幕块之间缺少空行。")
for previous, current in zip(cues, cues[1:]):
if current[0] < previous[0]:
add("warning", "W_START_ORDER", current[2], current[3], "开始时间比前一个有效字幕块更早,需核对排列。")
latest_end, latest_block = None, None
for start, end, number, line in sorted(cues):
if latest_end is not None and start < latest_end:
add("warning", "W_OVERLAP", number, line, f"时间区间与第 {latest_block} 块重叠;重叠可能有意安排,请人工复核。")
if latest_end is None or end > latest_end:
latest_end, latest_block = end, number
findings.sort(key=lambda item: (item["line"], item["code"]))
return {
"project_id": "XM-20261025-05",
"schema_version": 1,
"source_name": source_name,
"input_sha256": hashlib.sha256(raw).hexdigest(),
"encoding": "UTF-8 BOM" if raw.startswith(b"\xef\xbb\xbf") else "UTF-8",
"subtitle_blocks": len(blocks),
"errors": sum(item["severity"] == "error" for item in findings),
"warnings": sum(item["severity"] == "warning" for item in findings),
"scope": "仅检查本工具列出的 SRT 结构规则;未核验翻译、听校、阅读体验、音画同步或平台准入。",
"findings": findings,
}
def write_report(input_path, report_path):
input_path, report_path = Path(input_path), Path(report_path)
if input_path.resolve() == report_path.resolve():
raise ValueError("报告路径不得与输入相同。")
if report_path.exists():
raise ValueError("报告路径已存在;为保护文件,请提供新的报告文件名。")
report = check_bytes(input_path.read_bytes(), input_path.name)
# x 模式确保即使检查后出现同名文件,也拒绝覆盖。
with report_path.open("x", encoding="utf-8", newline="\n") as stream:
json.dump(report, stream, ensure_ascii=False, indent=2)
stream.write("\n")
return report
def main(argv=None):
parser = argparse.ArgumentParser(description="离线只读检查 UTF-8 SRT,将结果写入独立新 JSON 报告。")
parser.add_argument("input", help="客户授权的 SRT 文件副本")
parser.add_argument("report", help="尚不存在的 JSON 报告路径,父文件夹须已存在")
args = parser.parse_args(argv)
try:
report = write_report(args.input, args.report)
except (OSError, ValueError) as exc:
print(f"未生成报告:{exc}", file=sys.stderr)
return 2
print(f"已生成结构报告:{report['subtitle_blocks']} 块,{report['errors']} 个错误,{report['warnings']} 个警告。")
return 1 if report["errors"] else 0
if __name__ == "__main__":
sys.exit(main())
源码许可
MIT License
Copyright (c) 2026 560674
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
使用免费,赞助自愿
文章、源码和样例均可免费获取。若这个小工具帮到了你,可以自愿赞助;赞助不解锁额外功能,不对应订单,也不会自动发货。本页不收集字幕或联系方式。
查看自愿赞助码

