偷懒工具5:Python+PySide6 开发,文件双向校验工具(多线程防卡版)
前言:考完文件,是不是经常遇到这样的痛点——标准工程模板与本地开发工程对比,手动找缺失、冗余文件费时又费力,稍不注意就漏查,影响项目提交效率?作为一名“高效摸鱼”的工程师,我利用摸鱼时间,用Python+PySide6开发了一款可视化文件双向校验工具,完美解决这个问题,今天分享给大家,直接复制就能用!
这款工具主打「多线程防卡顿」「暗黑科技风UI」「双向校验」「结果导出」,无需复杂操作,选择两个文件夹就能快速对比,自动识别缺失和冗余文件,适合所有需要文件一致性校验的场景(其他工程、文档对比都能用)。
一、工具核心亮点
先给大家亮个底,这款工具不是简单的代码堆砌,而是按照生产级标准开发,兼顾实用性和美观度,核心亮点如下:
-
多线程架构:将耗时的文件扫描操作丢到后台线程,前台UI不卡顿,操作流畅,哪怕对比上千个文件也不会出现“假死”现象;
-
暗黑科技风UI:精心设计的QSS样式,深蓝底色+荧光蓝高亮,字体清晰、布局合理,长时间使用不刺眼,贴合工程师审美;
-
双向精准校验:以“标准模板文件夹”为基准,对比“待校验文件夹”,同时识别「缺失文件」(模板有、目标无)和「冗余文件」(模板无、目标有),相对路径对比,不遗漏子文件夹文件;
-
鲁棒性强:完善的错误处理(路径无效、扫描异常),防止重复点击,鼠标状态切换提示,新手也能轻松上手;
-
可复用性高:代码结构清晰,可直接修改适配自己的项目场景(如添加MD5内容校验、忽略指定文件等)。
二、开发环境准备
工具基于Python+PySide6开发,环境搭建简单,无需复杂配置,步骤如下:
-
Python版本:3.8+(推荐3.9/3.10,兼容性更好);
-
核心依赖:PySide6(负责可视化界面),安装命令:
pip install PySide6; -
开发工具:任意Python编辑器(PyCharm、VS Code均可);
-
打包工具(可选):如需生成可执行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())
更多推荐



所有评论(0)