用 PYTHON 编写的一个分镜段落文本与SRT字幕合并匹配的函数实例

2025-07-06 11:37:59 4 分享链接 开发笔记 python

def on_format_materials():
    text = text_box.get("1.0", tk.END).strip()
    if not text:
        messagebox.showerror("错误", "文本框内容不能为空,请输入有效的文本。")
        return
    if not subtitle_content:
        messagebox.showerror("错误", "请先选择字幕文件!")
        return

    # 解析字幕内容为结构化数据(合并多行文本)
    subtitle_blocks = parse_subtitles(subtitle_content)
    if not subtitle_blocks:
        messagebox.showerror("错误", "字幕格式不正确,无法解析!")
        return

    # 获取整个字幕的最后结束时间
    overall_end_time = subtitle_blocks[-1]['end_time'] if subtitle_blocks else ""

    # 尝试按空行分隔段落
    paragraphs = text.split('\n\n')
    if len(paragraphs) == 1:
        # 若没有空行分隔,按序号分隔段落
        pattern = re.compile(r'^\d+\.\s', re.MULTILINE)
        matches = pattern.finditer(text)
        indices = [match.start() for match in matches]
        paragraphs = []
        for i in range(len(indices)):
            if i == len(indices) - 1:
                paragraphs.append(text[indices[i]:].strip())
            else:
                paragraphs.append(text[indices[i]:indices[i + 1]].strip())

    # 清理段落:移除序号和空白
    cleaned_paragraphs = []
    for para in paragraphs:
        # 移除行首数字序号(如 "1. ", "2) " 等)
        para = re.sub(r'^\s*\d+[.\)]\s*', '', para, flags=re.MULTILINE)
        cleaned_paragraphs.append(para.strip())

    # 为每个段落设置初始结束时间(整个字幕的最后结束时间)
    paragraph_end_times = {i: overall_end_time for i in range(len(cleaned_paragraphs))}

    # 从后往前处理每个段落
    last_matched_time = overall_end_time  # 记录上一个匹配的结束时间
    for p_idx in range(len(cleaned_paragraphs) - 1, -1, -1):
        para = cleaned_paragraphs[p_idx]
        para_lines = para.split('\n')
        matched = False  # 标记当前段落是否匹配成功

        # 从后往前处理段落中的每一行
        for line_idx in range(len(para_lines) - 1, -1, -1):
            line = para_lines[line_idx].strip()
            if not line:
                continue

            best_match = None
            best_match_index = -1

            # 从后往前匹配字幕块
            for s_idx in range(len(subtitle_blocks) - 1, -1, -1):
                sub_block = subtitle_blocks[s_idx]
                sub_text = sub_block['text']

                # 计算相似度(基于前缀子串,结果为1.0或0.0)
                similarity = calculate_similarity(line, sub_text)

                # 只要匹配成功(相似度为1.0),立即记录并跳出循环
                if similarity == 1.0:
                    best_match = sub_block
                    best_match_index = s_idx
                    break  # 找到匹配后立即退出字幕循环

            # 如果找到匹配,更新段落结束时间并删除已匹配索引及之后的所有字幕
            if best_match is not None:
                paragraph_end_times[p_idx] = best_match['end_time']
                last_matched_time = best_match['end_time']  # 更新最后匹配时间
                matched = True  # 标记为已匹配

                # 删除从best_match_index到结尾的所有字幕块
                if best_match_index != -1:
                    del subtitle_blocks[best_match_index:]

                # 跳出段落行循环,继续处理下一个段落
                break

        # 如果当前段落未匹配到任何字幕,使用上一个匹配的结束时间
        if not matched:
            paragraph_end_times[p_idx] = last_matched_time

    # 构建最终格式化文本
    formatted_text = ""
    for i, para in enumerate(cleaned_paragraphs):
        if para:
            formatted_text += para + '\n' + paragraph_end_times[i] + '\n\n'

    formatted_text = formatted_text.strip()
    text_box.delete("1.0", tk.END)
    text_box.insert(tk.END, formatted_text)

def parse_subtitles(subtitle_content):
    """解析字幕内容为结构化数据,将多行文本合并为一行"""
    blocks = []
    pattern = re.compile(r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n([\s\S]*?)(?=\n\n|$)')

    for match in pattern.finditer(subtitle_content):
        index = match.group(1)
        start_time = match.group(2)
        end_time = match.group(3)
        # 将多行文本合并为一行,用空格连接
        text = ' '.join(match.group(4).strip().split('\n'))

        blocks.append({
            'index': index,
            'start_time': start_time,
            'end_time': end_time,
            'text': text
        })

    return blocks

def calculate_similarity(line1, line2):
    """根据前缀子串关系计算相似度(结果为1.0或0.0)"""
    # 预处理:转换为小写并移除标点
    line1_clean = re.sub(r'[^\w\s]', '', line1).lower()
    line2_clean = re.sub(r'[^\w\s]', '', line2).lower()

    # 完全相等检查
    if line1_clean == line2_clean:
        return 1.0

    # 获取长度关系
    len1, len2 = len(line1_clean), len(line2_clean)

    # 段落文本长度 > 字幕文本
    if len1 > len2:
        # 检查字幕是否是段落的前缀子串
        return 1.0 if line1_clean.startswith(line2_clean) else 0.0

    # 段落文本长度 < 字幕文本
    elif len1 < len2:
        # 检查段落是否是字幕的前缀子串
        return 1.0 if line2_clean.startswith(line1_clean) else 0.0

    # 长度相等但内容不同
    else:
        return 0.0

以下是整合了所有优化建议的完整代码:

def on_format_materials():
    text = text_box.get("1.0", tk.END).strip()
    if not text:
        messagebox.showerror("错误", "文本框内容不能为空,请输入有效的文本。")
        return
    if not subtitle_content:
        messagebox.showerror("错误", "请先选择字幕文件!")
        return
    
    # 解析字幕内容为结构化数据(合并多行文本)
    subtitle_blocks = parse_subtitles(subtitle_content)
    if not subtitle_blocks:
        messagebox.showerror("错误", "字幕格式不正确,无法解析!")
        return
    
    # 获取整个字幕的最后结束时间
    overall_end_time = subtitle_blocks[-1]['end_time'] if subtitle_blocks else ""
    
    # 尝试按空行分隔段落
    paragraphs = text.split('\n\n')
    if len(paragraphs) == 1:
        # 若没有空行分隔,按序号分隔段落
        pattern = re.compile(r'^\d+\.\s', re.MULTILINE)
        matches = pattern.finditer(text)
        indices = [match.start() for match in matches]
        paragraphs = []
        for i in range(len(indices)):
            if i == len(indices) - 1:
                paragraphs.append(text[indices[i]:].strip())
            else:
                paragraphs.append(text[indices[i]:indices[i + 1]].strip())
    
    # 清理段落:移除序号和空白
    cleaned_paragraphs = []
    for para in paragraphs:
        # 移除行首数字序号(如 "1. ", "2) " 等)
        para = re.sub(r'^\s*\d+[.\)]\s*', '', para, flags=re.MULTILINE)
        cleaned_paragraphs.append(para.strip())
    
    # 为每个段落设置初始结束时间(整个字幕的最后结束时间)
    paragraph_end_times = {i: overall_end_time for i in range(len(cleaned_paragraphs))}
    
    # 从后往前处理每个段落
    for p_idx in range(len(cleaned_paragraphs) - 1, -1, -1):
        para = cleaned_paragraphs[p_idx]
        para_lines = para.split('\n')
        
        # 从后往前处理段落中的每一行
        for line_idx in range(len(para_lines) - 1, -1, -1):
            line = para_lines[line_idx].strip()
            if not line:
                continue
                
            best_match = None
            best_match_index = -1
            
            # 从后往前匹配字幕块
            for s_idx in range(len(subtitle_blocks) - 1, -1, -1):
                sub_block = subtitle_blocks[s_idx]
                sub_text = sub_block['text']
                
                # 计算相似度(基于前缀子串,结果为1.0或0.0)
                similarity = calculate_similarity(line, sub_text)
                
                # 只要匹配成功(相似度为1.0),立即记录并跳出循环
                if similarity == 1.0:
                    best_match = sub_block
                    best_match_index = s_idx
                    break  # 找到匹配后立即退出字幕循环
            
            # 如果找到匹配,更新段落结束时间并删除已匹配索引及之后的所有字幕
            if best_match is not None:
                paragraph_end_times[p_idx] = best_match['end_time']
                
                # 删除从best_match_index到结尾的所有字幕块
                if best_match_index != -1:
                    del subtitle_blocks[best_match_index:]  # 关键优化:删除当前及之后的所有字幕
                
                # 跳出段落行循环,继续处理下一个段落
                break
    
    # 构建最终格式化文本
    formatted_text = ""
    for i, para in enumerate(cleaned_paragraphs):
        if para:
            formatted_text += para + '\n' + paragraph_end_times[i] + '\n\n'
    
    formatted_text = formatted_text.strip()
    text_box.delete("1.0", tk.END)
    text_box.insert(tk.END, formatted_text)

def parse_subtitles(subtitle_content):
    """解析字幕内容为结构化数据,将多行文本合并为一行"""
    blocks = []
    pattern = re.compile(r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n([\s\S]*?)(?=\n\n|$)')
    
    for match in pattern.finditer(subtitle_content):
        index = match.group(1)
        start_time = match.group(2)
        end_time = match.group(3)
        # 将多行文本合并为一行,用空格连接
        text = ' '.join(match.group(4).strip().split('\n'))
        
        blocks.append({
            'index': index,
            'start_time': start_time,
            'end_time': end_time,
            'text': text
        })
    
    return blocks

def calculate_similarity(line1, line2):
    """根据前缀子串关系计算相似度(结果为1.0或0.0)"""
    # 预处理:转换为小写并移除标点
    line1_clean = re.sub(r'[^\w\s]', '', line1).lower()
    line2_clean = re.sub(r'[^\w\s]', '', line2).lower()
    
    # 完全相等检查
    if line1_clean == line2_clean:
        return 1.0
    
    # 获取长度关系
    len1, len2 = len(line1_clean), len(line2_clean)
    
    # 段落文本长度 > 字幕文本
    if len1 > len2:
        # 检查字幕是否是段落的前缀子串
        return 1.0 if line1_clean.startswith(line2_clean) else 0.0
    
    # 段落文本长度 < 字幕文本
    elif len1 < len2:
        # 检查段落是否是字幕的前缀子串
        return 1.0 if line2_clean.startswith(line1_clean) else 0.0
    
    # 长度相等但内容不同
    else:
        return 0.0

核心优化点:

  1. 删除已匹配索引后的所有字幕

    del subtitle_blocks[best_match_index:]
    • 确保后续段落只能匹配到更早的字幕
    • 大幅减少循环次数,提高性能
  2. 严格的前缀子串匹配

    • 只判断是否为前缀子串(返回1.0或0.0)
    • 移除了所有相似度阈值判断
  3. 提前终止循环

    • 找到第一个匹配后立即退出循环
    • 避免不必要的后续比较

这个版本在保持匹配准确性的同时,通过批量删除字幕块显著优化了性能,特别适合处理较长的字幕文件和文本内容。

用 PYTHON 编写的一个分镜段落文本与SRT字幕合并匹配的函数实例