背景

近期电脑一个盘亮红了,需要找出那个文件或文件夹吃了空间。手动找太麻烦了,就用python写个文件夹大小扫描工具。

image-20250415111853297.png

程序界面展示

image-20250415112114475.png

双击可打开文件夹!!

程序功能说明

  1. 文件/文件夹标识

    • 在树状视图中添加了"类型"列,清晰地标识每个项目是"文件"还是"文件夹"
    • 使用 item.is_dir() 方法判断项目类型
    • item_types 字典中存储每个树项目的类型信息
  2. 基本扫描功能

    • 扫描指定文件夹下所有文件和文件夹的大小
    • 显示每个项目的路径、大小和占总大小的百分比
    • 按大小降序排列结果
  3. 用户界面

    • 路径输入框和浏览按钮
    • 扫描进度条
    • 状态栏显示当前操作状态
    • 树状视图显示扫描结果
  4. 交互功能

    • 右键菜单提供"打开"、"打开所在文件夹"和"复制路径"功能
    • 双击项目可直接打开
    • 跨平台支持(Windows、macOS、Linux)
  5. 错误处理

    • 处理权限错误和IO错误
    • 文件操作失败时显示错误消息

使用这个工具,用户可以轻松地:

  • 分析文件夹中哪些文件/子文件夹占用空间最大
  • 快速访问这些文件/文件夹
  • 复制路径用于其他操作

这个工具在管理磁盘空间、清理大文件时特别有用。

打包程序

为了方便其他用户使用,使用pyinstaller将程序打包成可执行文件。

pyinstaller -F -w 扫描磁盘工具.py

程序源码

import os
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
import threading
from pathlib import Path
import humanize  # 用于格式化文件大小
import subprocess
import platform
import pyperclip  # 用于复制到剪贴板


class FolderScannerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("文件夹大小扫描工具")
        self.root.geometry("1000x600")
        self.root.minsize(1000, 600)

        # 创建主框架
        self.main_frame = ttk.Frame(self.root, padding=10)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建顶部控制区域
        self.control_frame = ttk.Frame(self.main_frame)
        self.control_frame.pack(fill=tk.X, pady=5)

        # 路径输入框
        self.path_var = tk.StringVar()
        ttk.Label(self.control_frame, text="文件夹路径:").pack(side=tk.LEFT, padx=5)
        self.path_entry = ttk.Entry(self.control_frame, textvariable=self.path_var, width=50)
        self.path_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

        # 浏览按钮
        self.browse_btn = ttk.Button(self.control_frame, text="浏览", command=self.browse_folder)
        self.browse_btn.pack(side=tk.LEFT, padx=5)

        # 扫描按钮
        self.scan_btn = ttk.Button(self.control_frame, text="扫描", command=self.start_scan)
        self.scan_btn.pack(side=tk.LEFT, padx=5)

        # 创建进度条
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(self.main_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=5)

        # 创建状态标签
        self.status_var = tk.StringVar(value="就绪")
        self.status_label = ttk.Label(self.main_frame, textvariable=self.status_var)
        self.status_label.pack(anchor=tk.W, pady=5)

        # 创建结果显示区域
        self.result_frame = ttk.Frame(self.main_frame)
        self.result_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        # 创建树状视图 - 添加了type列显示文件类型
        self.tree = ttk.Treeview(self.result_frame, columns=("type", "path", "size", "percentage"), show="headings")
        self.tree.heading("type", text="类型")
        self.tree.heading("path", text="路径")
        self.tree.heading("size", text="大小")
        self.tree.heading("percentage", text="占比")
        self.tree.column("type", width=80, anchor=tk.CENTER)
        self.tree.column("path", width=500, anchor=tk.W)
        self.tree.column("size", width=100, anchor=tk.E)
        self.tree.column("percentage", width=100, anchor=tk.CENTER)

        # 添加滚动条
        self.scrollbar_y = ttk.Scrollbar(self.result_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=self.scrollbar_y.set)

        self.scrollbar_x = ttk.Scrollbar(self.result_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
        self.tree.configure(xscrollcommand=self.scrollbar_x.set)

        # 布局树状视图和滚动条
        self.scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        self.scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)

        # 创建右键菜单
        self.context_menu = tk.Menu(self.tree, tearoff=0)
        self.context_menu.add_command(label="打开", command=self.open_item)
        self.context_menu.add_command(label="打开所在文件夹", command=self.open_containing_folder)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="复制路径", command=self.copy_path)

        # 绑定右键点击事件
        self.tree.bind("<Button-3>", self.show_context_menu)
        # 绑定双击事件
        self.tree.bind("<Double-1>", lambda event: self.open_item())

        # 扫描线程
        self.scan_thread = None
        # 存储项目路径的字典
        self.item_paths = {}
        # 存储项目类型的字典
        self.item_types = {}

    def browse_folder(self):
        folder_path = filedialog.askdirectory()
        if folder_path:
            self.path_var.set(folder_path)

    def start_scan(self):
        folder_path = self.path_var.get()
        if not folder_path or not os.path.isdir(folder_path):
            self.status_var.set("错误:请选择有效的文件夹路径")
            return

        # 清空树状视图和路径字典
        for item in self.tree.get_children():
            self.tree.delete(item)
        self.item_paths = {}
        self.item_types = {}

        # 禁用扫描按钮
        self.scan_btn.configure(state=tk.DISABLED)
        self.browse_btn.configure(state=tk.DISABLED)

        # 更新状态
        self.status_var.set("正在扫描...")

        # 启动扫描线程
        self.scan_thread = threading.Thread(target=self.scan_folder, args=(folder_path,))
        self.scan_thread.daemon = True
        self.scan_thread.start()

    def scan_folder(self, folder_path):
        try:
            # 获取文件夹下所有项目
            items = list(os.scandir(folder_path))
            total_items = len(items)

            # 计算总大小
            total_size = 0
            item_data = []

            for i, item in enumerate(items):
                # 获取项目大小
                size = self.get_item_size(item)
                # 确定项目类型
                item_type = "文件夹" if item.is_dir() else "文件"
                item_data.append((item, size, item_type))
                total_size += size

                # 更新进度
                progress = (i + 1) / total_items * 100
                self.progress_var.set(progress)
                self.root.update_idletasks()

            # 处理每个项目
            results = []
            for item, size, item_type in item_data:
                percentage = (size / total_size * 100) if total_size > 0 else 0
                results.append((item.path, item.name, size, percentage, item_type))

            # 按大小排序
            results.sort(key=lambda x: x[2], reverse=True)

            # 更新UI
            self.root.after(0, lambda: self.update_results(results, total_size, folder_path))

        except Exception as e:
            self.root.after(0, lambda: self.status_var.set(f"错误:{str(e)}"))
        finally:
            self.root.after(0, self.reset_ui)

    def get_item_size(self, item):
        """获取文件或文件夹的大小"""
        try:
            if item.is_file():
                return item.stat().st_size
            elif item.is_dir():
                return sum(f.stat().st_size for f in Path(item.path).glob('**/*') if f.is_file())
            return 0
        except (PermissionError, OSError):
            # 处理权限错误或其他IO错误
            return 0

    def update_results(self, results, total_size, base_folder):
        # 添加总大小信息
        item_id = self.tree.insert("", tk.END, values=(
            "文件夹",
            base_folder,
            humanize.naturalsize(total_size, binary=True),
            "100%"
        ))
        self.item_paths[item_id] = base_folder
        self.item_types[item_id] = "文件夹"

        # 添加每个项目
        for full_path, name, size, percentage, item_type in results:
            item_id = self.tree.insert("", tk.END, values=(
                item_type,
                full_path,
                humanize.naturalsize(size, binary=True),
                f"{percentage:.2f}%"
            ))
            self.item_paths[item_id] = full_path
            self.item_types[item_id] = item_type

            # 更新状态
            self.status_var.set(
                f"扫描完成,共 {len(results)} 个项目,总大小 {humanize.naturalsize(total_size, binary=True)}")

    def get_selected_path(self):
        """获取当前选中项目的路径"""
        selected = self.tree.selection()
        if not selected:
            return None

        item_id = selected[0]
        if item_id in self.item_paths:
            return self.item_paths[item_id]
        return None

    def reset_ui(self):
        # 启用扫描按钮
        self.scan_btn.configure(state=tk.NORMAL)
        self.browse_btn.configure(state=tk.NORMAL)
        # 重置进度条
        self.progress_var.set(0)

    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def open_item(self):
        """打开所选项目"""
        path = self.get_selected_path()
        if not path:
            return

        try:
            if platform.system() == "Windows":
                os.startfile(path)
            elif platform.system() == "Darwin":  # macOS
                subprocess.run(["open", path])
            else:  # Linux
                subprocess.run(["xdg-open", path])
        except Exception as e:
            messagebox.showerror("错误", f"无法打开文件: {str(e)}")

    def open_containing_folder(self):
        """打开所选项目所在的文件夹"""
        path = self.get_selected_path()
        if not path:
            return

        # 如果是文件,获取其所在目录
        if os.path.isfile(path):
            dir_path = os.path.dirname(path)
            try:
                if platform.system() == "Windows":
                    # 在Windows上,我们可以选中文件
                    subprocess.run(["explorer", "/select,", path])
                    return
                elif platform.system() == "Darwin":  # macOS
                    # 在macOS上,我们可以显示文件
                    subprocess.run(["open", "-R", path])
                    return
            except Exception:
                pass  # 如果特定方法失败,回退到打开文件夹

            path = dir_path

        try:
            if platform.system() == "Windows":
                os.startfile(path)
            elif platform.system() == "Darwin":  # macOS
                subprocess.run(["open", path])
            else:  # Linux
                subprocess.run(["xdg-open", path])
        except Exception as e:
            messagebox.showerror("错误", f"无法打开文件夹: {str(e)}")

    def copy_path(self):
        """复制所选项目的路径到剪贴板"""
        path = self.get_selected_path()
        if path:
            pyperclip.copy(path)
            self.status_var.set(f"已复制路径: {path}")

if __name__ == "__main__":
    # 安装依赖:pip install humanize pyperclip
    try:
        import humanize
        import pyperclip
    except ImportError:
        print("请先安装所需库: pip install humanize pyperclip")
        import sys
        sys.exit(1)

    root = tk.Tk()
    app = FolderScannerApp(root)
    root.mainloop()

关注@运维躬行录,在聊天框输入【文件大小扫描】,可免费领取打包文件及《python自动化办公教程》祝你效率翻倍

标签: none