Python中基于Tkinter设计批量文件搜索和删除工具(第22节)


在日常工作和生活中,我们经常需要在电脑中查找或删除大量文件,在文件繁杂、种类繁多的情况下,如果通过手动查找或删除文件,可能需要花费数小时,既费时又费力,而通过Python编程可在几秒内完成。

在Python的系统开发或维护中,我们经常需要清理服务器的日志目录或临时文件等,当这些文件数量达到上百个时,手动删除显然不现实。本节教程将介绍如何通过Python安全高效地完成大规模文件搜索和删除任务。

1、批量文件搜索和删除工具介绍

我们的目标是开发一个批量文件搜索和删除工具,结合Python的Tkinter图形用户界面库可以很方便地创建GUI图形界面,以实现批量查找和删除文件的功能。

在指定目录下查找文件或文件夹,主要使用os模块和fnmatch模块。首先,使用os模块的os.walk()方法递归遍历指定目录中所有的目录路径、子目录列表和文件列表,再调用fnmatch模块的fnmatch()方法来检查每个文件的名称是否匹配。fnmatch模块经常用于批量查找文件,通过模式匹配筛选出需要查找的文件名,支持精确、模糊或格式匹配。

功能概述

本程序是基于Python的Tkinter库开发的GUI应用程序,主要功能包括:

  • 支持搜索文件或者文件夹,同时支持“*”通配符搜索,输入“*”可以搜索全部的文件和文件夹;输入“*.py”、“*.jpg”、“*.txt”、“*.js”、“*.css”等可以搜索指定后缀名的文件。

  • 使用多线程搜索,避免程序界面卡死。

  • 在搜索结果中,通过右击鼠标可以删除单个文件,也可以点击界面中的“删除以上所有文件”按钮,一次性删除列表中所有的文件或者文件夹,注意,删除后不可恢复,建议先备份重要数据。

  • 在搜索结果中双击鼠标左键,可以直接打开搜索的文件,还可以通过右击鼠标打开指定文件,或者定位文件所在的目录。

  • 搜索结果支持排序操作,可以按名称排序或者修改时间排序。

  • 支持导出搜索结果列表到本地的txt文件内。

  • 跨平台支持(支持Windows/macOS/Linux)

2、批量文件搜索和删除工具界面演示

Python中基于Tkinter设计批量文件搜索和删除工具

Python中基于Tkinter设计批量文件搜索和删除工具

3、代码实现

(1)导入必要的库

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os, shutil, threading, platform, subprocess, fnmatch, webbrowser

这里导入了Tkinter库用于创建GUI应用图形界面。os库用于系统功能操作,它提供了通用的操作系统交互功能,用于文件管理、路径操作、进程控制等。shutil库同样作为Python标准库的一部分,提供了文件复制、移动、删除、压缩/解压等功能,是对os库的补充。fnmatch库用于文件名匹配,提供模式匹配功能,常用于批量处理文件名。threading库用于多线程编程,允许程序同时执行多个任务,从而提高程序执行的效率和响应速度。subprocess库用于创建和管理子进程,允许执行外部命令,使用subprocess模块可以快速启动新进程、与其进行交互,甚至获取输出结果。

(2)批量文件搜索和删除工具的完整代码如下所示:

动手练一练:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os, shutil, threading, platform, subprocess, fnmatch, webbrowser

# 定义主窗口
class Main:
    def __init__(self, app):
        # 初始化主窗口
        self.app = app
        self.app.title("批量文件搜索和删除工具")

        # 创建窗口“帮助”菜单栏
        self.menu_bar = tk.Menu(self.app)
        self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.file_menu.add_command(label="关于程序", command=self.program_about)
        self.file_menu.add_command(label="程序讲解", command=self.help_window)
        self.file_menu.add_separator()  
        self.file_menu.add_command(label="退出", command=self.app.quit)
        self.menu_bar.add_cascade(label="帮助", menu=self.file_menu)
        self.app.config(menu=self.menu_bar)

        # 设置窗口居中
        window_width = 900
        window_height = 680
        screen_width = self.app.winfo_screenwidth()
        screen_height = self.app.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        self.app.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))

        # 窗口样式配置
        self.style = ttk.Style()
        self.set_styles()

        # 主界面创建
        self.create_widgets()

        # 在搜索结果中,鼠标右键显示菜单
        self.context_menu = tk.Menu(self.app, tearoff=0, font=('宋体', 12),
                                 bg='#F0F0F0', fg='#333333',
                                 activebackground='#0078D4',
                                 activeforeground='white')
        self.context_menu.add_command(label="打开文件", command=self.open_file)
        self.context_menu.add_command(label="打开所在目录", command=self.open_file_location)
        self.context_menu.add_command(label="删除该文件", command=self.delete_selected_files)

    # 定义样式函数
    def set_styles(self):
        # 设置主题为clam
        self.style.theme_use('clam')

        # 组件基础样式配置
        self.style.configure('TFrame', background='#F3F3F3')
        self.style.configure('TLabel', background='#F3F3F3',
                             foreground='#605E5C')
        self.style.configure('TEntry', fieldbackground='#FFFFFF',
                             bordercolor='#CCCCCC', relief='solid')
        self.style.configure('TButton', padding=6, relief='flat',
                             background='#0078D4',
                             font=('宋体', 12),
                             foreground='white')
        self.style.map('TButton',
                       background=[('active', '#004da3'), ('disabled', '#E0E0E0')],
                       foreground=[('disabled', '#A0A0A0')])
        self.style.configure('TLabelframe', bordercolor='#D0D0D0',
                             relief='groove', padding=10, background='#F3F3F3')
        self.style.configure('TLabelframe.Label', foreground='#0078D4', font=('宋体', 14, 'bold'), background='#F3F3F3')

    # 定义主界面组件
    def create_widgets(self):
        # 定义窗口主容器
        window_frame = ttk.Frame(self.app)
        window_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=15)

        # 定义搜索条件面板
        search_frame = ttk.LabelFrame(window_frame, text=" 搜索条件 ", padding=15)
        search_frame.pack(fill=tk.X, pady=(0, 15))

        # 定义文件名输入框架
        file_frame = ttk.Frame(search_frame)
        file_frame.pack(fill=tk.X, pady=5)
        ttk.Label(file_frame, text="文件名/文件夹名:", font=('宋体', 12), width=16).pack(side=tk.LEFT, padx=5)
        self.name_entry = ttk.Entry(file_frame, font=('宋体', 12))
        self.name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

        # 定义“备注说明”框架
        description_frame = ttk.Frame(search_frame)
        description_frame.pack(fill=tk.X, pady=5)
        description_text = '备注:支持查找目录中的文件或者文件夹,要想查找目录中全部的文件和文件夹请输入“*”,删除文件后不可恢复,建议先备份重要数据。'
        ttk.Label(description_frame, text=description_text, font=('宋体', 10), foreground="#1192f4").pack(side=tk.LEFT, padx=5)

        # 定义目录选择框架
        choose_frame = ttk.Frame(search_frame)
        choose_frame.pack(fill=tk.X, pady=5)
        ttk.Label(choose_frame, text="目录:", font=('宋体', 12), width=8).pack(side=tk.LEFT)
        self.directory_entry = ttk.Entry(choose_frame, font=('宋体', 12))
        self.directory_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Button(choose_frame, text="浏览目录", command=self.browse_directory, width=8).pack(side=tk.LEFT)

        # 创建“开始搜索”按钮
        button_frame = ttk.Frame(window_frame)
        button_frame.pack(fill=tk.X, pady=10)
        self.start_button = ttk.Button(button_frame, text="开始搜索", command=self.get_search,
                                     style='TButton', width=5, padding=(5, 5))
        self.start_button.pack(ipadx=15, padx=(0, 10))

        # 搜索结果列表区
        result_frame = ttk.LabelFrame(window_frame, text=" 搜索结果 ", padding=5)
        result_frame.pack(fill=tk.BOTH, expand=True)

        # 在结果列表上方增加排序按钮
        order_frame  = ttk.Frame(result_frame)
        order_frame .pack(fill=tk.X, pady=5)
        tk.Button(order_frame , text="按名称排序", command=lambda: self.sort_results('name'), width=10, font=('宋体', 11), bg='#0078D4', fg='#FFFFFF').pack(side=tk.LEFT, padx=(0,10))
        tk.Button(order_frame , text="按时间排序", command=lambda: self.sort_results('time'), width=10, font=('宋体', 11), bg='#0078D4', fg='#FFFFFF').pack(side=tk.LEFT)
        tk.Button(order_frame , text="保存以下搜索结果", command=self.save_result, width=15, font=('宋体', 11), bg='#0078D4', fg='#FFFFFF').pack(side=tk.RIGHT)

        # 定义列表控件,用于显示搜索结果
        self.search_result = tk.Listbox(
            result_frame, 
            font=('宋体', 12),
            bg='#FFFFFF',
            relief='flat',
            selectbackground='#0078D4',
            selectforeground='#ffffff',
            activestyle='none'
        )
        self.search_result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 创建“删除”按钮
        delete_frame = ttk.Frame(window_frame)
        delete_frame.pack(fill=tk.X, pady=10)
        self.delete_button = ttk.Button(delete_frame, text="删除以上所有文件", command=self.delete_all_files,
                                     style='TButton', width=12, padding=(5, 5))
        self.delete_button.pack(ipadx=15, padx=(0, 10))

        # 创建滚动条控件
        scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.search_result.configure(yscrollcommand=scrollbar.set)
        scrollbar.config(command=self.search_result.yview)

        # 底部创建状态栏区域
        status_frame = ttk.Frame(self.app, relief='sunken', padding=(10, 5))
        status_frame.pack(side=tk.BOTTOM, fill=tk.X)
        self.state_var = tk.StringVar()
        ttk.Label(status_frame, 
                  textvariable=self.state_var, 
                  font=('宋体', 12), 
                  foreground="#666").pack(side=tk.LEFT)

        # 搜索结果中,鼠标点击事件绑定
        self.search_result.bind("<Button-3>", self.show_context_menu)
        self.search_result.bind("<Double-Button-1>", lambda e: self.open_file())

    # 定义函数,使用多线程处理,避免界面卡死
    def get_search(self):
        directory = self.directory_entry.get()
        file_name = self.name_entry.get()

        # 验证输入的内容是否有效
        if not file_name:
            messagebox.showerror("错误", "请输入你要查找的文件或者文件夹", parent=self.app)
            return

        if not directory or not os.path.isdir(directory):
            messagebox.showerror("错误", "请选择有效的目录")
            return

        self.search_result.delete(0, tk.END)
        self.delete_button.config(state=tk.DISABLED)
        self.state_var.set("搜索中...")

        # 创建搜索线程
        threading.Thread(target=self.main_search, args=(directory, file_name), daemon=True).start()

    # 定义搜索函数
    def main_search(self, directory, file_name):
        try:
            directory = directory.replace("/", "\\")
            file_name = file_name.replace("/", "\\")
            result_files = []
            for app, dirs, files in os.walk(directory):
                for name in files:
                    if fnmatch.fnmatch(name, file_name):
                        result_files.append(os.path.join(app, name))
                for name in dirs:
                    if fnmatch.fnmatch(name, file_name):
                        result_files.append(os.path.join(app, name))
        except Exception as e:
            messagebox.showerror("错误", f"文件搜索失败:{str(e)}")

        if not result_files:
            messagebox.showinfo("提示", "没有找到任何结果")
        self.app.after(0, self.update_results, result_files)

    def update_results(self, files):
        self.delete_button.config(state=tk.NORMAL)
        for file in files:
            self.search_result.insert(tk.END, file)
        self.state_var.set(f"找到 {len(self.search_result.get(0, tk.END))} 个文件")

    def show_context_menu(self, event):
        if self.search_result.curselection():
            self.context_menu.tk_popup(event.x_root, event.y_root)

    def get_selected_file(self):
        selection = self.search_result.curselection()
        if selection:
            return self.search_result.get(selection[0])
        return None

    # 在搜索结果中,双击左鼠标绑定打开文件函数
    def open_file(self):
        file_path = self.get_selected_file()
        if file_path and os.path.isfile(file_path):
            try:
                if platform.system() == "Windows":
                    os.startfile(file_path)
                else:
                    opener = "open" if platform.system() == "Darwin" else "xdg-open"
                    subprocess.call([opener, file_path])
            except Exception as e:
                messagebox.showerror("错误", str(e))
        elif file_path and os.path.isdir(file_path):
            windows_path = file_path.replace("/", "\\")
            subprocess.run(['explorer', windows_path])
        else:
            messagebox.showerror("错误", "这不是一个有效的文件或文件夹")

    # 定义函数,用于选择打开指定目录
    def browse_directory(self):
        directory = filedialog.askdirectory()
        if directory:
            self.directory_entry.delete(0, tk.END)
            self.directory_entry.insert(0, directory)

    # 定义函数,打开文件所在目录
    def open_file_location(self):
        file_path = self.get_selected_file()
        if file_path:
            # 获取文件所在文件夹的路径
            directory = os.path.dirname(file_path)
            # 判断操作系统类型
            if platform.system() == 'Windows':
                # 在Windows系统上使用os.startfile()打开文件夹
                os.startfile(directory)
            elif platform.system() in ['Linux', 'Darwin']:
                # 在Linux或macOS系统上使用subprocess.run()打开文件夹
                subprocess.run(['xdg-open', directory])
            else:
                messagebox.showerror("错误", "不支持该操作系统")

    # 定义函数,在搜索结果中删除指定文件
    def delete_selected_files(self):
        # 获取选中的文件列表
        file_path = self.get_selected_file()
        # 判断如果是文件
        if file_path and os.path.isfile(file_path):
            if messagebox.askyesno("删除", "您确定要删除该文件吗?"):
                try:
                    os.remove(file_path)
                    index = self.search_result.get(0, tk.END).index(file_path)
                    # 从列表中移除文件名以反映更新状态
                    self.search_result.delete(index)
                    self.state_var.set(f"找到 {len(self.search_result.get(0, tk.END))} 个文件")
                except Exception as e:
                    messagebox.showerror("错误", str(e))
        # 判断如果是文件夹
        elif file_path and os.path.isdir(file_path):
            if messagebox.askyesno("删除", "您确定要删除该文件夹吗?"):
                try:
                    # 使用shutil模块的rmtree函数来递归删除指定文件夹及其所有内容
                    shutil.rmtree(file_path)
                    index = self.search_result.get(0, tk.END).index(file_path)
                    # 从列表中移除文件名以反映更新状态
                    self.search_result.delete(index)
                    self.state_var.set(f"找到 {len(self.search_result.get(0, tk.END))} 个文件")
                except Exception as e:
                    messagebox.showerror("错误", str(e))
        else:
            messagebox.showerror("错误", "这不是一个有效的文件或文件夹")

    # 定义函数,删除搜索结果中所有的文件及文件夹
    def delete_all_files(self):
        # 获取搜索结果中所有的文件和文件夹列表
        file_path = self.search_result.get(0, tk.END)
        if len(self.search_result.get(0, tk.END)) > 0:
            if messagebox.askyesno("删除", f"您确定要删除这{len(self.search_result.get(0, tk.END))}个文件或文件夹吗?"):
                total = len(self.search_result.get(0, tk.END))
                # 使用for循环遍历file_path元组
                for result in file_path:
                    # 判断如果是文件
                    if result and os.path.isfile(result):
                        try:
                            os.remove(result)
                            index = self.search_result.get(0, tk.END).index(result)
                            # 从列表中移除文件名以反映更新状态
                            self.search_result.delete(index)
                        except Exception as e:
                            messagebox.showerror("错误", str(e))
                    # 判断如果是文件夹
                    elif result and os.path.isdir(result):
                        try:
                            # 使用shutil模块的rmtree函数来递归删除指定文件夹及其所有内容
                            shutil.rmtree(result)
                            index = self.search_result.get(0, tk.END).index(result)
                            # 从列表中移除文件名以反映更新状态
                            self.search_result.delete(index)
                        except Exception as e:
                            messagebox.showerror("错误", str(e))
                    else:
                        pass
                messagebox.showinfo("提示", f"共删除{total}个文件")
                self.get_search()
                self.state_var.set(f"找到 {len(self.search_result.get(0, tk.END))} 个文件")
        else:
            pass

    # 定义函数,用于排序搜索结果
    def sort_results(self, sort_by):
        # 获取当前列表中的所有文件路径
        all_items = self.search_result.get(0, tk.END)
        if not all_items:
            return
        # 根据名称或者时间排序方式进行排序
        if sort_by == 'name':
            sorted_files = sorted(all_items, key=lambda x: os.path.basename(x).lower())
        elif sort_by == 'time':
            sorted_files = sorted(all_items, key=lambda x: os.path.getmtime(x), reverse=True)
        else:
            return

        # 清空结果,并重新插入排序后的结果
        self.search_result.delete(0, tk.END)
        for file in sorted_files:
            self.search_result.insert(tk.END, file)

    # 保存搜索结果到本地的txt文件内
    def save_result(self):
        # 创建一个文件对话框来选择文件保存的位置
        save_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
        if save_path:
            # 获取搜索结果的内容
            content = self.search_result.get(0, tk.END)
            # 打开文件,用于写入
            with open(save_path, "w") as file:
                for item in content:
                    # 写入搜索结果的每一项,并添加换行符
                    file.write(item + '\n')

    # 关于程序页面
    def program_about(self):
        about = tk.Tk()
        about.title('关于程序')

        # 设置窗口居中
        window_width = 560
        window_height = 450
        screen_width = about.winfo_screenwidth()
        screen_height = about.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        about.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))
        about.resizable(width=False, height=False)

        about_frame = tk.Frame(about, width=560, height=450)
        about_frame.pack()
        tk.Label(about_frame, text='批量文件搜索和删除工具', font=("宋体", 16)).place(x=150, y=20)
        tk.Label(about_frame, text='使用编程语言:Python', font=("宋体", 14)).place(x=50, y=90)
        tk.Label(about_frame, text='搜索方式:支持搜索文件或者文件夹', font=("宋体", 14)).place(x=50, y=150)
        tk.Label(about_frame, text='删除文件:删除后不可恢复,请备份重要数据', font=("宋体", 14)).place(x=50, y=210)
        tk.Label(about_frame, text='搜索全部:输入“*”可以搜索全部的文件和文件夹', font=("宋体", 14)).place(x=50, y=270)
        tk.Label(about_frame, text='智能搜索:输入“*.py”可以搜索全部的py后缀名文件', font=("宋体", 14)).place(x=50, y=330)
        tk.Label(about_frame, text='保存搜索结果方式:保存到txt文件', font=("宋体", 14)).place(x=50, y=390)
        tk.Label(about_frame, text='创作者:www.pyhint.com', font=("宋体", 14)).place(x=50, y=450)
        about.mainloop()

    # 程序讲解页面
    def help_window(self):
        webbrowser.open("https://www.pyhint.com/article/155.html")

# 当前模块直接被执行
if __name__ == "__main__":
    # 创建主窗口
    app = tk.Tk()
    Main(app)
    # 开启主循环,让窗口处于显示状态
    app.mainloop()