Python编程:实现文件比对

Python提供了多个用于文件比对的库,适用于不同的比较场景。以下是主要的文件比对库及其特点:

1. 标准库中的比对工具

1.1 filecmp 模块

  • 功能:文件和目录比较

  • 特点

    • 比较文件内容(浅层和深层比较)

    • 比较目录结构

    • 内置dircmp类用于目录比较

  • 典型用途

    import filecmp
    # 文件比较
    filecmp.cmp('file1.txt', 'file2.txt', shallow=False)
    # 目录比较
    comparison = filecmp.dircmp('dir1', 'dir2')

1.2 difflib 模块

  • 功能:文本差异比较

  • 特点

    • 生成差异报告(unified diff, context diff等)

    • 支持行级比较

    • 可计算相似度比率

  • 典型用途

    import difflib
    # 生成差异
    diff = difflib.unified_diff(text1.splitlines(), text2.splitlines())
    # 相似度比较
    similarity = difflib.SequenceMatcher(None, text1, text2).ratio()

2. 第三方库

2.1 deepdiff (推荐)

  • 功能:深度差异比较

  • 特点

    • 支持复杂数据结构比较

    • 可比较文本、字典、列表、集合等

    • 提供详细的差异报告

  • 安装pip install deepdiff

  • 典型用途

    from deepdiff import DeepDiff
    diff = DeepDiff(file1_content, file2_content)

2.2 python-diff/diff-match-patch

  • 功能:高级文本差异比较

  • 特点

    • 支持多种差异算法

    • 可用于文本合并

  • 安装pip install diff-match-patch

  • 典型用途

    from diff_match_patch import diff_match_patch
    dmp = diff_match_patch()
    diffs = dmp.diff_main(text1, text2)

2.3 hashlib (用于快速比较)

  • 功能:通过哈希值比较文件

  • 特点

    • 快速判断文件是否相同

    • 支持多种哈希算法

  • 典型用途

    import hashlib
    def file_hash(filename):
        h = hashlib.md5()
        with open(filename, 'rb') as f:
            while chunk := f.read(8192):
                h.update(chunk)
        return h.hexdigest()
    
    file_hash('file1.txt') == file_hash('file2.txt')

2.4 pandas (用于结构化数据比较)

  • 功能:CSV/Excel等结构化文件比较

  • 特点

    • 表格数据比较

    • 支持列级、行级比较

  • 典型用途

    import pandas as pd
    df1 = pd.read_csv('file1.csv')
    df2 = pd.read_csv('file2.csv')
    diff = df1.compare(df2)

2.5 binaryornot (二进制文件识别)

  • 功能:识别二进制文件

  • 特点

    • 判断文件是否为二进制

    • 避免对二进制文件进行文本比较

  • 安装pip install binaryornot

  • 典型用途

    from binaryornot.check import is_binary
    if not is_binary('file.bin'):
        # 进行文本比较

3. 特殊用途库

3.1 pygtrie (用于文件名比较)

  • 功能:基于前缀树的文件名比较

  • 特点

    • 高效比较大量文件名

    • 支持前缀匹配

  • 安装pip install pygtrie

3.2 fuzzywuzzy (模糊比较)

  • 功能:模糊字符串匹配

  • 特点

    • 处理拼写差异

    • 计算相似度分数

  • 安装pip install fuzzywuzzy python-Levenshtein

  • 典型用途

    from fuzzywuzzy import fuzz
    similarity = fuzz.ratio("file1.txt", "file2.txt")

4. 综合比较工具选择建议

场景 推荐库 特点
简单文件内容比较 filecmp 内置,无需安装
详细文本差异 difflib 内置,生成差异报告
深度结构比较 deepdiff 复杂数据结构比较
大型文件快速比较 hashlib 哈希值比较
表格数据比较 pandas 结构化数据比较
二进制文件处理 binaryornot 识别二进制文件
模糊匹配 fuzzywuzzy 处理名称差异

5. 高级应用示例:综合使用多个库

import os
import filecmp
import difflib
from deepdiff import DeepDiff
from binaryornot.check import is_binary

def comprehensive_compare(file1, file2):
    # 基本检查
    if os.path.getsize(file1) != os.path.getsize(file2):
        print("文件大小不同")
        return False
    
    # 二进制检查
    if is_binary(file1) or is_binary(file2):
        print("二进制文件,使用哈希比较")
        return filecmp.cmp(file1, file2, shallow=False)
    
    # 文本文件比较
    with open(file1, 'r') as f1, open(file2, 'r') as f2:
        content1 = f1.read()
        content2 = f2.read()
    
    if content1 == content2:
        return True
    
    # 生成差异报告
    diff = difflib.unified_diff(
        content1.splitlines(), 
        content2.splitlines(),
        fromfile=file1,
        tofile=file2
    )
    print('\n'.join(diff))
    
    # 深度差异分析
    ddiff = DeepDiff(content1, content2)
    print(f"深度差异: {ddiff}")
    
    return False

这些库可以根据具体需求组合使用,构建强大的文件比对工具。

PyQt实现类似Beyond Compare的功能

Beyond Compare是一款强大的文件和文件夹比较工具,下面将使用PyQt5实现一个简化版的类似功能,包含文件比较、文件夹比较和文本差异高亮显示。

完整实现代码

import sys
import os
import filecmp
import difflib
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QTextEdit, QPushButton, QFileDialog, QTreeWidget,
                             QTreeWidgetItem, QSplitter, QMessageBox)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QColor, QFont, QBrush


class FileCompareApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyCompare - 类似Beyond Compare的工具")
        self.setGeometry(100, 100, 1200, 800)
        
        self.init_ui()
        self.left_dir = ""
        self.right_dir = ""
        
    def init_ui(self):
        # 主窗口布局
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        
        # 顶部按钮区域
        button_layout = QHBoxLayout()
        
        self.left_file_btn = QPushButton("选择左侧文件/文件夹")
        self.left_file_btn.clicked.connect(lambda: self.select_file_or_dir('left'))
        button_layout.addWidget(self.left_file_btn)
        
        self.right_file_btn = QPushButton("选择右侧文件/文件夹")
        self.right_file_btn.clicked.connect(lambda: self.select_file_or_dir('right'))
        button_layout.addWidget(self.right_file_btn)
        
        self.compare_btn = QPushButton("比较")
        self.compare_btn.clicked.connect(self.compare)
        button_layout.addWidget(self.compare_btn)
        
        main_layout.addLayout(button_layout)
        
        # 主内容区域
        self.splitter = QSplitter(Qt.Horizontal)
        
        # 左侧文件树
        self.left_tree = QTreeWidget()
        self.left_tree.setHeaderLabel("左侧文件")
        self.left_tree.itemClicked.connect(self.on_tree_item_clicked)
        self.splitter.addWidget(self.left_tree)
        
        # 右侧文件树
        self.right_tree = QTreeWidget()
        self.right_tree.setHeaderLabel("右侧文件")
        self.right_tree.itemClicked.connect(self.on_tree_item_clicked)
        self.splitter.addWidget(self.right_tree)
        
        # 底部文本比较区域
        self.text_splitter = QSplitter(Qt.Horizontal)
        
        self.left_text = QTextEdit()
        self.left_text.setReadOnly(True)
        self.left_text.setFont(QFont("Courier New", 10))
        self.text_splitter.addWidget(self.left_text)
        
        self.right_text = QTextEdit()
        self.right_text.setReadOnly(True)
        self.right_text.setFont(QFont("Courier New", 10))
        self.text_splitter.addWidget(self.right_text)
        
        # 垂直分割器
        v_splitter = QSplitter(Qt.Vertical)
        v_splitter.addWidget(self.splitter)
        v_splitter.addWidget(self.text_splitter)
        v_splitter.setSizes([400, 400])
        
        main_layout.addWidget(v_splitter)
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)
        
    def select_file_or_dir(self, side):
        options = QFileDialog.Options()
        path, _ = QFileDialog.getOpenFileName(self, f"选择{side}文件", "", "All Files (*);;Text Files (*.txt)", options=options)
        
        if not path:  # 如果用户没有选择文件,尝试选择文件夹
            path = QFileDialog.getExistingDirectory(self, f"选择{side}文件夹")
            if not path:
                return
        
        if side == 'left':
            self.left_dir = path
            self.populate_tree(self.left_tree, path)
        else:
            self.right_dir = path
            self.populate_tree(self.right_tree, path)
    
    def populate_tree(self, tree, path):
        tree.clear()
        
        if os.path.isfile(path):
            item = QTreeWidgetItem([os.path.basename(path)])
            item.setData(0, Qt.UserRole, path)
            tree.addTopLevelItem(item)
        elif os.path.isdir(path):
            self.add_dir_to_tree(tree, path)
    
    def add_dir_to_tree(self, tree, dir_path, parent=None):
        try:
            items = os.listdir(dir_path)
        except PermissionError:
            return
        
        items.sort()
        
        for item_name in items:
            item_path = os.path.join(dir_path, item_name)
            
            if parent is None:
                tree_item = QTreeWidgetItem([item_name])
                tree.addTopLevelItem(tree_item)
            else:
                tree_item = QTreeWidgetItem(parent, [item_name])
            
            tree_item.setData(0, Qt.UserRole, item_path)
            
            if os.path.isdir(item_path):
                self.add_dir_to_tree(tree, item_path, tree_item)
    
    def on_tree_item_clicked(self, item, column):
        file_path = item.data(0, Qt.UserRole)
        
        if os.path.isfile(file_path):
            sender = self.sender()
            
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
            except UnicodeDecodeError:
                content = "无法以文本形式显示此文件"
            
            if sender == self.left_tree:
                self.left_text.setPlainText(content)
            else:
                self.right_text.setPlainText(content)
    
    def compare(self):
        if not self.left_dir or not self.right_dir:
            QMessageBox.warning(self, "警告", "请先选择左右两侧的文件或文件夹")
            return
        
        # 比较文件或文件夹
        if os.path.isfile(self.left_dir) and os.path.isfile(self.right_dir):
            self.compare_files(self.left_dir, self.right_dir)
        elif os.path.isdir(self.left_dir) and os.path.isdir(self.right_dir):
            self.compare_dirs(self.left_dir, self.right_dir)
        else:
            QMessageBox.warning(self, "警告", "请选择相同类型的项目进行比较(文件与文件,文件夹与文件夹)")
    
    def compare_files(self, file1, file2):
        try:
            with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
                lines1 = f1.readlines()
                lines2 = f2.readlines()
        except UnicodeDecodeError:
            QMessageBox.warning(self, "警告", "无法比较非文本文件")
            return
        
        self.left_text.setPlainText(''.join(lines1))
        self.right_text.setPlainText(''.join(lines2))
        
        # 高亮显示差异
        self.highlight_differences(lines1, lines2)
    
    def highlight_differences(self, lines1, lines2):
        # 清除之前的高亮
        self.left_text.setExtraSelections([])
        self.right_text.setExtraSelections([])
        
        # 使用difflib找出差异
        differ = difflib.SequenceMatcher(None, lines1, lines2)
        opcodes = differ.get_opcodes()
        
        left_selections = []
        right_selections = []
        
        for tag, i1, i2, j1, j2 in opcodes:
            if tag == 'replace' or tag == 'delete':
                # 左侧文件的高亮
                selection = QTextEdit.ExtraSelection()
                selection.cursor = self.left_text.textCursor()
                selection.cursor.setPosition(0)
                
                start_pos = sum(len(line) for line in lines1[:i1])
                end_pos = sum(len(line) for line in lines1[:i2])
                
                selection.cursor.setPosition(start_pos)
                selection.cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
                
                if tag == 'replace':
                    selection.format.setBackground(QColor(255, 255, 0))  # 黄色
                else:
                    selection.format.setBackground(QColor(255, 200, 200))  # 浅红色
                
                left_selections.append(selection)
            
            if tag == 'replace' or tag == 'insert':
                # 右侧文件的高亮
                selection = QTextEdit.ExtraSelection()
                selection.cursor = self.right_text.textCursor()
                selection.cursor.setPosition(0)
                
                start_pos = sum(len(line) for line in lines2[:j1])
                end_pos = sum(len(line) for line in lines2[:j2])
                
                selection.cursor.setPosition(start_pos)
                selection.cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
                
                if tag == 'replace':
                    selection.format.setBackground(QColor(255, 255, 0))  # 黄色
                else:
                    selection.format.setBackground(QColor(200, 255, 200))  # 浅绿色
                
                right_selections.append(selection)
        
        self.left_text.setExtraSelections(left_selections)
        self.right_text.setExtraSelections(right_selections)
    
    def compare_dirs(self, dir1, dir2):
        self.left_tree.clear()
        self.right_tree.clear()
        
        self.populate_tree(self.left_tree, dir1)
        self.populate_tree(self.right_tree, dir2)
        
        comparison = filecmp.dircmp(dir1, dir2)
        
        # 标记不同的文件
        self.mark_different_files(comparison)
    
    def mark_different_files(self, comparison):
        # 标记左侧独有的文件为红色
        for item in self.get_all_items(self.left_tree):
            rel_path = os.path.relpath(item.data(0, Qt.UserRole), self.left_dir)
            if rel_path in comparison.left_only:
                item.setForeground(0, QBrush(QColor(255, 0, 0)))
        
        # 标记右侧独有的文件为绿色
        for item in self.get_all_items(self.right_tree):
            rel_path = os.path.relpath(item.data(0, Qt.UserRole), self.right_dir)
            if rel_path in comparison.right_only:
                item.setForeground(0, QBrush(QColor(0, 255, 0)))
        
        # 标记内容不同的文件为黄色
        for item in self.get_all_items(self.left_tree):
            rel_path = os.path.relpath(item.data(0, Qt.UserRole), self.left_dir)
            if rel_path in comparison.diff_files:
                item.setForeground(0, QBrush(QColor(255, 255, 0)))
        
        # 递归处理子目录
        for sub_dir in comparison.subdirs.values():
            self.mark_different_files(sub_dir)
    
    def get_all_items(self, tree):
        items = []
        iterator = QTreeWidgetItemIterator(tree)
        while iterator.value():
            items.append(iterator.value())
            iterator += 1
        return items


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = FileCompareApp()
    window.show()
    sys.exit(app.exec_())

功能说明

这个PyQt实现的Beyond Compare简化版包含以下功能:

  1. 文件/文件夹选择

    • 可以选择左侧和右侧的文件或文件夹进行比较

    • 支持通过文件对话框选择

  2. 目录比较

    • 显示两侧目录结构

    • 用不同颜色标记差异:

      • 红色:仅在左侧存在的文件

      • 绿色:仅在右侧存在的文件

      • 黄色:两侧都存在但内容不同的文件

  3. 文件内容比较

    • 显示文件内容差异

    • 高亮显示不同之处:

      • 黄色:被修改的内容

      • 浅红色:被删除的内容

      • 浅绿色:新增的内容

  4. 用户界面

    • 使用QSplitter实现可调整大小的分割窗口

    • 左侧和右侧对称布局,类似Beyond Compare

    • 使用树形控件显示目录结构

功能扩展迭代

要使这个工具更接近Beyond Compare,可以考虑添加以下功能:

  1. 二进制文件比较:添加十六进制查看器比较二进制文件

  2. 合并功能:允许用户选择将差异从一个文件复制到另一个文件

  3. 过滤选项:添加文件类型过滤和忽略特定文件/文件夹的选项

  4. 三向比较:支持三个文件或文件夹的比较

  5. 保存比较结果:将比较结果保存为报告

  6. 书签功能:允许用户标记重要的差异位置

  7. 同步功能:实现文件夹同步功能

这个实现提供了基本框架,你可以根据需要进一步扩展和完善功能。

你可能感兴趣的:(C++与python交互编程,python,哈希算法)