Hi,请  登录  或  注册

文件对比校验复制工具(Python 小工具)

24有时候在做源码项目二开时,会遇到这种情况:
新项目里陆陆续续加了一堆文件,需要同步到旧项目目录,但已经记不清究竟多了哪些文件,一个一个翻既浪费时间又容易漏。

这时候,用这个 文件对比校验复制工具 就比较省事了:先在新项目里扫一遍生成路径清单,再拿旧项目做存在性校验,最后一键把缺失的文件复制过去。

文件对比校验复制工具(Python 小工具)

文件对比校验复制工具 第1张


工具功能说明

这个工具是用 Python 写的,做成了一个简单的桌面小程序,核心有三块功能:

  1. 生成文件路径清单
    • 选中一个“新项目”文件夹,工具会递归扫描其中的所有文件。
    • 自动生成一个 file_list.txt 路径清单,保存在工具同目录。
  2. 存在性校验
    • 先选择刚才生成的路径清单,再选择“旧项目”的根目录。
    • 工具会以“锚点目录名”为基准(不区分大小写),从清单里提取相对路径,在旧项目里挨个查找。
    • 最终会生成两个文件:
      • exist_files.txt:旧项目里已存在的文件路径
      • missing_files.txt:旧项目里缺失的文件路径

文件对比校验复制工具 第2张

  1. 一键复制缺失文件
    • 在第三个标签页中,选择路径清单、源根目录(新项目)、目标目录(旧项目)。
    • 工具会按同样的“锚点 + 相对路径”的方式,把能找到的文件从源复制到目标,并保持原有目录结构。
    • 同样会生成 copied_files.txt / not_copied_files.txt 两个日志,方便回溯。

整个设计思路就是:只认锚点目录名,后半段路径全部按原样匹配复制,且不区分大小写,这样在 request / Request 这类目录名不一致的项目中,也能稳定工作。


使用步骤

  1. 下载/打包工具
    • 你可以直接下载附件中的 exe 版本使用;
    • 也可以把下面的 Python 源码自己用 pyinstaller 之类重新打包成 exe。
  2. 在“新项目”生成路径清单
    • 打开工具 → 切到“路径清单”标签;
    • 选择新项目根目录,工具会自动扫描并生成 file_list.txt
  3. 在“旧项目”做存在性校验
    • 切到“存在性校验”;
    • 先选择刚才生成的 file_list.txt
    • 再选择旧项目中的锚点根目录(例如旧项目里的 request 目录);
    • 点击“执行文件存在性校验”,稍等即可得到存在/缺失两个清单文件。
  4. 一键复制缺失文件(可选)
    • 切到“复制文件”;
    • 同样选择路径清单、源根目录(新项目)、目标目录(旧项目);
    • 执行复制操作,缺失的文件会按原目录结构复制过去。

Python 源码(完整)

下面是工具的完整源码,可以直接保存为 main.py,然后自行打包或运行。代码未做任何修改,你可以直接对比。

import os
import sys
import shutil
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import ttk

class ProgressDialog:
    """简洁进度弹窗"""

    def __init__(self, parent, title: str, total: int):
        self.total = max(1, int(total))
        self.top = tk.Toplevel(parent)
        self.top.title(title)
        self.top.transient(parent)
        self.top.resizable(False, False)
        self.top.configure(bg="#1a1a1a")
        self.top.attributes('-topmost', True)

        self.top.update_idletasks()
        pw = 360
        ph = 120
        px = parent.winfo_rootx() + (parent.winfo_width() - pw) // 2
        py = parent.winfo_rooty() + (parent.winfo_height() - ph) // 2
        self.top.geometry(f"{pw}x{ph}+{max(0, px)}+{max(0, py)}")

        wrap = tk.Frame(self.top, bg="#1f2937", highlightthickness=1, highlightbackground="#334155")
        wrap.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        title_lbl = tk.Label(wrap, text=title, font=("Microsoft YaHei UI", 11, "bold"), bg="#1f2937", fg="#e5e7eb")
        title_lbl.pack(anchor="w")

        self.var_text = tk.StringVar(value="准备中…")
        text_lbl = tk.Label(wrap, textvariable=self.var_text, font=("Microsoft YaHei UI", 9), bg="#1f2937",
                            fg="#9ca3af")
        text_lbl.pack(anchor="w", pady=(6, 6))

        self.pb = ttk.Progressbar(wrap, orient=tk.HORIZONTAL, mode='determinate', length=300, maximum=self.total)
        self.pb.pack(fill=tk.X)

        self.var_percent = tk.StringVar(value="0%")
        percent_lbl = tk.Label(wrap, textvariable=self.var_percent, font=("Consolas", 10), bg="#1f2937", fg="#e5e7eb")
        percent_lbl.pack(anchor="e", pady=(6, 0))

        self.top.grab_set()
        self.top.protocol("WM_DELETE_WINDOW", lambda: None)

    def step(self, i: int, note=None):
        i = max(0, min(self.total, int(i)))
        if note:
            self.var_text.set(note)
        self.pb['value'] = i
        pct = int(i * 100 / self.total)
        self.var_percent.set(f"{pct}%")
        self.top.update_idletasks()

    def close(self):
        try:
            self.top.grab_release()
        except Exception:
            pass
        self.top.destroy()

class FileToolApp:
    """
    文件路径工具 - 锚点模式(不区分大小写)
    功能2 & 3 均使用「锚点目录名」机制,并忽略大小写匹配
    """

    def __init__(self, root):
        self.root = root
        self.root.title("文件路径工具—老吴搭建教程")
        self.root.geometry("1000x760")
        self.root.minsize(900, 640)

        # 主题
        self.theme = "dark"
        self._init_theme_tokens()
        self._apply_theme()

        # 应用目录
        self.app_dir = self._resolve_app_dir()
        try:
            self.app_dir.mkdir(parents=True, exist_ok=True)
        except Exception:
            pass

        # 状态变量
        self.list_file_for_check = None
        self.root_dir_for_check = None
        self.list_file_for_copy = None
        self.source_root_for_copy = None
        self.target_dir_for_copy = None

        # UI 变量
        self.var_list_check = tk.StringVar(value="未选择")
        self.var_root_check = tk.StringVar(value="未选择")
        self.var_list_copy = tk.StringVar(value="未选择")
        self.var_source_copy = tk.StringVar(value="未选择")
        self.var_target_copy = tk.StringVar(value="未选择")

        self._build_ui()

    def _init_theme_tokens(self):
        self.TOKENS = {
            "dark": {
                "bg": "#0f1419",
                "elev": "#151b24",
                "card": "#1b2332",
                "primary": "#3b82f6",
                "primary_hover": "#2563eb",
                "text": "#e5e7eb",
                "muted": "#9aa4b2",
                "border": "#263041",
                "badge": "#222b3b",
            },
            "light": {
                "bg": "#f6f7fb",
                "elev": "#ffffff",
                "card": "#ffffff",
                "primary": "#2563eb",
                "primary_hover": "#1d4ed8",
                "text": "#111827",
                "muted": "#6b7280",
                "border": "#e5e7eb",
                "badge": "#eef2ff",
            },
        }

    def _resolve_app_dir(self):
        try:
            if getattr(sys, 'frozen', False):
                return Path(sys.executable).resolve().parent
        except Exception:
            pass
        return Path(__file__).resolve().parent

    def _apply_theme(self):
        t = self.TOKENS[self.theme]
        self.bg = t["bg"]
        self.elev = t["elev"]
        self.card = t["card"]
        self.primary = t["primary"]
        self.primary_hover = t["primary_hover"]
        self.text = t["text"]
        self.muted = t["muted"]
        self.border = t["border"]
        self.badge_bg = t["badge"]

        style = ttk.Style()
        try:
            style.theme_use('clam')
        except Exception:
            pass

        style.configure('TFrame', background=self.bg)
        style.configure('TLabel', background=self.bg, foreground=self.text)
        style.configure('Thin.TSeparator', background=self.border)
        style.configure('TNotebook', background=self.bg, borderwidth=0)
        style.configure('TNotebook.Tab', padding=(18, 10), font=('Microsoft YaHei UI', 10), background=self.elev,
                        foreground=self.muted)
        style.map('TNotebook.Tab',
                  background=[('selected', self.card), ('active', self.card)],
                  foreground=[('selected', self.text), ('active', self.text)])

        style.configure('Primary.TButton',
                        background=self.primary,
                        foreground='#ffffff',
                        borderwidth=0,
                        focusthickness=0,
                        padding=(18, 12),
                        font=('Microsoft YaHei UI', 10, 'bold'))
        style.map('Primary.TButton',
                  background=[('active', self.primary_hover), ('pressed', self.primary_hover)],
                  foreground=[('disabled', '#cccccc')])
        style.configure('PrimaryHover.TButton',
                        background=self.primary_hover,
                        foreground='#ffffff',
                        borderwidth=0,
                        focusthickness=0,
                        padding=(18, 12),
                        font=('Microsoft YaHei UI', 10, 'bold'))

        style.configure('Badge.TLabel', background=self.badge_bg, foreground=self.text, padding=(8, 3))

    def _toggle_theme(self):
        self.theme = 'light' if self.theme == 'dark' else 'dark'
        self._apply_theme()
        for w in self.root.winfo_children():
            w.destroy()
        self._build_ui()

    def _build_ui(self):
        self.root.configure(bg=self.bg)
        self._build_header()

        notebook_frame = tk.Frame(self.root, bg=self.bg)
        notebook_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=(8, 12))

        nb = ttk.Notebook(notebook_frame)
        nb.pack(fill=tk.BOTH, expand=True)

        tab1 = tk.Frame(nb, bg=self.bg)
        tab2 = tk.Frame(nb, bg=self.bg)
        tab3 = tk.Frame(nb, bg=self.bg)
        nb.add(tab1, text="? 路径清单")
        nb.add(tab2, text="✓ 存在性校验")
        nb.add(tab3, text="? 复制文件")

        self._tab_generate_list(tab1)
        self._tab_check_exist(tab2)
        self._tab_copy_files(tab3)

        self._build_footer()

    def _build_header(self):
        header_wrap = tk.Frame(self.root, bg=self.bg)
        header_wrap.pack(fill=tk.X, padx=18, pady=(18, 8))

        header = tk.Frame(header_wrap, bg=self.elev, highlightthickness=1, highlightbackground=self.border)
        header.pack(fill=tk.X)

        top_bar = tk.Frame(header, bg=self.primary, height=3)
        top_bar.pack(fill=tk.X)

        content = tk.Frame(header, bg=self.elev)
        content.pack(fill=tk.BOTH, expand=True, padx=16, pady=12)

        left = tk.Frame(content, bg=self.elev)
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        title = tk.Label(left, text="文件路径工具", font=("Microsoft YaHei UI", 20, "bold"), bg=self.elev,
                         fg=self.primary)
        title.pack(anchor="w")

        subtitle = tk.Label(left, text="基于锚点目录名校验与复制 · 不区分大小写", font=("Microsoft YaHei UI", 10), bg=self.elev,
                            fg=self.muted)
        subtitle.pack(anchor="w", pady=(6, 0))

        right = tk.Frame(content, bg=self.elev)
        right.pack(side=tk.RIGHT)

        theme_btn = ttk.Button(
            right,
            text=("切换到亮色" if self.theme == 'dark' else "切换到暗色"),
            command=self._toggle_theme,
            style='Primary.TButton',
        )
        theme_btn.pack()

    def _build_footer(self):
        footer_wrap = tk.Frame(self.root, bg=self.bg)
        footer_wrap.pack(fill=tk.X, padx=18, pady=(0, 18))

        footer = tk.Frame(footer_wrap, bg=self.elev, highlightthickness=1, highlightbackground=self.border)
        footer.pack(fill=tk.X)

        content = tk.Frame(footer, bg=self.elev)
        content.pack(fill=tk.BOTH, expand=True, padx=14, pady=10)

        tip = tk.Label(content, text="提示:以所选目录名为锚点(不区分大小写),提取路径后半段进行匹配。", font=("Microsoft YaHei UI", 9), bg=self.elev,
                       fg=self.muted)
        tip.pack(side=tk.LEFT)

        brand = tk.Label(content, text="文件校验 匹配 复制", font=("Microsoft YaHei UI", 9), bg=self.elev, fg=self.muted)
        brand.pack(side=tk.RIGHT)

    def _card(self, parent, title_text: str, desc_text: str):
        card = tk.Frame(parent, bg=self.elev, highlightthickness=1, highlightbackground=self.border)
        card.pack(fill=tk.X, pady=12)

        top = tk.Frame(card, bg=self.primary, height=3)
        top.pack(fill=tk.X)

        body = tk.Frame(card, bg=self.elev)
        body.pack(fill=tk.BOTH, expand=True, padx=16, pady=14)

        title = tk.Label(body, text=title_text, font=("Microsoft YaHei UI", 13, "bold"), bg=self.elev, fg=self.primary)
        title.pack(anchor="w")

        if desc_text:
            desc = tk.Label(body, text=desc_text, font=("Microsoft YaHei UI", 9), bg=self.elev, fg=self.muted)
            desc.pack(anchor="w", pady=(6, 6))

        sep = tk.Frame(body, bg=self.border, height=1)
        sep.pack(fill=tk.X, pady=8)

        def on_enter(_): card.configure(highlightbackground=self.primary)
        def on_leave(_): card.configure(highlightbackground=self.border)

        card.bind("<Enter>", on_enter)
        card.bind("<Leave>", on_leave)

        return body

    def _primary_btn(self, parent, text, command):
        btn = ttk.Button(parent, text=text, command=command, style='Primary.TButton')
        btn.pack(fill=tk.X)
        btn.bind("<Enter>", lambda e: btn.configure(style='PrimaryHover.TButton'))
        btn.bind("<Leave>", lambda e: btn.configure(style='Primary.TButton'))
        return btn

    def _line_item(self, parent, label_text: str, var: tk.StringVar):
        row = tk.Frame(parent, bg=self.elev)
        row.pack(fill=tk.X, pady=6)

        label = tk.Label(row, text=label_text, font=("Microsoft YaHei UI", 10), bg=self.elev, fg=self.text)
        label.pack(side=tk.LEFT)

        value = ttk.Label(row, textvariable=var, style='Badge.TLabel')
        value.pack(side=tk.RIGHT)

    def _tab_generate_list(self, tab):
        container = tk.Frame(tab, bg=self.bg)
        container.pack(fill=tk.BOTH, expand=True, padx=6, pady=8)
        card = self._card(container, "生成文件夹路径清单", "扫描文件夹中的所有文件并生成完整路径列表")
        self._primary_btn(card, "选择文件夹并生成路径清单", self.generate_path_list)

    def _tab_check_exist(self, tab):
        container = tk.Frame(tab, bg=self.bg)
        container.pack(fill=tk.BOTH, expand=True, padx=6, pady=8)
        card = self._card(container, "校验文件是否存在", "以所选目录名为锚点(不区分大小写),从清单路径中提取子路径并在本地查找")
        self._primary_btn(card, "选择路径清单文件", self.select_list_file_for_check)
        self._primary_btn(card, "选择本地根目录(如 request / Request)", self.select_root_dir_for_check)
        self._primary_btn(card, "执行文件存在性校验", self.check_files_existence)
        self._line_item(card, "路径清单:", self.var_list_check)
        self._line_item(card, "本地根目录:", self.var_root_check)

    def _tab_copy_files(self, tab):
        container = tk.Frame(tab, bg=self.bg)
        container.pack(fill=tk.BOTH, expand=True, padx=6, pady=8)
        card = self._card(container, "复制匹配的文件", "基于锚点(不区分大小写)提取子路径,从源目录复制到目标目录")
        self._primary_btn(card, "选择路径清单文件", self.select_list_file_for_copy)
        self._primary_btn(card, "选择源根目录(如 request / Request)", self.select_source_root_for_copy)
        self._primary_btn(card, "选择目标文件夹", self.select_target_dir_for_copy)
        self._primary_btn(card, "执行文件复制", self.copy_matched_files)
        self._line_item(card, "路径清单:", self.var_list_copy)
        self._line_item(card, "源根目录:", self.var_source_copy)
        self._line_item(card, "目标文件夹:", self.var_target_copy)

    # ===== 功能实现 =====
    def generate_path_list(self):
        folder = filedialog.askdirectory(title="选择要扫描的文件夹")
        if not folder:
            return
        folder_path = Path(folder)
        output_file = self.app_dir / "file_list.txt"
        paths = []
        for file in folder_path.rglob("*"):
            if file.is_file():
                paths.append(str(file.resolve()))
        with open(output_file, "w", encoding="utf-8") as f:
            f.write("\n".join(paths))
        messagebox.showinfo("完成", f"已生成路径清单到:\n{output_file}\n\n共找到 {len(paths)} 个文件")

    def select_list_file_for_check(self):
        file = filedialog.askopenfilename(title="选择路径清单文本文件",
                                          filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
        if file:
            self.list_file_for_check = Path(file)
            self.var_list_check.set(self.list_file_for_check.name)
            messagebox.showinfo("已选择", f"路径清单:\n{self.list_file_for_check}")

    def select_root_dir_for_check(self):
        folder = filedialog.askdirectory(title="选择本地根目录(如 request / Request)")
        if folder:
            self.root_dir_for_check = Path(folder)
            self.var_root_check.set(self.root_dir_for_check.name)
            messagebox.showinfo("已选择", f"本地根目录:\n{self.root_dir_for_check}")

    def check_files_existence(self):
        if not self.list_file_for_check or not self.root_dir_for_check:
            messagebox.showerror("错误", "请先选择路径清单和本地根目录!")
            return

        anchor_name = self.root_dir_for_check.name
        anchor_lower = anchor_name.lower()  # ← 关键:统一转小写用于匹配

        exist_list = []
        missing_list = []

        with open(self.list_file_for_check, "r", encoding="utf-8") as f:
            lines = [line.strip() for line in f if line.strip()]

        pd = ProgressDialog(self.root, "正在校验文件…", len(lines))
        try:
            for i, full_path in enumerate(lines, start=1):
                try:
                    normalized = full_path.replace("\\", "/")
                    parts = normalized.split("/")

                    # 从后往前找锚点(忽略大小写)
                    idx = -1
                    for j in range(len(parts) - 1, -1, -1):
                        if parts[j].lower() == anchor_lower:
                            idx = j
                            break

                    if idx == -1:
                        missing_list.append(full_path)
                        continue

                    rel_parts = parts[idx + 1:]
                    if not rel_parts:
                        missing_list.append(full_path)
                        continue

                    local_file = self.root_dir_for_check / Path(*rel_parts)
                    if local_file.exists():
                        exist_list.append(full_path)
                    else:
                        missing_list.append(full_path)

                except Exception:
                    missing_list.append(full_path)

                pd.step(i, note=f"处理 {i}/{len(lines)}…")
        finally:
            pd.close()

        exist_file = self.app_dir / "exist_files.txt"
        missing_file = self.app_dir / "missing_files.txt"

        with open(exist_file, "w", encoding="utf-8") as f:
            f.write("\n".join(exist_list))
        with open(missing_file, "w", encoding="utf-8") as f:
            f.write("\n".join(missing_list))

        messagebox.showinfo(
            "完成",
            f"校验完成!\n\n存在: {len(exist_list)} 个\n缺失: {len(missing_list)} 个\n\n结果已保存到程序目录。"
        )

    def select_list_file_for_copy(self):
        file = filedialog.askopenfilename(title="选择路径清单文本文件",
                                          filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
        if file:
            self.list_file_for_copy = Path(file)
            self.var_list_copy.set(self.list_file_for_copy.name)
            messagebox.showinfo("已选择", f"路径清单:\n{self.list_file_for_copy}")

    def select_source_root_for_copy(self):
        folder = filedialog.askdirectory(title="选择源根目录(如 request / Request)")
        if folder:
            self.source_root_for_copy = Path(folder)
            self.var_source_copy.set(self.source_root_for_copy.name)
            messagebox.showinfo("已选择", f"源根目录:\n{self.source_root_for_copy}")

    def select_target_dir_for_copy(self):
        folder = filedialog.askdirectory(title="选择目标文件夹(复制到此处)")
        if folder:
            self.target_dir_for_copy = Path(folder)
            self.var_target_copy.set(self.target_dir_for_copy.name)
            messagebox.showinfo("已选择", f"目标文件夹:\n{self.target_dir_for_copy}")

    def copy_matched_files(self):
        if not all([self.list_file_for_copy, self.source_root_for_copy, self.target_dir_for_copy]):
            messagebox.showerror("错误", "请先选择路径清单、源根目录和目标文件夹!")
            return

        anchor_name = self.source_root_for_copy.name
        anchor_lower = anchor_name.lower()

        copied_list = []
        not_copied_list = []

        with open(self.list_file_for_copy, "r", encoding="utf-8") as f:
            lines = [line.strip() for line in f if line.strip()]

        pd = ProgressDialog(self.root, "正在复制匹配的文件…", len(lines))
        try:
            for i, full_path in enumerate(lines, start=1):
                try:
                    normalized = full_path.replace("\\", "/")
                    parts = normalized.split("/")

                    # 从后往前找锚点(忽略大小写)
                    idx = -1
                    for j in range(len(parts) - 1, -1, -1):
                        if parts[j].lower() == anchor_lower:
                            idx = j
                            break

                    if idx == -1:
                        not_copied_list.append(full_path)
                        continue

                    rel_parts = parts[idx + 1:]
                    if not rel_parts:
                        not_copied_list.append(full_path)
                        continue

                    rel_path = Path(*rel_parts)
                    src_file = self.source_root_for_copy / rel_path
                    dst_file = self.target_dir_for_copy / rel_path  # 保留相对结构

                    if src_file.exists() and src_file.is_file():
                        dst_file.parent.mkdir(parents=True, exist_ok=True)
                        shutil.copy2(src_file, dst_file)
                        copied_list.append(full_path)
                    else:
                        not_copied_list.append(full_path)

                except Exception:
                    not_copied_list.append(full_path)

                pd.step(i, note=f"处理 {i}/{len(lines)}…")
        finally:
            pd.close()

        # 保存日志
        copied_log = self.app_dir / "copied_files.txt"
        not_copied_log = self.app_dir / "not_copied_files.txt"

        with open(copied_log, "w", encoding="utf-8") as f:
            f.write("\n".join(copied_list))
        with open(not_copied_log, "w", encoding="utf-8") as f:
            f.write("\n".join(not_copied_list))

        messagebox.showinfo(
            "完成",
            f"复制完成!\n\n成功: {len(copied_list)} 个\n未找到或失败: {len(not_copied_list)} 个\n\n"
            f"结果已保存到程序目录:\n- copied_files.txt\n- not_copied_files.txt"
        )

if __name__ == "__main__":
    root = tk.Tk()
    try:
        root.tk.call('tk', 'scaling', 1.15)
    except Exception:
        pass
    app = FileToolApp(root)
    root.mainloop()

其它类似工具

  • 文件对比软件 UltraCompare for mac v23.0.0.30 中文修复版
  • 文件对比工具 ExamDiff Pro Master Edition v10.0.1.12 免激活码
  • 文件对比软件 Beyond Compare v4.4.6 免激活

工具参数

  • 工具名称:文件对比校验复制工具
  • 实现语言:Python + Tkinter
  • 工具格式:exe(附 Python 源码,可自行打包)
  • 工具大小:9.6M

下载地址:


隐藏内容,解锁需要先评论本文
评论后刷新解锁

文章名称:文件对比校验复制工具(Python 小工具)
除非特别注明,本站所有文章均为原创,转载请注明出处:264玫瑰资源库
部分教程资源来源于互联网,请谨慎辨别广告内容,避免上当受骗!

评论 抢沙发

登录

找回密码

注册