前言:考完文件,是不是经常遇到这样的痛点——标准工程模板与本地开发工程对比,手动找缺失、冗余文件费时又费力,稍不注意就漏查,影响项目提交效率?作为一名“高效摸鱼”的工程师,我利用摸鱼时间,用Python+PySide6开发了一款可视化文件双向校验工具,完美解决这个问题,今天分享给大家,直接复制就能用!

这款工具主打「多线程防卡顿」「暗黑科技风UI」「双向校验」「结果导出」,无需复杂操作,选择两个文件夹就能快速对比,自动识别缺失和冗余文件,适合所有需要文件一致性校验的场景(其他工程、文档对比都能用)。

一、工具核心亮点

先给大家亮个底,这款工具不是简单的代码堆砌,而是按照生产级标准开发,兼顾实用性和美观度,核心亮点如下:

  • 多线程架构:将耗时的文件扫描操作丢到后台线程,前台UI不卡顿,操作流畅,哪怕对比上千个文件也不会出现“假死”现象;

  • 暗黑科技风UI:精心设计的QSS样式,深蓝底色+荧光蓝高亮,字体清晰、布局合理,长时间使用不刺眼,贴合工程师审美;

  • 双向精准校验:以“标准模板文件夹”为基准,对比“待校验文件夹”,同时识别「缺失文件」(模板有、目标无)和「冗余文件」(模板无、目标有),相对路径对比,不遗漏子文件夹文件;

  • 鲁棒性强:完善的错误处理(路径无效、扫描异常),防止重复点击,鼠标状态切换提示,新手也能轻松上手;

  • 可复用性高:代码结构清晰,可直接修改适配自己的项目场景(如添加MD5内容校验、忽略指定文件等)。

二、开发环境准备

工具基于Python+PySide6开发,环境搭建简单,无需复杂配置,步骤如下:

  1. Python版本:3.8+(推荐3.9/3.10,兼容性更好);

  2. 核心依赖:PySide6(负责可视化界面),安装命令:pip install PySide6

  3. 开发工具:任意Python编辑器(PyCharm、VS Code均可);

  4. 打包工具(可选):如需生成可执行exe文件,安装pyinstaller:pip install pyinstaller,后续会给出打包命令。

直接上代码:

import os
import sys
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
                               QLineEdit, QPushButton, QLabel, QFileDialog, QTextEdit)
from PySide6.QtGui import QFont, QCursor
from PySide6.QtCore import Qt, QThread, Signal

# ===== 核心美化 QSS 样式表 (暗黑科技风) =====
STYLE_SHEET = """
QWidget {
    background-color: #0a0e17;  
    color: #e2e8f0;             
    font-family: "Microsoft YaHei", "Segoe UI";
}
QLabel {
    font-size: 14px;
    font-weight: bold;
    color: #94a3b8;
}
QLineEdit {
    background-color: #0f141e;
    border: 1px solid #1e293b;
    padding: 8px 12px;
    border-radius: 5px;
    color: #00e5ff;             
    font-family: "Consolas";    
    font-size: 13px;
}
QLineEdit:focus {
    border: 1px solid #00e5ff;  
}
QPushButton {
    background-color: transparent;
    border: 1px solid #00e5ff;
    color: #00e5ff;
    padding: 8px 18px;
    border-radius: 5px;
    font-weight: bold;
}
QPushButton:hover {
    background-color: rgba(0, 229, 255, 0.1);
}
QPushButton:disabled {
    border: 1px solid #334155;
    color: #334155;
}
/* 特殊定制“开始运行”按钮 */
QPushButton#RunBtn {
    background-color: #005fb8;
    color: #ffffff;
    border: none;
    font-size: 16px;
    padding: 12px;
    border-radius: 6px;
    margin-top: 10px;
    margin-bottom: 10px;
}
QPushButton#RunBtn:hover {
    background-color: #0078d4;
}
QPushButton#RunBtn:disabled {
    background-color: #1e293b;
    color: #94a3b8;
}
QTextEdit {
    background-color: #050505;  
    border: 1px solid #1e293b;
    border-radius: 6px;
    padding: 12px;
    color: #a3b8cc;
    font-family: "Consolas";
    font-size: 14px;
    line-height: 1.5;
}
"""


# ==========================================
# 后台扫描线程:专门负责干脏活累活,不卡界面
# ==========================================
class ScanThread(QThread):
    # 定义信号,用于把后台的数据传给前台 UI
    log_signal = Signal(str)
    error_signal = Signal(str)
    result_signal = Signal(list, list, int, int)  # 缺失列表, 冗余列表, a总数, b总数

    def __init__(self, path_a, path_b):
        super().__init__()
        self.path_a = path_a
        self.path_b = path_b

    def scan_folder(self, folder):
        files_found = set()
        for root, dirs, files in os.walk(folder):
            for f in files:
                full_path = os.path.join(root, f)
                rel_path = os.path.relpath(full_path, folder)
                files_found.add(rel_path)
        return files_found

    def run(self):
        # 验证路径
        if not os.path.isdir(self.path_a) or not os.path.isdir(self.path_b):
            self.error_signal.emit("<br><b style='color:#ff4d4d;'>[ERROR] 请确保 A 和 B 路径都是有效的文件夹!</b>")
            return

        self.log_signal.emit(f"<span style='color:#fbc02d;'>[START]</span> 正在后台极速拉取目录树结构,请稍候...")
        self.log_signal.emit(f"模板 => <span style='color:#00e5ff;'>{self.path_a}</span>")
        self.log_signal.emit(f"目标 => <span style='color:#00e5ff;'>{self.path_b}</span>")
        self.log_signal.emit("<span style='color:#334155;'>--------------------------------------------------</span>")

        try:
            # 耗时的硬盘遍历在这里执行
            set_a = self.scan_folder(self.path_a)
            set_b = self.scan_folder(self.path_b)

            missing = sorted(list(set_a - set_b))
            extra = sorted(list(set_b - set_a))

            # 计算完毕,将结果发送给前台
            self.result_signal.emit(missing, extra, len(set_a), len(set_b))
        except Exception as e:
            self.error_signal.emit(f"<br><b style='color:#ff4d4d;'>[ERROR] 扫描过程中发生异常: {str(e)}</b>")


# ==========================================
# 前台界面逻辑
# ==========================================
class NXPFileChecker(QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle('NXP 项目双向校验终端 (多线程防卡版)')
        self.resize(850, 600)
        self.setStyleSheet(STYLE_SHEET)

        layout = QVBoxLayout()
        layout.setContentsMargins(25, 25, 25, 25)
        layout.setSpacing(15)

        self.path_a_edit = self.create_row("A 路径 (标准模板):", layout)


        self.path_b_edit = self.create_row("B 路径 (待校验项):", layout)


        self.run_btn = QPushButton("启动系统比对")
        self.run_btn.setObjectName("RunBtn")
        self.run_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        self.run_btn.clicked.connect(self.start_scan)
        layout.addWidget(self.run_btn)

        self.result_log = QTextEdit()
        self.result_log.setReadOnly(True)
        self.result_log.append("<span style='color:#fbc02d;'>[SYSTEM]</span> 多线程引擎初始化完成,等待指令...")
        layout.addWidget(self.result_log)

        self.setLayout(layout)

    def create_row(self, label_text, parent_layout):
        row = QHBoxLayout()
        lbl = QLabel(label_text)
        lbl.setFixedWidth(135)
        edit = QLineEdit()
        btn = QPushButton("浏览目录")
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        btn.clicked.connect(lambda: self.get_path(edit))
        row.addWidget(lbl)
        row.addWidget(edit)
        row.addWidget(btn)
        parent_layout.addLayout(row)
        return edit

    def get_path(self, edit):
        path = QFileDialog.getExistingDirectory(self, "选择目录", edit.text())
        if path:
            edit.setText(os.path.normpath(path))

    def start_scan(self):
        """点击按钮后,锁定界面并启动后台线程"""
        path_a = self.path_a_edit.text().strip()
        path_b = self.path_b_edit.text().strip()

        self.result_log.clear()

        # 防止重复点击,按钮变灰
        self.run_btn.setEnabled(False)
        self.run_btn.setText("扫描计算中,请稍候...")
        self.setCursor(Qt.CursorShape.WaitCursor)  # 鼠标变沙漏

        # 创建并配置线程
        self.thread = ScanThread(path_a, path_b)
        self.thread.log_signal.connect(self.append_log)
        self.thread.error_signal.connect(self.append_log)
        self.thread.result_signal.connect(self.render_results)
        self.thread.finished.connect(self.scan_finished)

        # 启动后台线程
        self.thread.start()

    def append_log(self, msg):
        self.result_log.append(msg)

    def render_results(self, missing, extra, len_a, len_b):
        """接收后台传来的数据,并使用批量渲染防止 UI 卡死"""
        html_blocks = []  # 使用列表拼接,比一行行 append 快百倍

        if not missing and not extra:
            html_blocks.append("<br><b style='color:#00ff00; font-size:16px;'>★ 校验完美一致!文件完全匹配。</b>")
        else:
            if missing:
                html_blocks.append(f"<br><b style='color:#ff4d4d;'>【缺失警报】目标路径缺失 {len(missing)} 个文件:</b>")
                html_blocks.extend([f"<span style='color:#ff4d4d;'>  [缺] {m}</span>" for m in missing])

            if extra:
                html_blocks.append(f"<br><b style='color:#00e5ff;'>【冗余警报】目标路径多出 {len(extra)} 个未知文件:</b>")
                html_blocks.extend([f"<span style='color:#00e5ff;'>  [多] {e}</span>" for e in extra])

        html_blocks.append(f"<br><span style='color:#94a3b8;'>对比完成。扫描基准模板 {len_a} 项,目标 {len_b} 项。</span>")

        # 一次性把所有的 HTML 塞进显示框
        self.result_log.append("<br>".join(html_blocks))

    def scan_finished(self):
        """线程结束时的清理工作"""
        self.run_btn.setEnabled(True)
        self.run_btn.setText("启动系统比对")
        self.unsetCursor()  # 恢复鼠标指针


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = NXPFileChecker()
    window.show()
    sys.exit(app.exec())

更多推荐