52. shutil实现文件_文件夹的复制与删除

Python shutil模块文件操作全解析

一、shutil模块概述

模块功能定位

概念定义

模块功能定位是指通过分析模块在系统中的角色和职责,明确其核心功能和边界范围。在Python中,模块通常是一个.py文件,包含相关的函数、类和变量,用于实现特定的功能集合。

使用场景
  1. 大型项目开发时,需要划分清晰的模块职责
  2. 团队协作开发,避免功能重复或遗漏
  3. 代码重构时,评估模块的合理性和必要性
  4. 系统架构设计阶段,规划模块间的依赖关系
常见误区或注意事项
  1. 避免创建"上帝模块"(包含过多不相关功能)
  2. 模块间耦合度过高会导致维护困难
  3. 功能划分过细会增加模块间的调用复杂度
  4. 忽略模块的可测试性和可扩展性
示例代码
# 正确定位示例:用户认证模块
# user_auth.py
def login(username, password):
    """处理用户登录逻辑"""
    # 实现代码...

def logout(user_id):
    """处理用户登出逻辑"""
    # 实现代码...

def reset_password(email):
    """处理密码重置逻辑"""
    # 实现代码...

# 错误定位示例:混合功能的模块
# utils.py (过于宽泛的定位)
def send_email():
    """发送邮件"""
    # 实现代码...

def format_date():
    """格式化日期"""
    # 实现代码...

def encrypt_data():
    """数据加密"""
    # 实现代码...

os模块与pathlib的互补关系

概念定义

os模块是Python标准库中用于操作系统相关功能的传统模块,而pathlib是Python 3.4引入的面向对象的文件系统路径操作模块。两者在文件系统操作方面存在功能重叠,但各有侧重。

使用场景
  1. os模块更适合:

    • 需要与大量现有代码兼容时
    • 执行底层系统调用时(如os.fork()
    • 处理环境变量(os.environ
  2. pathlib更适合:

    • 面向对象的路径操作
    • 链式方法调用
    • 新项目开发
常见误区
  1. 认为两者必须二选一(实际上可以混合使用)
  2. 在性能敏感场景过度使用pathlib(某些操作比os慢)
  3. 忽略pathlib的跨平台优势
示例代码
from pathlib import Path
import os

# 混合使用示例
path = Path("docs") / "readme.txt"  # pathlib创建路径
os.chmod(path, 0o755)  # os修改权限

# 路径转换
os_path = str(path)  # Path转字符串给os使用
path_obj = Path(os.getcwd())  # os结果转Path

主要操作类型分类

在Python中,操作类型可以根据其功能和行为进行分类。以下是常见的操作类型分类:

1. 算术操作

用于执行基本的数学运算。

a = 10
b = 3
print(a + b)  # 加法
print(a - b)  # 减法
print(a * b)  # 乘法
print(a / b)  # 除法
print(a % b)  # 取模
print(a ** b) # 幂运算
print(a // b) # 整除
2. 比较操作

用于比较两个值的大小或相等性,返回布尔值(TrueFalse)。

x = 5
y = 10
print(x == y)  # 等于
print(x != y)  # 不等于
print(x > y)   # 大于
print(x < y)   # 小于
print(x >= y)  # 大于等于
print(x <= y)  # 小于等于
3. 逻辑操作

用于组合多个布尔表达式。

a = True
b = False
print(a and b)  # 逻辑与
print(a or b)   # 逻辑或
print(not a)    # 逻辑非
4. 赋值操作

用于给变量赋值或更新变量的值。

x = 5    # 简单赋值
x += 3   # 加法赋值(等价于 x = x + 3)
x -= 2   # 减法赋值
x *= 4   # 乘法赋值
x /= 2   # 除法赋值
5. 成员操作

用于检查某个值是否存在于序列(如列表、字符串等)中。

numbers = [1, 2, 3, 4, 5]
print(3 in numbers)      # 检查3是否在列表中
print(6 not in numbers)  # 检查6是否不在列表中
6. 身份操作

用于比较两个对象的存储地址是否相同(即是否为同一个对象)。

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)     # True,a和b引用同一个对象
print(a is c)     # False,a和c是不同的对象
print(a == c)     # True,值相同
注意事项
  1. 逻辑操作中,andor具有短路特性,即如果第一个操作数已经能确定结果,则不会计算第二个操作数。
  2. 身份操作(is)与比较操作(==)不同,前者比较对象地址,后者比较值。
  3. 赋值操作(如+=)对于可变对象(如列表)和不可变对象(如整数)的行为可能不同。

二、文件复制操作

copy() 基础文件复制

概念定义

copy() 是 Python 中 shutil 模块提供的一个函数,用于将源文件复制到目标位置。它执行的是文件内容的逐字节复制,适用于大多数文件复制场景。

使用场景
  1. 需要将文件从一个目录复制到另一个目录
  2. 创建文件的备份副本
  3. 在文件处理前保留原始文件的副本
常见误区或注意事项
  1. 如果目标文件已存在,copy() 会覆盖它而不发出警告
  2. 源文件必须存在且可读,否则会抛出异常
  3. 不会保留文件的元数据(如权限、时间戳等),如需保留应使用 copy2()
  4. 不能用于复制目录(需使用 copytree()
示例代码
import shutil

# 基本文件复制
shutil.copy('source.txt', 'destination.txt')

# 指定目标目录(保持原文件名)
shutil.copy('source.txt', '/path/to/destination/folder/')

# 处理可能的异常
try:
    shutil.copy('source.txt', 'destination.txt')
except IOError as e:
    print(f"无法复制文件: {
     
     e}")

copy2()

概念定义

copy2()是Python标准库shutil模块中的一个函数,用于复制文件并尽可能保留文件的元数据。它是copy()函数的增强版本,在复制文件内容的同时还会尝试保留文件的权限、最后访问时间、最后修改时间等元数据。

使用场景

当需要复制文件并希望保留原始文件的以下属性时,可以使用copy2()

  • 文件权限
  • 最后访问时间
  • 最后修改时间
  • 其他平台特定的元数据
注意事项
  1. 并非所有平台都能完全保留所有元数据
  2. 对于符号链接,copy2()会复制链接指向的文件内容,而不是链接本身
  3. 如果目标文件已存在,它将被覆盖
  4. 不能用于复制目录(如需复制目录应使用copytree()
示例代码
import shutil

# 复制文件并保留元数据
shutil.copy2('source.txt', 'destination.txt')

# 也可以指定完整路径
shutil.copy2('/path/to/source/file', '/path/to/destination/file')
与copy()的区别

copy()函数只复制文件内容,而copy2()会额外尝试保留文件元数据。如果不需要保留元数据,使用copy()即可。

copyfile() 底层文件内容复制

概念定义

copyfile() 是 Python 中 shutil 模块提供的一个函数,用于高效复制文件内容(不包含元数据)。它直接操作文件描述符,在底层通过读取源文件内容并写入目标文件实现复制。

使用场景
  1. 需要快速复制文件内容(如日志文件、数据文件等)
  2. 不需要保留文件权限、时间戳等元数据
  3. 需要在不同存储位置之间传输文件内容
注意事项
  1. 目标文件必须具有写入权限
  2. 不会自动创建目标路径中的目录
  3. 如果目标文件已存在,会被静默覆盖
  4. 不能用于复制特殊文件(如设备文件)
示例代码
import shutil

# 基本用法
shutil.copyfile('source.txt', 'destination.txt')

# 带路径的复制
shutil.copyfile('/path/to/source.log', '/backup/destination.log')

# 错误处理示例
try:
    shutil.copyfile(
新增一个前后端链条的一个函数,实现的主要功能就是从API上拉取一个base_dir,然后调用下面这个函数 def copy_folder_to_scl_project(source_folder): """ 将指定文件夹复制到当前用户的SCL_project目录下 并在history目录保存带时间戳的历史版本 参数: source_folder (str): 要复制的源文件夹路径 返回: tuple: (最新版本路径, 历史版本路径) 失败返回 (None, None) """ # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger('FolderCopier') try: # 1. 验证源文件夹 if not os.path.isdir(source_folder): logger.error(f"源路径不是有效的文件夹: {source_folder}") return None, None # 2. 获取当前用户名和构建目标路径 username = getpass.getuser() target_base = os.path.join("C:\\Users", username, "SCL_project") folder_name = os.path.basename(os.path.normpath(source_folder)) # 3. 创建目标目录结构 os.makedirs(target_base, exist_ok=True) logger.info(f"基础目录: {target_base}") # 4. 创建历史版本目录 (带时间戳) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") history_folder_name = f"{folder_name}_{timestamp}" history_path = os.path.join(target_base, "history", history_folder_name) os.makedirs(os.path.join(target_base, "history"), exist_ok=True) # 5. 复制到历史版本目录 shutil.copytree(source_folder, history_path, ignore=shutil.ignore_patterns('*.tmp', '~$*', 'Thumbs.db')) logger.info(f"历史版本保存至: {history_path}") # 6. 复制到最新版本目录 latest_path = os.path.join(target_base, folder_name) if os.path.exists(latest_path): shutil.rmtree(latest_path) # 删除现有文件夹 shutil.copytree(source_folder, latest_path, ignore=shutil.ignore_patterns('*.tmp', '~$*', 'Thumbs.db')) logger.info(f"最新版本保存至: {latest_path}") # 7. 统计文件总数 file_count = sum([len(files) for _, _, files in os.walk(latest_path)]) logger.info(f"复制文件总数: {file_count}") return latest_path, history_path except Exception as e: logger.exception(f"复制过程中发生错误: {str(e)}") return None, None
09-29
import os import subprocess import shutil import time import tkinter as tk from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage import threading import queue import traceback import webbrowser import datetime import configparser class DiffProcessorApp: def __init__(self, root): self.root = root root.title("高级文件夹比较工具") root.geometry("1000x700") root.configure(bg="#f5f5f5") # 创建现代风格主题 self.style = ttk.Style() self.style.theme_use('clam') # 自定义主题颜色 self.style.configure('TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#333", background="#4CAF50", bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('disabled', '#9E9E9E')]) self.style.configure('TLabel', font=('Segoe UI', 9), background="#f5f5f5") self.style.configure('TLabelframe', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", relief="flat", borderwidth=2) self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", foreground="#2E7D32") self.style.configure('Treeview', font=('Segoe UI', 9), rowheight=25) self.style.configure('Treeview.Heading', font=('Segoe UI', 9, 'bold')) # 创建主框架 main_frame = ttk.Frame(root, padding="15") main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 标题区域 header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) # 添加标题图标 try: icon = PhotoImage(file="folder_icon.png") self.icon_label = ttk.Label(header_frame, image=icon) self.icon_label.image = icon self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) except: self.icon_label = ttk.Label(header_frame, text="📁", font=("Arial", 24)) self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) title_label = ttk.Label(header_frame, text="高级文件夹比较工具", font=("Segoe UI", 18, "bold"), foreground="#2E7D32") title_label.pack(side=tk.LEFT) # 文件选择区域 file_frame = ttk.LabelFrame(main_frame, text="文件夹选择", padding="12") file_frame.pack(fill=tk.X, pady=5) # 文件夹选择 - 使用独立的配置键 self.old_folder_entry, _ = self.create_folder_selector(file_frame, "原始文件夹:", "old_folder") self.new_folder_entry, _ = self.create_folder_selector(file_frame, "修改后文件夹:", "new_folder") # 比较选项区域 options_frame = ttk.LabelFrame(main_frame, text="比较选项", padding="12") options_frame.pack(fill=tk.X, pady=5) # 递归比较选项 self.recursive_var = tk.BooleanVar(value=True) recursive_check = ttk.Checkbutton(options_frame, text="递归比较子文件夹", variable=self.recursive_var) recursive_check.grid(row=0, column=0, padx=10, pady=5, sticky=tk.W) # 文件过滤 filter_frame = ttk.Frame(options_frame) filter_frame.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="文件过滤:").pack(side=tk.LEFT, padx=(0, 5)) self.filter_var = tk.StringVar(value="*.*") filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=15) filter_entry.pack(side=tk.LEFT) # 输出设置区域 self.excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12") self.excel_frame.pack(fill=tk.X, pady=5) # 目标Excel选择 ttk.Label(self.excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.excel_file_entry = ttk.Entry(self.excel_frame, width=60) self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(self.excel_frame, text="浏览...", command=lambda: self.select_file(self.excel_file_entry, "excel_file", [("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5) # WinMerge路径设置 winmerge_frame = ttk.Frame(self.excel_frame) winmerge_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5) ttk.Label(winmerge_frame, text="WinMerge路径:").grid(row=0, column=0, sticky=tk.W) self.winmerge_entry = ttk.Entry(winmerge_frame, width=60) self.winmerge_entry.grid(row=0, column=1, padx=5) self.winmerge_entry.insert(0, r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe") # 默认路径 ttk.Button(winmerge_frame, text="浏览...", command=lambda: self.select_file(self.winmerge_entry, "winmerge_path", [("WinMerge 可执行文件", "*.exe")])).grid(row=0, column=2) # 执行按钮区域 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=10) self.run_button = ttk.Button(button_frame, text="执行比较", command=self.start_processing, width=20, style='TButton') self.run_button.pack(side=tk.LEFT) # 停止按钮 self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_processing, width=10, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=10) # 查看文件夹报告按钮 self.view_folder_report_button = ttk.Button(button_frame, text="查看文件夹报告", command=lambda: self.view_report("folder"), width=15, state=tk.DISABLED) self.view_folder_report_button.pack(side=tk.LEFT, padx=10) # 进度条 self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=700, mode='determinate') self.progress.pack(fill=tk.X, pady=5) # 状态信息 status_frame = ttk.Frame(main_frame) status_frame.pack(fill=tk.X, pady=5) self.status_var = tk.StringVar(value="准备就绪") status_label = ttk.Label(status_frame, textvariable=self.status_var, font=("Segoe UI", 9), foreground="#2E7D32") status_label.pack(side=tk.LEFT) # 日志和预览区域 notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=5) # 文件夹结构标签 tree_frame = ttk.Frame(notebook, padding="5") notebook.add(tree_frame, text="文件夹结构") # 创建树形视图 self.tree = ttk.Treeview(tree_frame, columns=("Status"), show="tree") self.tree.heading("#0", text="文件夹结构", anchor=tk.W) self.tree.heading("Status", text="状态", anchor=tk.W) self.tree.column("#0", width=400) self.tree.column("Status", width=100) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 日志标签 log_frame = ttk.Frame(notebook, padding="5") notebook.add(log_frame, text="执行日志") self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD, font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) self.log_text.config(state=tk.DISABLED) # 设置网格权重 tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) # 线程控制 self.processing = False self.queue = queue.Queue() self.folder_report_path = None self.files_dir = None # 配置文件路径 self.config_file = "folder_compare_config.ini" # 加载保存的路径 self.load_paths() # 启动队列处理 self.root.after(100, self.process_queue) def create_folder_selector(self, parent, label_text, config_key): """创建文件夹选择器组件并绑定保存功能""" frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=5) ttk.Label(frame, text=label_text).grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) entry = ttk.Entry(frame, width=70) entry.grid(row=0, column=1, padx=5, pady=5) button = ttk.Button(frame, text="浏览文件夹...", command=lambda: self.select_folder(entry, config_key)) button.grid(row=0, column=2, padx=5, pady=5) return entry, button def select_folder(self, entry, config_key): """选择文件夹并保存路径,同时设置文件对话框的初始目录""" # 获取上次保存的路径作为初始目录 initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() foldername = filedialog.askdirectory(initialdir=initial_dir) if foldername: entry.delete(0, tk.END) entry.insert(0, foldername) self.populate_folder_tree(foldername) self.save_path(config_key, foldername) def select_file(self, entry, config_key, filetypes=None): """选择文件并保存路径,同时设置文件对话框的初始目录""" if filetypes is None: filetypes = [("所有文件", "*.*")] # 获取上次保存的路径作为初始目录 initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() # 如果路径是文件,则使用其父目录 if os.path.isfile(initial_dir): initial_dir = os.path.dirname(initial_dir) filename = filedialog.askopenfilename(filetypes=filetypes, initialdir=initial_dir) if filename: entry.delete(0, tk.END) entry.insert(0, filename) self.save_path(config_key, filename) def get_last_path(self, config_key): """获取上次保存的路径""" config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if config.has_option('Paths', config_key): return config.get('Paths', config_key) return None def populate_folder_tree(self, path): """填充文件夹结构树""" self.tree.delete(*self.tree.get_children()) if not os.path.isdir(path): return root_node = self.tree.insert("", "end", text=os.path.basename(path), values=("文件夹",), open=True) self.add_tree_nodes(root_node, path) def add_tree_nodes(self, parent, path): """递归添加树节点""" try: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isdir(item_path): node = self.tree.insert(parent, "end", text=item, values=("文件夹",)) self.add_tree_nodes(node, item_path) else: self.tree.insert(parent, "end", text=item, values=("文件",)) except PermissionError: self.log_message(f"权限错误: 无法访问 {path}") def log_message(self, message): """记录日志消息""" self.queue.put(("log", message)) def update_progress(self, value): """更新进度条""" self.queue.put(("progress", value)) def update_status(self, message): """更新状态信息""" self.queue.put(("status", message)) def process_queue(self): """处理线程队列中的消息""" try: while not self.queue.empty(): msg_type, data = self.queue.get_nowait() if msg_type == "log": self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, data + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) elif msg_type == "progress": self.progress['value'] = data elif msg_type == "status": self.status_var.set(data) except queue.Empty: pass self.root.after(100, self.process_queue) def view_report(self, report_type): """查看生成的报告""" if report_type == "folder" and self.folder_report_path and os.path.exists(self.folder_report_path): try: webbrowser.open(self.folder_report_path) except Exception as e: messagebox.showerror("错误", f"无法打开文件夹报告: {str(e)}") else: messagebox.showwarning("警告", f"没有可用的{report_type}报告文件") def process_folders(self, old_path, new_path, excel_file): """处理文件夹比较的线程函数""" try: report_dir = os.path.dirname(excel_file) or os.getcwd() os.makedirs(report_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.folder_report_path = os.path.join(report_dir, f"folder_diff_report_{timestamp}.html") # 生成比较报告 self.update_status("生成比较报告...") self.update_progress(30) winmerge_path = self.winmerge_entry.get() if not self.run_winmerge(winmerge_path, old_path, new_path): self.update_status("WinMerge执行失败") return if not os.path.exists(self.folder_report_path) or os.path.getsize(self.folder_report_path) == 0: self.log_message("警告: 文件夹报告为空或未生成") else: self.log_message(f"文件夹报告生成成功: {self.folder_report_path} ({os.path.getsize(self.folder_report_path)} bytes)") self.view_folder_report_button.config(state=tk.NORMAL) self.copy_detail_files(report_dir) self.update_status("打开Excel文件...") self.update_progress(80) if not self.open_excel_file(excel_file): self.update_status("打开Excel失败") return self.update_progress(100) self.update_status("处理完成!") self.log_message("文件夹比较流程执行完毕") messagebox.showinfo("完成", "已生成报告并打开Excel文件") self.delete_winmerge_reports() except Exception as e: error_msg = f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.log_message(error_msg) self.update_status("执行失败") messagebox.showerror("错误", f"处理失败: {str(e)}") finally: if self.processing: self.stop_processing() def delete_winmerge_reports(self): """删除WinMerge生成的报告文件.files目录和复制的HTML文件""" if not self.folder_report_path: return # 1. 删除主报告文件 if os.path.exists(self.folder_report_path): try: os.remove(self.folder_report_path) self.log_message(f"已删除报告文件: {self.folder_report_path}") except Exception as e: self.log_message(f"删除报告文件失败: {str(e)}") # 2. 删除对应的.files目录 base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if os.path.exists(files_dir): try: shutil.rmtree(files_dir) self.log_message(f"已删除.files目录: {files_dir}") except Exception as e: self.log_message(f"删除.files目录失败: {str(e)}") # 3. 删除本次运行复制的HTML文件 report_dir = os.path.dirname(self.folder_report_path) if os.path.exists(report_dir) and hasattr(self, 'copied_html_files'): deleted_count = 0 for file_path in self.copied_html_files: if os.path.exists(file_path): try: os.remove(file_path) deleted_count += 1 self.log_message(f"已删除复制的HTML文件: {file_path}") except Exception as e: self.log_message(f"删除HTML文件失败: {file_path} - {str(e)}") self.log_message(f"已删除 {deleted_count}/{len(self.copied_html_files)} 个复制的HTML文件") # 清空列表 self.copied_html_files = [] # 4. 重置状态 self.folder_report_path = None self.view_folder_report_button.config(state=tk.DISABLED) def copy_detail_files(self, report_dir): """复制.files目录中的HTML文件到报告目录,并记录复制文件""" base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if not os.path.exists(files_dir): self.log_message(f"警告: 详细文件目录不存在 {files_dir}") return html_files = [f for f in os.listdir(files_dir) if f.lower().endswith('.html')] if not html_files: self.log_message(f"警告: 详细文件目录中没有HTML文件 {files_dir}") return # 初始化复制文件列表 if not hasattr(self, 'copied_html_files'): self.copied_html_files = [] copied_count = 0 for file_name in html_files: src_path = os.path.join(files_dir, file_name) dst_path = os.path.join(report_dir, file_name) try: shutil.copy2(src_path, dst_path) copied_count += 1 # 记录复制文件路径 self.copied_html_files.append(dst_path) except Exception as e: self.log_message(f"复制文件失败: {file_name} - {str(e)}") self.log_message(f"已复制 {copied_count}/{len(html_files)} 个详细HTML文件到报告目录") def start_processing(self): """启动处理线程""" if self.processing: self.log_message("警告: 处理正在进行中") return old_path = self.old_folder_entry.get() new_path = self.new_folder_entry.get() excel_file = self.excel_file_entry.get() validation_errors = [] if not old_path: validation_errors.append("原始文件夹路径为空") elif not os.path.isdir(old_path): validation_errors.append(f"原始文件夹路径无效: {old_path}") if not new_path: validation_errors.append("新文件夹路径为空") elif not os.path.isdir(new_path): validation_errors.append(f"新文件夹路径无效: {new_path}") if not excel_file: validation_errors.append("Excel文件路径为空") elif not excel_file.lower().endswith(('.xlsx', '.xlsm')): validation_errors.append("Excel文件必须是.xlsx或.xlsm格式") winmerge_path = self.winmerge_entry.get() if not winmerge_path or not os.path.exists(winmerge_path): validation_errors.append("WinMerge路径无效或未设置") if validation_errors: self.log_message("错误: " + "; ".join(validation_errors)) messagebox.showerror("输入错误", "\n".join(validation_errors)) return self.run_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.view_folder_report_button.config(state=tk.DISABLED) self.processing = True thread = threading.Thread(target=self.process_folders, args=(old_path, new_path, excel_file)) thread.daemon = True thread.start() self.log_message("处理线程已启动") def run_winmerge(self, winmerge_path, path1, path2): """针对WinMerge 2.16.12.0优化的报告生成方法""" if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}") return False os.makedirs(os.path.dirname(self.folder_report_path), exist_ok=True) cmd = [ winmerge_path, '/u', '/nosplash', '/dl', 'Base', '/dr', 'Modified', '/noninteractive' ] if self.recursive_var.get(): cmd.append('/r') file_filter = self.filter_var.get() if file_filter and file_filter != "*.*": cmd.extend(['-f', file_filter]) cmd.extend(['/or', self.folder_report_path]) cmd.extend([path1, path2]) self.update_status("正在生成比较报告...") return self.execute_winmerge_command(cmd, "比较报告") def execute_winmerge_command(self, cmd, report_type): """执行WinMerge命令并处理结果""" try: self.log_message(f"开始生成{report_type}...") self.log_message(f"执行命令: {' '.join(cmd)}") start_time = time.time() process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace', creationflags=subprocess.CREATE_NO_WINDOW ) timeout = 900 try: stdout, stderr = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() self.log_message(f"{report_type}生成超时({timeout}秒),已终止进程") return False elapsed = time.time() - start_time self.log_message(f"{report_type}生成完成,耗时: {elapsed:.2f}秒") if stdout.strip(): self.log_message(f"WinMerge输出:\n{stdout[:2000]}") if stderr.strip(): self.log_message(f"WinMerge错误:\n{stderr[:1000]}") if process.returncode == 0: self.log_message(f"{report_type}命令执行成功") return True elif process.returncode == 1: self.log_message(f"{report_type}命令执行完成(发现差异)") return True else: error_msg = f"{report_type}生成失败(退出码{process.returncode})" self.log_message(error_msg) return False except Exception as e: self.log_message(f"{report_type}生成错误: {str(e)}\n{traceback.format_exc()}") return False def open_excel_file(self, excel_path): """打开Excel文件并等待用户操作""" self.log_message("正在打开Excel文件...") try: if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False os.startfile(excel_path) self.log_message(f"Excel文件已打开: {excel_path}") self.log_message("Excel已打开,请手动执行操作") messagebox.showinfo("Excel已打开", "Excel文件已打开,请手动执行所需操作") return True except Exception as e: self.log_message(f"打开Excel文件失败: {str(e)}\n{traceback.format_exc()}") return False def stop_processing(self): """停止处理""" self.processing = False self.stop_button.config(state=tk.DISABLED) self.run_button.config(state=tk.NORMAL) self.view_folder_report_button.config(state=tk.NORMAL) self.update_status("操作已停止") def load_paths(self): """从配置文件加载保存的路径""" config = configparser.ConfigParser() if os.path.exists(self.config_file): try: config.read(self.config_file) # 原始文件夹路径 if config.has_option('Paths', 'old_folder'): old_path = config.get('Paths', 'old_folder') self.old_folder_entry.delete(0, tk.END) self.old_folder_entry.insert(0, old_path) if os.path.isdir(old_path): self.populate_folder_tree(old_path) # 修改后文件夹路径 if config.has_option('Paths', 'new_folder'): new_path = config.get('Paths', 'new_folder') self.new_folder_entry.delete(0, tk.END) self.new_folder_entry.insert(0, new_path) # Excel文件路径 if config.has_option('Paths', 'excel_file'): excel_path = config.get('Paths', 'excel_file') self.excel_file_entry.delete(0, tk.END) self.excel_file_entry.insert(0, excel_path) # WinMerge路径 if config.has_option('Paths', 'winmerge_path'): winmerge_path = config.get('Paths', 'winmerge_path') self.winmerge_entry.delete(0, tk.END) self.winmerge_entry.insert(0, winmerge_path) self.log_message("已加载上次保存的路径") except Exception as e: self.log_message(f"加载配置文件失败: {str(e)}") else: self.log_message("未找到配置文件,将使用默认路径") def save_path(self, key, path): """保存单个路径到配置文件""" config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if not config.has_section('Paths'): config.add_section('Paths') config.set('Paths', key, path) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message(f"已保存路径: {key} = {path}") except Exception as e: self.log_message(f"保存路径失败: {str(e)}") def save_all_paths(self): """保存所有路径到配置文件""" config = configparser.ConfigParser() config.add_section('Paths') config.set('Paths', 'old_folder', self.old_folder_entry.get()) config.set('Paths', 'new_folder', self.new_folder_entry.get()) config.set('Paths', 'excel_file', self.excel_file_entry.get()) config.set('Paths', 'winmerge_path', self.winmerge_entry.get()) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message("所有路径已保存") except Exception as e: self.log_message(f"保存所有路径失败: {str(e)}") def on_closing(self): """窗口关闭时的处理""" self.save_all_paths() self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = DiffProcessorApp(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop() 这是我的代码·,请在我的代码的基础上进行更改
08-23
点击执行比较时,出现处理失败:‘NoneType’ object is not subscriptable #软件著作人:杨晨 2025.07.11 import os import subprocess import shutil import time import tkinter as tk from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage import threading import queue import traceback import webbrowser import datetime import configparser import win32com.client class DiffProcessorApp: def __init__(self, root): self.root = root root.title("ファイル比較実施工具") root.geometry("1000x700") root.configure(bg="#f5f5f5") self.style = ttk.Style() self.style.theme_use('clam') self.style.configure('TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#333", background="#4CAF50", bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('disabled', '#9E9E9E')]) self.style.configure('View.TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#008000", background="#4CAF50", bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('View.TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('active', '#004D00'), ('disabled', '#808080')]) self.style.configure('TLabel', font=('Segoe UI', 9), background="#f5f5f5") self.style.configure('TLabelframe', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", relief="flat", borderwidth=2) self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", foreground="#2E7D32") self.style.configure('Treeview', font=('Segoe UI', 9), rowheight=25) self.style.configure('Treeview.Heading', font=('Segoe UI', 9, 'bold')) main_frame = ttk.Frame(root, padding="15") main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) try: icon = PhotoImage(file="folder_icon.png") self.icon_label = ttk.Label(header_frame, image=icon) self.icon_label.image = icon self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) except: self.icon_label = ttk.Label(header_frame, text="📁", font=("Arial", 24)) self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) title_label = ttk.Label(header_frame, text="ファイル比較実施工具", font=("Segoe UI", 18, "bold"), foreground="#2E7D32") title_label.pack(side=tk.LEFT) file_frame = ttk.LabelFrame(main_frame, text="文件夹选择", padding="12") file_frame.pack(fill=tk.X, pady=5) self.old_folder_entry, _ = self.create_folder_selector(file_frame, "原始文件夹:", "old_folder") self.new_folder_entry, _ = self.create_folder_selector(file_frame, "修改后文件夹:", "new_folder") options_frame = ttk.LabelFrame(main_frame, text="比较选项", padding="12") options_frame.pack(fill=tk.X, pady=5) self.recursive_var = tk.BooleanVar(value=True) recursive_check = ttk.Checkbutton(options_frame, text="递归比较子文件夹", variable=self.recursive_var) recursive_check.grid(row=0, column=0, padx=10, pady=5, sticky=tk.W) filter_frame = ttk.Frame(options_frame) filter_frame.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="文件过滤:").pack(side=tk.LEFT, padx=(0, 5)) self.filter_var = tk.StringVar(value="*.*") filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=15) filter_entry.pack(side=tk.LEFT) self.excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12") self.excel_frame.pack(fill=tk.X, pady=5) ttk.Label(self.excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.excel_file_entry = ttk.Entry(self.excel_frame, width=60) self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(self.excel_frame, text="浏览...", command=lambda: self.select_file(self.excel_file_entry, "excel_file", [("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5) winmerge_frame = ttk.Frame(self.excel_frame) winmerge_frame.grid(row=1, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5) ttk.Label(winmerge_frame, text="WinMerge路径:").grid(row=0, column=0, sticky=tk.W) self.winmerge_entry = ttk.Entry(winmerge_frame, width=60) self.winmerge_entry.grid(row=0, column=1, padx=5) self.winmerge_entry.insert(0, r"E:\App\WinMerge\WinMerge2.16.46.0\WinMergeU.exe") ttk.Button(winmerge_frame, text="浏览...", command=lambda: self.select_file(self.winmerge_entry, "winmerge_path", [("WinMerge 可执行文件", "*.exe")])).grid(row=0, column=2) delete_frame = ttk.Frame(self.excel_frame) delete_frame.grid(row=2, column=0, columnspan=3, sticky=tk.W, padx=5, pady=5) self.delete_temp_files_var = tk.BooleanVar(value=False) delete_check = ttk.Checkbutton( delete_frame, text="完成后删除临时文件", variable=self.delete_temp_files_var, command=self.update_view_button_state ) delete_check.grid(row=0, column=0, padx=5, sticky=tk.W) self.delete_status_label = ttk.Label( delete_frame, text="(勾选后将删除报告文件,无法查看)", foreground="#FF0000", font=("Segoe UI", 9) ) self.delete_status_label.grid(row=0, column=1, padx=5) button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=10) self.run_button = ttk.Button(button_frame, text="执行比较", command=self.start_processing, width=20, style='TButton') self.run_button.pack(side=tk.LEFT) self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_processing, width=10, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=10) self.view_folder_report_button = ttk.Button(button_frame, text="查看文件夹报告", command=lambda: self.view_report("folder"), width=15, state=tk.DISABLED, style='View.TButton') self.view_folder_report_button.pack(side=tk.LEFT, padx=10) self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=700, mode='determinate') self.progress.pack(fill=tk.X, pady=5) status_frame = ttk.Frame(main_frame) status_frame.pack(fill=tk.X, pady=5) self.status_var = tk.StringVar(value="准备就绪") status_label = ttk.Label(status_frame, textvariable=self.status_var, font=("Segoe UI", 9), foreground="#2E7D32") status_label.pack(side=tk.LEFT) notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=5) tree_frame = ttk.Frame(notebook, padding="5") notebook.add(tree_frame, text="文件夹结构") self.tree = ttk.Treeview(tree_frame, columns=("Status"), show="tree") self.tree.heading("#0", text="文件夹结构", anchor=tk.W) self.tree.heading("Status", text="状态", anchor=tk.W) self.tree.column("#0", width=400) self.tree.column("Status", width=100) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") log_frame = ttk.Frame(notebook, padding="5") notebook.add(log_frame, text="执行日志") self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD, font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) self.log_text.config(state=tk.DISABLED) tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) self.processing = False self.queue = queue.Queue() self.folder_report_path = None self.files_dir = None self.copied_html_files = [] self.config_file = "folder_compare_config.ini" self.load_paths() self.root.after(100, self.process_queue) def update_view_button_state(self): if self.delete_temp_files_var.get(): self.view_folder_report_button.config(state=tk.DISABLED) self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD') else: if self.folder_report_path and os.path.exists(self.folder_report_path): self.view_folder_report_button.config(state=tk.NORMAL) self.style.configure('View.TButton', foreground='#008000', background='#4CAF50') else: self.view_folder_report_button.config(state=tk.DISABLED) self.style.configure('View.TButton', foreground='#808080', background='#BDBDBD') self.view_folder_report_button.configure(style='View.TButton') def create_folder_selector(self, parent, label_text, config_key): frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=5) ttk.Label(frame, text=label_text).grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) entry = ttk.Entry(frame, width=70) entry.grid(row=0, column=1, padx=5, pady=5) button = ttk.Button(frame, text="浏览文件夹...", command=lambda: self.select_folder(entry, config_key)) button.grid(row=0, column=2, padx=5, pady=5) return entry, button def select_folder(self, entry, config_key): initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() foldername = filedialog.askdirectory(initialdir=initial_dir) if foldername: entry.delete(0, tk.END) entry.insert(0, foldername) self.populate_folder_tree(foldername) self.save_path(config_key, foldername) def select_file(self, entry, config_key, filetypes=None): if filetypes is None: filetypes = [("所有文件", "*.*")] initial_dir = self.get_last_path(config_key) if not initial_dir or not os.path.exists(initial_dir): initial_dir = os.getcwd() if os.path.isfile(initial_dir): initial_dir = os.path.dirname(initial_dir) filename = filedialog.askopenfilename(filetypes=filetypes, initialdir=initial_dir) if filename: entry.delete(0, tk.END) entry.insert(0, filename) self.save_path(config_key, filename) def get_last_path(self, config_key): config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if config.has_option('Paths', config_key): return config.get('Paths', config_key) return None def populate_folder_tree(self, path): self.tree.delete(*self.tree.get_children()) if not os.path.isdir(path): return root_node = self.tree.insert("", "end", text=os.path.basename(path), values=("文件夹",), open=True) self.add_tree_nodes(root_node, path) def add_tree_nodes(self, parent, path): try: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isdir(item_path): node = self.tree.insert(parent, "end", text=item, values=("文件夹",)) self.add_tree_nodes(node, item_path) else: self.tree.insert(parent, "end", text=item, values=("文件",)) except PermissionError: self.log_message(f"权限错误: 无法访问 {path}") def log_message(self, message): self.queue.put(("log", message)) def update_progress(self, value): self.queue.put(("progress", value)) def update_status(self, message): self.queue.put(("status", message)) def process_queue(self): try: while not self.queue.empty(): msg_type, data = self.queue.get_nowait() if msg_type == "log": self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, data + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) elif msg_type == "progress": self.progress['value'] = data elif msg_type == "status": self.status_var.set(data) except queue.Empty: pass self.root.after(100, self.process_queue) def view_report(self, report_type): if report_type == "folder" and self.folder_report_path and os.path.exists(self.folder_report_path): try: webbrowser.open(self.folder_report_path) except Exception as e: messagebox.showerror("错误", f"无法打开文件夹报告: {str(e)}") else: messagebox.showwarning("警告", f"没有可用的{report_type}报告文件") def process_folders(self, old_path, new_path, excel_file): try: report_dir = os.path.dirname(excel_file) or os.getcwd() os.makedirs(report_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.folder_report_path = os.path.join(report_dir, f"folder_diff_report_{timestamp}.html") self.update_status("生成比较报告...") self.update_progress(30) winmerge_path = self.winmerge_entry.get() if not self.run_winmerge(winmerge_path, old_path, new_path): self.update_status("WinMerge执行失败") return if not os.path.exists(self.folder_report_path) or os.path.getsize(self.folder_report_path) == 0: self.log_message("警告: 文件夹报告为空或未生成") else: self.log_message(f"文件夹报告生成成功: {self.folder_report_path} ({os.path.getsize(self.folder_report_path)} bytes)") self.copy_detail_files(report_dir) self.update_status("打开Excel文件...") self.update_progress(80) if not self.open_excel_file(excel_file): self.update_status("打开Excel失败") return if self.delete_temp_files_var.get(): self.delete_winmerge_reports() self.log_message("已删除所有临时文件") else: self.log_message("保留临时文件,可查看报告") self.root.after(100, self.update_view_button_state) self.update_progress(100) self.update_status("处理完成!") self.log_message("文件夹比较流程执行完毕") messagebox.showinfo("完成", "已生成报告并打开Excel文件") except Exception as e: error_msg = f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.log_message(error_msg) self.update_status("执行失败") messagebox.showerror("错误", f"处理失败: {str(e)}") finally: if self.processing: self.stop_processing() def delete_winmerge_reports(self): if not self.folder_report_path: return if os.path.exists(self.folder_report_path): try: os.remove(self.folder_report_path) self.log_message(f"已删除报告文件: {self.folder_report_path}") except Exception as e: self.log_message(f"删除报告文件失败: {str(e)}") base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if os.path.exists(files_dir): try: shutil.rmtree(files_dir) self.log_message(f"已删除.files目录: {files_dir}") except Exception as e: self.log_message(f"删除.files目录失败: {str(e)}") report_dir = os.path.dirname(self.folder_report_path) if os.path.exists(report_dir) and hasattr(self, 'copied_html_files') and self.copied_html_files: deleted_count = 0 for file_path in self.copied_html_files: if os.path.exists(file_path): try: os.remove(file_path) deleted_count += 1 self.log_message(f"已删除复制的HTML文件: {file_path}") except Exception as e: self.log_message(f"删除HTML文件失败: {file_path} - {str(e)}") self.log_message(f"已删除 {deleted_count}/{len(self.copied_html_files)} 个复制的HTML文件") self.copied_html_files = [] self.folder_report_path = None self.view_folder_report_button.config(state=tk.DISABLED) self.root.after(100, self.update_view_button_state) def copy_detail_files(self, report_dir): base_path = os.path.splitext(self.folder_report_path)[0] files_dir = base_path + ".files" if not os.path.exists(files_dir): self.log_message(f"警告: 详细文件目录不存在 {files_dir}") return html_files = [f for f in os.listdir(files_dir) if f.lower().endswith('.html')] if not html_files: self.log_message(f"警告: 详细文件目录中没有HTML文件 {files_dir}") return if not hasattr(self, 'copied_html_files') or not self.copied_html_files: self.copied_html_files = [] copied_count = 0 for file_name in html_files: src_path = os.path.join(files_dir, file_name) dst_path = os.path.join(report_dir, file_name) try: shutil.copy2(src_path, dst_path) copied_count += 1 self.copied_html_files.append(dst_path) except Exception as e: self.log_message(f"复制文件失败: {file_name} - {str(e)}") self.log_message(f"已复制 {copied_count}/{len(html_files)} 个详细HTML文件到报告目录") def start_processing(self): if self.processing: self.log_message("警告: 处理正在进行中") return old_path = self.old_folder_entry.get() new_path = self.new_folder_entry.get() excel_file = self.excel_file_entry.get() validation_errors = [] if not old_path: validation_errors.append("原始文件夹路径为空") elif not os.path.isdir(old_path): validation_errors.append(f"原始文件夹路径无效: {old_path}") if not new_path: validation_errors.append("新文件夹路径为空") elif not os.path.isdir(new_path): validation_errors.append(f"新文件夹路径无效: {new_path}") if not excel_file: validation_errors.append("Excel文件路径为空") elif not excel_file.lower().endswith(('.xlsx', '.xlsm')): validation_errors.append("Excel文件必须是.xlsx或.xlsm格式") winmerge_path = self.winmerge_entry.get() if not winmerge_path or not os.path.exists(winmerge_path): validation_errors.append("WinMerge路径无效或未设置") if validation_errors: self.log_message("错误: " + "; ".join(validation_errors)) messagebox.showerror("输入错误", "\n".join(validation_errors)) return self.run_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.view_folder_report_button.config(state=tk.DISABLED) self.processing = True self.copied_html_files = [] thread = threading.Thread(target=self.process_folders, args=(old_path, new_path, excel_file)) thread.daemon = True thread.start() self.log_message("处理线程已启动") def run_winmerge(self, winmerge_path, path1, path2): if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}") return False os.makedirs(os.path.dirname(self.folder_report_path), exist_ok=True) cmd = [ winmerge_path, '/u', '/nosplash', '/dl', 'Base', '/dr', 'Modified', '/noninteractive', '/minimize' ] if self.recursive_var.get(): cmd.extend(['/r', '/s']) else: cmd.extend(['/r-', '/s-']) file_filter = self.filter_var.get() if file_filter and file_filter != "*.*": cmd.extend(['-f', file_filter]) cmd.extend(['/or', self.folder_report_path]) cmd.extend([path1, path2]) self.update_status("正在生成比较报告...") return self.execute_winmerge_command(cmd, "比较报告") def execute_winmerge_command(self, cmd, report_type): try: self.log_message(f"开始生成{report_type}...") self.log_message(f"执行命令: {' '.join(cmd)}") start_time = time.time() creation_flags = 0 if os.name == 'nt': creation_flags = subprocess.CREATE_NO_WINDOW process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace', creationflags=creation_flags ) timeout = 900 try: stdout, stderr = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() self.log_message(f"{report_type}生成超时({timeout}秒),已终止进程") return False elapsed = time.time() - start_time self.log_message(f"{report_type}生成完成,耗时: {elapsed:.2f}秒") if stdout.strip(): self.log_message(f"WinMerge输出:\n{stdout[:2000]}") if stderr.strip(): self.log_message(f"WinMerge错误:\n{stderr[:1000]}") if process.returncode == 0: self.log_message(f"{report_type}命令执行成功") return True elif process.returncode == 1: self.log_message(f"{report_type}命令执行完成(发现差异)") return True elif process.returncode == 2: self.log_message(f"{report_type}命令执行完成(发现错误)") return False else: error_msg = f"{report_type}生成失败(退出码{process.returncode})" self.log_message(error_msg) return False except Exception as e: self.log_message(f"{report_type}生成错误: {str(e)}\n{traceback.format_exc()}") return False def open_excel_file(self, excel_path): self.log_message("正在打开Excel文件并执行宏...") try: if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False # 创建Excel应用程序对象 excel_app = win32com.client.Dispatch("Excel.Application") excel_app.Visible = True # 让Excel界面可见 excel_app.DisplayAlerts = False # 禁用警告提示 # 打开工作簿 wb = excel_app.Workbooks.Open(os.path.abspath(excel_path)) # 执行Sheet1中的宏 try: # 获取Sheet1对象 sheet1 = wb.Sheets("Sheet1") # 执行宏"ExposedMacro" excel_app.Run("Sheet1.ExposedMacro") self.log_message("宏 'ExposedMacro' 执行成功") except Exception as macro_e: error_msg = f"执行宏失败: {str(macro_e)}" if hasattr(macro_e, 'excepinfo') and macro_e.excepinfo[5]: error_msg += f" (详细错误: {macro_e.excepinfo[5]})" self.log_message(error_msg) messagebox.showerror("宏执行错误", f"无法执行宏: {error_msg}") # 保存并关闭工作簿,退出Excel wb.Save() wb.Close() excel_app.Quit() self.log_message(f"Excel文件处理完成: {excel_path}") return True except Exception as e: error_msg = f"打开Excel文件失败: {str(e)}" if hasattr(e, 'excepinfo') and e.excepinfo[5]: error_msg += f" (详细错误: {e.excepinfo[5]})" self.log_message(error_msg) messagebox.showerror("Excel错误", error_msg) return False def stop_processing(self): self.processing = False self.stop_button.config(state=tk.DISABLED) self.run_button.config(state=tk.NORMAL) self.root.after(100, self.update_view_button_state) self.update_status("操作已停止") def load_paths(self): config = configparser.ConfigParser() if os.path.exists(self.config_file): try: config.read(self.config_file) if config.has_option('Paths', 'old_folder'): old_path = config.get('Paths', 'old_folder') self.old_folder_entry.delete(0, tk.END) self.old_folder_entry.insert(0, old_path) if os.path.isdir(old_path): self.populate_folder_tree(old_path) if config.has_option('Paths', 'new_folder'): new_path = config.get('Paths', 'new_folder') self.new_folder_entry.delete(0, tk.END) self.new_folder_entry.insert(0, new_path) if config.has_option('Paths', 'excel_file'): excel_path = config.get('Paths', 'excel_file') self.excel_file_entry.delete(0, tk.END) self.excel_file_entry.insert(0, excel_path) if config.has_option('Paths', 'winmerge_path'): winmerge_path = config.get('Paths', 'winmerge_path') self.winmerge_entry.delete(0, tk.END) self.winmerge_entry.insert(0, winmerge_path) self.log_message("已加载上次保存的路径") except Exception as e: self.log_message(f"加载配置文件失败: {str(e)}") else: self.log_message("未找到配置文件,将使用默认路径") def save_path(self, key, path): config = configparser.ConfigParser() if os.path.exists(self.config_file): config.read(self.config_file) if not config.has_section('Paths'): config.add_section('Paths') config.set('Paths', key, path) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message(f"已保存路径: {key} = {path}") except Exception as e: self.log_message(f"保存路径失败: {str(e)}") def save_all_paths(self): config = configparser.ConfigParser() config.add_section('Paths') config.set('Paths', 'old_folder', self.old_folder_entry.get()) config.set('Paths', 'new_folder', self.new_folder_entry.get()) config.set('Paths', 'excel_file', self.excel_file_entry.get()) config.set('Paths', 'winmerge_path', self.winmerge_entry.get()) try: with open(self.config_file, 'w') as configfile: config.write(configfile) self.log_message("所有路径已保存") except Exception as e: self.log_message(f"保存所有路径失败: {str(e)}") def on_closing(self): self.save_all_paths() self.root.destroy() if __name__ == "__main__": root = tk.Tk() app = DiffProcessorApp(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop()
09-10
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值