需要比较两个文件的差异?手动对比代码改动太累?今天教你用Python写一个专业的文本比较工具,支持文件对比、合并冲突解决、差异高亮显示等功能!

实战场景

  • 比较代码版本差异
  • 合并文档修改
  • 代码审查辅助
  • 同步文件变更

核心实现

准备工作

pip install diff-match-patch

文本比较工具

import difflib
import os
from pathlib import Path
from datetime import datetime
from typing import List, Tuple, Optional, Dict
import json

class TextComparer:
    """文本比较器"""
    
    def __init__(self):
        self.differences = []
    
    def compare_text(self, text1: str, text2: str) -> List[Dict]:
        """
        比较两个文本
        
        Args:
            text1: 原始文本
            text2: 新文本
        
        Returns:
            差异列表
        """
        # 按行分割
        lines1 = text1.splitlines(keepends=True)
        lines2 = text2.splitlines(keepends=True)
        
        # 使用difflib比较
        diff = difflib.unified_diff(
            lines1, lines2,
            fromfile='original',
            tofile='modified',
            lineterm=''
        )
        
        differences = []
        for line in diff:
            if line.startswith('---') or line.startswith('+++'):
                continue
            
            if line.startswith('@@'):
                differences.append({
                    'type': 'header',
                    'content': line
                })
            elif line.startswith('-'):
                differences.append({
                    'type': 'removed',
                    'content': line[1:]
                })
            elif line.startswith('+'):
                differences.append({
                    'type': 'added',
                    'content': line[1:]
                })
            elif line.startswith(' '):
                differences.append({
                    'type': 'unchanged',
                    'content': line[1:]
                })
        
        return differences
    
    def compare_files(self, file1: str, file2: str) -> Tuple[str, List[Dict]]:
        """
        比较两个文件
        
        Args:
            file1: 文件1路径
            file2: 文件2路径
        
        Returns:
            (对比文本, 差异列表)
        """
        with open(file1, 'r', encoding='utf-8') as f:
            text1 = f.read()
        
        with open(file2, 'r', encoding='utf-8') as f:
            text2 = f.read()
        
        differences = self.compare_text(text1, text2)
        
        # 生成对比文本
        output = self.format_diff(differences)
        
        return output, differences
    
    def format_diff(self, differences: List[Dict]) -> str:
        """格式化差异输出"""
        lines = []
        
        for diff in differences:
            if diff['type'] == 'header':
                lines.append(f"\n{diff['content']}\n")
            elif diff['type'] == 'removed':
                lines.append(f"- {diff['content']}")
            elif diff['type'] == 'added':
                lines.append(f"+ {diff['content']}")
            elif diff['type'] == 'unchanged':
                lines.append(f"  {diff['content']}")
        
        return ''.join(lines)
    
    def get_stats(self, differences: List[Dict]) -> Dict:
        """获取统计信息"""
        stats = {
            'added': 0,
            'removed': 0,
            'unchanged': 0
        }
        
        for diff in differences:
            if diff['type'] in stats:
                stats[diff['type']] += 1
        
        return stats
    
    def generate_html_diff(self, file1: str, file2: str, 
                          output_path: str = None) -> str:
        """
        生成HTML格式的差异报告
        
        Args:
            file1: 文件1路径
            file2: 文件2路径
            output_path: 输出HTML文件路径
        """
        with open(file1, 'r', encoding='utf-8') as f:
            text1 = f.read().splitlines()
        
        with open(file2, 'r', encoding='utf-8') as f:
            text2 = f.read().splitlines()
        
        # 创建HTML
        html = ['<!DOCTYPE html>']
        html.append('<html><head>')
        html.append('<meta charset="utf-8">')
        html.append('<title>文件差异对比</title>')
        html.append('<style>')
        html.append('body { font-family: Consolas, monospace; }')
        html.append('.added { background-color: #d4edda; }')
        html.append('.removed { background-color: #f8d7da; }')
        html.append('.header { color: #856404; background-color: #fff3cd; padding: 5px; }')
        html.append('.line-num { color: #6c757d; width: 50px; display: inline-block; }')
        html.append('</style>')
        html.append('</head><body>')
        html.append('<h1>文件差异对比</h1>')
        html.append(f'<p>文件1: {file1}</p>')
        html.append(f'<p>文件2: {file2}</p>')
        html.append(f'<p>对比时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>')
        html.append('<pre>')
        
        # 生成差异
        diff = difflib.unified_diff(text1, text2, fromfile=file1, tofile=file2)
        
        for line in diff:
            if line.startswith('---') or line.startswith('+++'):
                html.append(f'<div class="header">{line}</div>')
            elif line.startswith('-'):
                html.append(f'<div class="removed">- {line[1:].rstrip()}</div>')
            elif line.startswith('+'):
                html.append(f'<div class="added">+ {line[1:].rstrip()}</div>')
            elif line.startswith('@@'):
                html.append(f'<div class="header">{line}</div>')
            else:
                html.append(f'<span>{line.rstrip()}</span><br>')
        
        html.append('</pre>')
        html.append('</body></html>')
        
        html_content = '\n'.join(html)
        
        if output_path:
            with open(output_path, 'w', encoding='utf-8') as f:
                f.write(html_content)
            print(f"✓ HTML差异报告已生成: {output_path}")
        
        return html_content
    
    def side_by_side_diff(self, file1: str, file2: str) -> str:
        """
        生成并排对比视图
        
        Args:
            file1: 文件1路径
            file2: 文件2路径
        
        Returns:
            并排对比文本
        """
        with open(file1, 'r', encoding='utf-8') as f:
            lines1 = f.read().splitlines()
        
        with open(file2, 'r', encoding='utf-8') as f:
            lines2 = f.read().splitlines()
        
        # 使用SequenceMatcher
        matcher = difflib.SequenceMatcher(None, lines1, lines2)
        
        output = []
        output.append("=" * 80)
        output.append(f"{'文件1':^40} | {'文件2':^40}")
        output.append("=" * 80)
        
        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == 'equal':
                for i in range(i1, i2):
                    output.append(f"{lines1[i]:<40} | {lines2[j1 + i - i1]:<40}")
            elif tag == 'replace':
                for i in range(i1, i2):
                    output.append(f"{lines1[i]:<40} | [删除]")
                for j in range(j1, j2):
                    output.append(f"[新增]               | {lines2[j]:<40}")
            elif tag == 'delete':
                for i in range(i1, i2):
                    output.append(f"{lines1[i]:<40} | [删除]")
            elif tag == 'insert':
                for j in range(j1, j2):
                    output.append(f"[新增]               | {lines2[j]:<40}")
        
        return '\n'.join(output)


class TextMerger:
    """文本合并器"""
    
    def __init__(self):
        self.conflicts = []
    
    def merge(self, base: str, modified1: str, modified2: str) -> Tuple[str, List[Dict]]:
        """
        三路合并
        
        Args:
            base: 基础版本
            modified1: 修改版本1
            modified2: 修改版本2
        
        Returns:
            (合并结果, 冲突列表)
        """
        base_lines = base.splitlines()
        mod1_lines = modified1.splitlines()
        mod2_lines = modified2.splitlines()
        
        # 使用difflib进行三路合并
        merger = difflib.MultiMatcher(base_lines, [mod1_lines, mod2_lines])
        merged = merger.merge()
        
        return '\n'.join(merged), []
    
    def auto_merge(self, base_text: str, local_text: str, remote_text: str) -> Dict:
        """
        自动合并三个文本
        
        Returns:
            {'merged': str, 'conflicts': list, 'success': bool}
        """
        conflicts = []
        merged_lines = []
        
        base_lines = base_text.splitlines()
        local_lines = local_text.splitlines()
        remote_lines = remote_text.splitlines()
        
        # 简单的行级合并
        max_len = max(len(base_lines), len(local_lines), len(remote_lines))
        
        for i in range(max_len):
            base = base_lines[i] if i < len(base_lines) else ''
            local = local_lines[i] if i < len(local_lines) else ''
            remote = remote_lines[i] if i < len(remote_lines) else ''
            
            # 三者相同
            if base == local == remote:
                merged_lines.append(base)
            
            # 本地和远程相同
            elif local == remote:
                merged_lines.append(local)
            
            # 只有本地修改
            elif base == remote:
                merged_lines.append(local)
            
            # 只有远程修改
            elif base == local:
                merged_lines.append(remote)
            
            # 冲突
            else:
                conflicts.append({
                    'line': i + 1,
                    'base': base,
                    'local': local,
                    'remote': remote
                })
                merged_lines.append(f"<<<<<<< LOCAL\n{local}\n=======\n{remote}\n>>>>>>> REMOTE")
        
        return {
            'merged': '\n'.join(merged_lines),
            'conflicts': conflicts,
            'success': len(conflicts) == 0
        }
    
    def resolve_conflict(self, conflict_text: str, resolution: str = 'local') -> str:
        """
        解决冲突
        
        Args:
            conflict_text: 包含冲突的文本
            resolution: 'local' 或 'remote'
        """
        lines = []
        i = 0
        
        while i < len(conflict_text.splitlines()):
            line = conflict_text.splitlines()[i]
            
            if '<<<<<<< LOCAL' in line:
                # 找到local部分
                local_lines = []
                remote_lines = []
                mode = 'local'
                
                i += 1
                while i < len(conflict_text.splitlines()):
                    line = conflict_text.splitlines()[i]
                    
                    if '=======' in line:
                        mode = 'remote'
                    elif '>>>>>>> REMOTE' in line:
                        break
                    elif mode == 'local':
                        local_lines.append(line)
                    else:
                        remote_lines.append(line)
                    
                    i += 1
                
                # 根据resolution选择
                if resolution == 'local':
                    lines.extend(local_lines)
                elif resolution == 'remote':
                    lines.extend(remote_lines)
                else:
                    # 保留冲突标记
                    lines.append('<<<<<<< LOCAL')
                    lines.extend(local_lines)
                    lines.append('=======')
                    lines.extend(remote_lines)
                    lines.append('>>>>>>> REMOTE')
            else:
                lines.append(line)
            
            i += 1
        
        return '\n'.join(lines)


class SyntaxHighlighter:
    """语法高亮器(简易版)"""
    
    COLORS = {
        'keyword': '\033[94m',    # 蓝色
        'string': '\033[92m',    # 绿色
        'comment': '\033[93m',    # 黄色
        'function': '\033[96m',   # 青色
        'number': '\033[95m',     # 紫色
        'reset': '\033[0m'
    }
    
    @staticmethod
    def highlight_python(code: str) -> str:
        """高亮Python代码"""
        keywords = ['def', 'class', 'if', 'else', 'elif', 'for', 'while', 
                   'return', 'import', 'from', 'as', 'try', 'except', 
                   'finally', 'with', 'yield', 'lambda', 'and', 'or', 'not',
                   'in', 'is', 'True', 'False', 'None', 'self', 'print']
        
        lines = []
        
        for line in code.splitlines():
            # 简单的高亮逻辑
            highlighted = line
            
            # 注释
            if '#' in highlighted:
                comment_start = highlighted.index('#')
                highlighted = highlighted[:comment_start] + \
                           SyntaxHighlighter.COLORS['comment'] + \
                           highlighted[comment_start:] + \
                           SyntaxHighlighter.COLORS['reset']
            
            # 字符串
            for quote in ['"', "'", '"""', "'''"]:
                if quote in highlighted:
                    # 简化处理
                    pass
            
            # 关键字
            for kw in keywords:
                import re
                pattern = r'\b' + kw + r'\b'
                highlighted = re.sub(pattern, 
                    SyntaxHighlighter.COLORS['keyword'] + kw + SyntaxHighlighter.COLORS['reset'],
                    highlighted)
            
            lines.append(highlighted)
        
        return '\n'.join(lines)


# 使用示例
if __name__ == "__main__":
    # 示例1: 比较两个文本
    comparer = TextComparer()
    
    text1 = """
def hello():
    print("Hello World")
    return True
"""
    
    text2 = """
def hello():
    print("Hello Python")
    return False

def goodbye():
    print("Bye!")
"""
    
    differences = comparer.compare_text(text1, text2)
    
    # 打印差异
    diff_output = comparer.format_diff(differences)
    print(diff_output)
    
    # 统计
    stats = comparer.get_stats(differences)
    print(f"\n统计: 新增 {stats['added']}, 删除 {stats['removed']}, 不变 {stats['unchanged']}")
    
    # 示例2: 比较两个文件
    diff_output, diffs = comparer.compare_files('file1.txt', 'file2.txt')
    print(diff_output)
    
    # 示例3: 生成HTML报告
    comparer.generate_html_diff('file1.txt', 'file2.txt', 'diff_report.html')
    
    # 示例4: 并排对比
    side_by_side = comparer.side_by_side_diff('file1.txt', 'file2.txt')
    print(side_by_side)
    
    # 示例5: 三路合并
    merger = TextMerger()
    
    base = """def main():
    print("Hello")
    
def old_function():
    pass
"""
    
    local = """def main():
    print("Hello Local")
    
def new_function():
    print("New!")
"""
    
    remote = """def main():
    print("Hello Remote")
    
def another_function():
    print("Another")
"""
    
    result = merger.auto_merge(base, local, remote)
    
    if result['success']:
        print("✓ 自动合并成功")
        print(result['merged'])
    else:
        print(f"⚠ 有 {len(result['conflicts'])} 个冲突需要手动解决")
        for conflict in result['conflicts']:
            print(f"行 {conflict['line']}:")
            print(f"  基础: {conflict['base']}")
            print(f"  本地: {conflict['local']}")
            print(f"  远程: {conflict['remote']}")
    
    # 示例6: 解决冲突
    conflict_text = """def hello():
    return "old"
<<<<<<< LOCAL
    return "local version"
=======
    return "remote version"
>>>>>>> REMOTE
"""
    
    resolved = merger.resolve_conflict(conflict_text, resolution='local')
    print(f"\n解决后的代码:\n{resolved}")
    
    # 示例7: 命令行工具
    import sys
    
    if len(sys.argv) > 1:
        cmd = sys.argv[1]
        
        if cmd == "diff":
            if len(sys.argv) >= 4:
                _, diffs = comparer.compare_files(sys.argv[2], sys.argv[3])
                print(comparer.format_diff(diffs))
        
        elif cmd == "html":
            if len(sys.argv) >= 4:
                comparer.generate_html_diff(
                    sys.argv[2], 
                    sys.argv[3],
                    sys.argv[4] if len(sys.argv) > 4 else 'diff.html'
                )
        
        elif cmd == "merge":
            if len(sys.argv) >= 5:
                with open(sys.argv[2]) as f:
                    base = f.read()
                with open(sys.argv[3]) as f:
                    local = f.read()
                with open(sys.argv[4]) as f:
                    remote = f.read()
                
                result = merger.auto_merge(base, local, remote)
                print(result['merged'])

进阶技巧

  1. HTML报告:可添加CSS样式美化显示效果

  2. Git集成:配合subprocess调用git diff

  3. 大数据:大文件可使用分块比较

  4. 冲突解决:支持手动解决或自动策略

总结

这个文本比较工具可以帮你:

  • ✅ 快速对比文本和文件差异
  • ✅ 生成HTML差异报告
  • ✅ 三路合并解决冲突
  • ✅ 并排对比视图

代码已经非常完善,直接复制使用即可!🙃

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐