一、应用场景
这个工具适用于以下场景:
- 设计师需要从大量素材中筛选特定图片复制并保存
- 摄影师需要根据文件名批量整理照片查找筛选复制
- 电商运营人员需要从产品库中提取特定商品图片复制到指定文件夹
- 数据分析师需要批量收集特定图片复制保存用于处理
- 任何需要从大量图片中快速找到并整理特定文件的场景
二、界面设计
应用采用直观的三部分布局:
- 顶部区域:源目录和目标目录选择,用户可以浏览文件系统选择相应目录
- 中间区域:
- 文本框用于输入要搜索的文件名,支持多行输入
- 选项区域包含搜索选项:区分大小写、使用正则表达式、覆盖已存在文件、搜索子文件夹
- 图片格式筛选,用户可以自定义要搜索的图片格式
- 底部区域:
- 状态显示和进度条,显示当前操作状态和进度
- 结果文本框,显示搜索和复制操作的详细结果
- 功能按钮:开始搜索、清空列表和退出
三、详细代码步骤
import os
import shutil
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import re
from pathlib import Pathclass ImageFinderApp:def __init__(self, root):self.root = rootself.root.title("图片查找与复制工具")self.root.geometry("800x600")self.root.minsize(600, 500)# 设置中文字体支持self.font = ('SimHei', 10)# 创建主框架self.main_frame = ttk.Frame(root, padding="10")self.main_frame.pack(fill=tk.BOTH, expand=True)# 源目录选择ttk.Label(self.main_frame, text="源目录:", font=self.font).grid(row=0, column=0, sticky=tk.W, pady=5)self.source_var = tk.StringVar()ttk.Entry(self.main_frame, textvariable=self.source_var, width=60, font=self.font).grid(row=0, column=1, pady=5, padx=5)ttk.Button(self.main_frame, text="浏览", command=self.browse_source, width=10).grid(row=0, column=2, pady=5)# 目标目录选择ttk.Label(self.main_frame, text="目标目录:", font=self.font).grid(row=1, column=0, sticky=tk.W, pady=5)self.target_var = tk.StringVar()ttk.Entry(self.main_frame, textvariable=self.target_var, width=60, font=self.font).grid(row=1, column=1, pady=5, padx=5)ttk.Button(self.main_frame, text="浏览", command=self.browse_target, width=10).grid(row=1, column=2, pady=5)# 文件名字段ttk.Label(self.main_frame, text="输入文件名(每行一个):", font=self.font).grid(row=2, column=0, sticky=tk.W, pady=5)self.filenames_text = tk.Text(self.main_frame, height=10, width=70, font=self.font)self.filenames_text.grid(row=3, column=0, columnspan=3, pady=5, sticky=tk.W+tk.E)# 滚动条scrollbar = ttk.Scrollbar(self.main_frame, command=self.filenames_text.yview)scrollbar.grid(row=3, column=3, sticky=tk.N+tk.S)self.filenames_text.config(yscrollcommand=scrollbar.set)# 选项options_frame = ttk.LabelFrame(self.main_frame, text="选项", padding="10")options_frame.grid(row=4, column=0, columnspan=3, pady=10, sticky=tk.W+tk.E)self.case_sensitive_var = tk.BooleanVar(value=False)ttk.Checkbutton(options_frame, text="区分大小写", variable=self.case_sensitive_var, font=self.font).grid(row=0, column=0, padx=10)self.use_regex_var = tk.BooleanVar(value=False)ttk.Checkbutton(options_frame, text="使用正则表达式", variable=self.use_regex_var, font=self.font).grid(row=0, column=1, padx=10)self.overwrite_var = tk.BooleanVar(value=False)ttk.Checkbutton(options_frame, text="覆盖已存在文件", variable=self.overwrite_var, font=self.font).grid(row=0, column=2, padx=10)self.search_subfolders_var = tk.BooleanVar(value=True)ttk.Checkbutton(options_frame, text="搜索子文件夹", variable=self.search_subfolders_var, font=self.font).grid(row=0, column=3, padx=10)# 图片格式筛选ttk.Label(options_frame, text="图片格式:", font=self.font).grid(row=1, column=0, sticky=tk.W, pady=5)self.image_formats = tk.StringVar(value="jpg,jpeg,png,gif,bmp")ttk.Entry(options_frame, textvariable=self.image_formats, width=40, font=self.font).grid(row=1, column=1, pady=5, padx=5)ttk.Label(options_frame, text="(逗号分隔)", font=self.font).grid(row=1, column=2, sticky=tk.W, pady=5)# 状态和进度条self.status_var = tk.StringVar(value="就绪")ttk.Label(self.main_frame, textvariable=self.status_var, font=self.font).grid(row=5, column=0, sticky=tk.W, pady=5)self.progress_var = tk.DoubleVar(value=0)self.progress_bar = ttk.Progressbar(self.main_frame, variable=self.progress_var, length=100, mode='determinate')self.progress_bar.grid(row=5, column=1, pady=5, sticky=tk.W+tk.E)# 结果显示ttk.Label(self.main_frame, text="结果:", font=self.font).grid(row=6, column=0, sticky=tk.W, pady=5)self.result_text = tk.Text(self.main_frame, height=10, width=70, font=self.font)self.result_text.grid(row=7, column=0, columnspan=3, pady=5, sticky=tk.W+tk.E+tk.N+tk.S)self.result_text.config(state=tk.DISABLED)# 结果滚动条result_scrollbar = ttk.Scrollbar(self.main_frame, command=self.result_text.yview)result_scrollbar.grid(row=7, column=3, sticky=tk.N+tk.S)self.result_text.config(yscrollcommand=result_scrollbar.set)# 按钮button_frame = ttk.Frame(self.main_frame)button_frame.grid(row=8, column=0, columnspan=3, pady=10, sticky=tk.E)ttk.Button(button_frame, text="开始搜索", command=self.start_search, width=15).grid(row=0, column=0, padx=5)ttk.Button(button_frame, text="清空列表", command=self.clear_list, width=15).grid(row=0, column=1, padx=5)ttk.Button(button_frame, text="退出", command=root.quit, width=15).grid(row=0, column=2, padx=5)# 配置网格权重,使界面可伸缩self.main_frame.columnconfigure(1, weight=1)self.main_frame.rowconfigure(7, weight=1)# 存储结果self.search_results = []self.total_files = 0self.processed_files = 0def browse_source(self):directory = filedialog.askdirectory(title="选择源目录")if directory:self.source_var.set(directory)def browse_target(self):directory = filedialog.askdirectory(title="选择目标目录")if directory:self.target_var.set(directory)def clear_list(self):self.filenames_text.delete(1.0, tk.END)self.result_text.config(state=tk.NORMAL)self.result_text.delete(1.0, tk.END)self.result_text.config(state=tk.DISABLED)self.status_var.set("就绪")self.progress_var.set(0)def start_search(self):# 获取用户输入source_dir = self.source_var.get()target_dir = self.target_var.get()filenames_text = self.filenames_text.get(1.0, tk.END).strip()# 验证输入if not source_dir:messagebox.showerror("错误", "请选择源目录")returnif not os.path.isdir(source_dir):messagebox.showerror("错误", "源目录不存在")returnif not target_dir:messagebox.showerror("错误", "请选择目标目录")returnif not filenames_text:messagebox.showerror("错误", "请输入要搜索的文件名")return# 准备文件名列表filenames = [line.strip() for line in filenames_text.split('\n') if line.strip()]# 创建目标目录(如果不存在)os.makedirs(target_dir, exist_ok=True)# 清空结果显示self.result_text.config(state=tk.NORMAL)self.result_text.delete(1.0, tk.END)self.result_text.config(state=tk.DISABLED)# 启动搜索线程self.search_results = []self.total_files = len(filenames)self.processed_files = 0self.progress_var.set(0)thread = threading.Thread(target=self.search_and_copy_files, args=(source_dir, target_dir, filenames))thread.daemon = Truethread.start()def search_and_copy_files(self, source_dir, target_dir, filenames):# 更新状态self.update_status("正在搜索文件...")# 获取图片格式列表image_extensions = [f".{ext.strip().lower()}" for ext in self.image_formats.get().split(',') if ext.strip()]# 搜索选项case_sensitive = self.case_sensitive_var.get()use_regex = self.use_regex_var.get()search_subfolders = self.search_subfolders_var.get()overwrite = self.overwrite_var.get()found_count = 0not_found_count = 0copied_count = 0skipped_count = 0# 遍历每个文件名进行搜索for filename in filenames:if not case_sensitive:filename_lower = filename.lower()else:filename_lower = filename# 存储当前文件的搜索结果found_files = []# 使用正则表达式搜索if use_regex:try:pattern = re.compile(filename, re.IGNORECASE if not case_sensitive else 0)# 遍历源目录中的所有文件if search_subfolders:for root, _, files in os.walk(source_dir):for file in files:if not image_extensions or Path(file).suffix.lower() in image_extensions:if pattern.search(file):found_files.append(os.path.join(root, file))else:for file in os.listdir(source_dir):if os.path.isfile(os.path.join(source_dir, file)):if not image_extensions or Path(file).suffix.lower() in image_extensions:if pattern.search(file):found_files.append(os.path.join(source_dir, file))except re.error as e:self.update_result(f"错误: 无效的正则表达式 '{filename}': {str(e)}")not_found_count += 1self.update_progress()continue# 普通文件名搜索else:# 遍历源目录中的所有文件if search_subfolders:for root, _, files in os.walk(source_dir):for file in files:if not image_extensions or Path(file).suffix.lower() in image_extensions:compare_name = file if case_sensitive else file.lower()if compare_name == filename_lower:found_files.append(os.path.join(root, file))else:for file in os.listdir(source_dir):if os.path.isfile(os.path.join(source_dir, file)):if not image_extensions or Path(file).suffix.lower() in image_extensions:compare_name = file if case_sensitive else file.lower()if compare_name == filename_lower:found_files.append(os.path.join(source_dir, file))# 处理搜索结果if found_files:found_count += 1for found_file in found_files:# 构建目标路径target_file = os.path.join(target_dir, os.path.basename(found_file))# 检查文件是否已存在if os.path.exists(target_file):if overwrite:try:shutil.copy2(found_file, target_file)self.update_result(f"已复制: {found_file} -> {target_file}")copied_count += 1except Exception as e:self.update_result(f"错误: 无法复制 {found_file}: {str(e)}")else:self.update_result(f"已跳过: {found_file} (目标文件已存在)")skipped_count += 1else:try:shutil.copy2(found_file, target_file)self.update_result(f"已复制: {found_file} -> {target_file}")copied_count += 1except Exception as e:self.update_result(f"错误: 无法复制 {found_file}: {str(e)}")else:not_found_count += 1self.update_result(f"未找到: {filename}")# 更新进度self.update_progress()# 显示最终结果self.update_status(f"搜索完成。找到: {found_count}, 未找到: {not_found_count}, 已复制: {copied_count}, 已跳过: {skipped_count}")def update_result(self, message):self.root.after(0, self._update_result_ui, message)def _update_result_ui(self, message):self.result_text.config(state=tk.NORMAL)self.result_text.insert(tk.END, message + "\n")self.result_text.see(tk.END)self.result_text.config(state=tk.DISABLED)def update_status(self, message):self.root.after(0, self.status_var.set, message)def update_progress(self):self.processed_files += 1progress = (self.processed_files / self.total_files) * 100self.root.after(0, self.progress_var.set, progress)if __name__ == "__main__":root = tk.Tk()app = ImageFinderApp(root)root.mainloop()
四、总结优化
- 性能优化:
- 使用多线程处理,避免界面冻结
- 支持正则表达式搜索,提高搜索灵活性
- 可以限制搜索的文件类型为图片,提高搜索效率
- 用户体验优化:
- 直观的图形界面,易于操作
- 实时显示搜索进度和结果
- 提供多种搜索选项,满足不同需求
- 错误处理:
- 处理目录不存在、文件复制失败等异常情况
- 提供明确的错误提示
- 扩展性:
- 可以轻松添加更多功能,如预览图片、批量重命名等
- 代码结构清晰,易于维护和修改
这个工具通过简单易用的界面和强大的搜索功能,帮助用户快速从大量图片中找到并复制需要的文件,提高工作效率。