首页未分类一个python做的进程占用查找器

一个python做的进程占用查找器

分类未分类时间2026-08-21 21:59:17发布鼠王啊烈浏览6
摘要:---前言你有没有过这样的经历:想要删除文件,但是电脑总显示 `在另一个程序中打开`...


---


前言

你有没有过这样的经历:想要删除文件,但是电脑总显示 `在另一个程序中打开`


---

**文件占用管理器** 是一款基于 Python + Tkinter 的图形化工具,用于快速查看哪些进程正在占用指定文件,并允许用户**优雅终止**或**强制结束**这些进程。  

它特别适合解决 Windows 用户 `文件正在使用,无法删除/移动/修改` 的常见问题,比系统自带的任务管理器更直观、更聚焦。

代码展示



python
import psutil
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import queue
import sys

# 尝试导入拖放支持
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
    HAS_DND = True
except ImportError:
    HAS_DND = False
    print("提示:安装 tkinterdnd2 可启用拖放功能 (pip install tkinterdnd2)")


class FileLockerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("文件占用管理器")
        self.root.geometry("650x480")
        
        self.is_scanning = False
        self.scan_queue = queue.Queue()
        self.total_scanned = 0  # 用于调试计数
        
        self.create_widgets()
        self._set_buttons_state(True)
        
        if HAS_DND:
            self._setup_drag_drop()
    
    def _setup_drag_drop(self):
        self.root.drop_target_register(DND_FILES)
        self.root.dnd_bind('<<Drop>>', self.on_drop)
        self.entry_path.drop_target_register(DND_FILES)
        self.entry_path.dnd_bind('<<Drop>>', self.on_drop)
        self.status_var.set("就绪 — 可将文件拖入窗口自动扫描")
    
    def on_drop(self, event):
        files = event.data
        if files.startswith('{') and files.endswith('}'):
            files = files[1:-1]
        file_list = files.splitlines()
        if len(file_list) == 1 and ' ' in file_list[0] and not os.path.exists(file_list[0]):
            file_list = file_list[0].split()
        file_list = [f.strip() for f in file_list if f.strip()]
        if not file_list:
            return
        first_file = file_list[0].strip('"').strip("'")
        if os.path.exists(first_file):
            self.file_path.set(first_file)
            self.scan_processes()
        else:
            messagebox.showwarning("无效文件", f"无法识别文件路径:\n{first_file}")
    
    def create_widgets(self):
        frame_file = ttk.LabelFrame(self.root, text="选择文件", padding=10)
        frame_file.pack(fill="x", padx=10, pady=5)
        
        self.file_path = tk.StringVar()
        self.file_path.trace_add('write', self._on_path_change)
        
        self.entry_path = ttk.Entry(frame_file, textvariable=self.file_path, width=50)
        self.entry_path.pack(side="left", padx=5)
        
        btn_browse = ttk.Button(frame_file, text="浏览...", command=self.browse_file)
        btn_browse.pack(side="left")
        
        frame_process = ttk.LabelFrame(self.root, text="占用进程列表", padding=10)
        frame_process.pack(fill="both", expand=True, padx=10, pady=5)
        
        tree_frame = ttk.Frame(frame_process)
        tree_frame.pack(fill="both", expand=True)
        
        self.tree = ttk.Treeview(tree_frame, columns=("pid", "name"), show="headings", height=12)
        self.tree.heading("pid", text="PID")
        self.tree.heading("name", text="进程名称")
        self.tree.column("pid", width=80, anchor="center")
        self.tree.column("name", width=250)
        
        scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        self.tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        frame_actions = ttk.Frame(self.root)
        frame_actions.pack(fill="x", padx=10, pady=5)
        
        row1 = ttk.Frame(frame_actions)
        row1.pack(fill="x", pady=2)
        self.btn_scan = ttk.Button(row1, text="扫描占用", command=self.scan_processes)
        self.btn_scan.pack(side="left", padx=5)
        self.btn_clear = ttk.Button(row1, text="清空列表", command=self.clear_list)
        self.btn_clear.pack(side="left", padx=5)
        ttk.Separator(row1, orient="vertical").pack(side="left", fill="y", padx=10)
        
        row2 = ttk.Frame(frame_actions)
        row2.pack(fill="x", pady=2)
        self.btn_kill = ttk.Button(row2, text="结束进程 (优雅)", command=self.kill_processes)
        self.btn_kill.pack(side="left", padx=5)
        self.btn_force = ttk.Button(row2, text="强制结束进程", command=self.force_kill_processes)
        self.btn_force.pack(side="left", padx=5)
        
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief="sunken", anchor="w")
        status_bar.pack(fill="x", padx=10, pady=(5, 10))
    
    def _on_path_change(self, *args):
        self.clear_list()
    
    def _set_buttons_state(self, enabled):
        state = "normal" if enabled else "disabled"
        self.btn_scan.config(state=state)
        self.btn_kill.config(state=state)
        self.btn_force.config(state=state)
        self.btn_clear.config(state=state)
    
    def browse_file(self):
        file = filedialog.askopenfilename()
        if file:
            self.file_path.set(file)
            self.scan_processes()
    
    def scan_processes(self):
        if self.is_scanning:
            return
        
        file_path = self.file_path.get().strip()
        if not file_path:
            messagebox.showwarning("警告", "请先选择文件!")
            return
        
        try:
            abs_path = os.path.realpath(os.path.normpath(file_path))
        except Exception:
            abs_path = file_path
        
        if not os.path.exists(abs_path):
            messagebox.showerror("错误", f"文件不存在:\n{abs_path}")
            return
        
        self.clear_list()
        self.status_var.set("正在扫描进程(后台线程),请稍候...")
        self._set_buttons_state(False)
        self.is_scanning = True
        self.total_scanned = 0
        
        thread = threading.Thread(target=self._scan_thread, args=(abs_path,), daemon=True)
        thread.start()
        self._check_scan_complete()
    
    def _scan_thread(self, target_path):
        results = []
        found_pids = set()
        use_samefile = True  # 先尝试 samefile
        count = 0
        try:
            procs = psutil.process_iter(attrs=['pid', 'name', 'open_files'])
            for proc in procs:
                count += 1
                # 每 100 个进程更新一次状态(通过队列)
                if count % 100 == 0:
                    self.scan_queue.put(('progress', f"已扫描 {count} 个进程"))
                
                try:
                    open_files = proc.info.get('open_files')
                    if not open_files:
                        continue
                    for file_obj in open_files:
                        try:
                            if use_samefile:
                                # 使用 samefile 比较
                                match = os.path.samefile(file_obj.path, target_path)
                            else:
                                # 回退到规范化字符串比较(忽略大小写)
                                norm_file = os.path.normpath(os.path.realpath(file_obj.path))
                                norm_target = os.path.normpath(target_path)
                                # Windows 下忽略大小写
                                if sys.platform == 'win32':
                                    match = norm_file.lower() == norm_target.lower()
                                else:
                                    match = norm_file == norm_target
                            
                            if match:
                                pid = proc.info['pid']
                                if pid not in found_pids:
                                    found_pids.add(pid)
                                    results.append((pid, proc.info['name']))
                                break  # 该进程已匹配,无需继续检查其他文件
                        except (OSError, ValueError) as e:
                            # samefile 失败,切换为字符串比较
                            if use_samefile:
                                print(f"[调试] samefile 失败,切换为字符串比较:{e}")
                                use_samefile = False
                            # 字符串比较已在 else 中处理,但这里因为 use_samefile 变为 False,下次循环会走 else
                            # 但当前这一次无法重试,所以直接再比较一次(通过递归调用自身?不,简单起见:再试一次字符串比较)
                            if not use_samefile:
                                # 立即用字符串比较
                                norm_file = os.path.normpath(os.path.realpath(file_obj.path))
                                norm_target = os.path.normpath(target_path)
                                if sys.platform == 'win32':
                                    match = norm_file.lower() == norm_target.lower()
                                else:
                                    match = norm_file == norm_target
                                if match:
                                    pid = proc.info['pid']
                                    if pid not in found_pids:
                                        found_pids.add(pid)
                                        results.append((pid, proc.info['name']))
                                    break
                            continue
                except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                    continue
                except Exception:
                    continue
        except Exception as e:
            self.scan_queue.put(('error', str(e)))
            return
        
        # 将总扫描进程数也传递
        self.scan_queue.put(('result', results, count))
    
    def _check_scan_complete(self):
        try:
            item = self.scan_queue.get_nowait()
            if item[0] == 'result':
                results = item[1]
                total_count = item[2]
                for pid, name in results:
                    self.tree.insert("", "end", values=(pid, name))
                if results:
                    self.status_var.set(f"扫描完成,共找到 {len(results)} 个进程(共扫描 {total_count} 个进程)")
                    first = self.tree.get_children()
                    if first:
                        self.tree.selection_set(first[0])
                        self.tree.focus(first[0])
                else:
                    self.status_var.set(f"未找到占用进程(共扫描 {total_count} 个进程)")
                    messagebox.showinfo("提示", 
                        "未扫描到任何进程占用该文件。\n"
                        "可能原因:\n"
                        "1. 文件确实未被任何程序打开。\n"
                        "2. 文件被系统进程占用,需要管理员权限才能查看。\n"
                        "3. 路径比较失败,已尝试 samefile 和字符串比较。\n"
                        "4. 请以管理员身份重新运行此程序。")
                self.is_scanning = False
                self._set_buttons_state(True)
            elif item[0] == 'progress':
                self.status_var.set(item[1])
                # 继续等待
                if self.is_scanning:
                    self.root.after(100, self._check_scan_complete)
                return
            elif item[0] == 'error':
                messagebox.showerror("扫描错误", f"扫描过程中发生异常:\n{item[1]}")
                self.status_var.set("扫描出错")
                self.is_scanning = False
                self._set_buttons_state(True)
        except queue.Empty:
            if self.is_scanning:
                self.root.after(100, self._check_scan_complete)
            return
        except Exception as e:
            self.status_var.set(f"检查线程异常:{e}")
            self.is_scanning = False
            self._set_buttons_state(True)
    
    def kill_processes(self):
        self._kill_selected(force=False)
    
    def force_kill_processes(self):
        self._kill_selected(force=True)
    
    def _kill_selected(self, force):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择要结束的进程!")
            return
        
        action = "强制终止" if force else "终止"
        if not messagebox.askyesno("确认", f"确定要{action}选中的 {len(selected)} 个进程吗?\n该操作不可撤销!"):
            return
        
        success_count = 0
        for item in selected:
            pid = int(self.tree.item(item, "values")[0])
            try:
                proc = psutil.Process(pid)
                if force:
                    proc.kill()
                else:
                    proc.terminate()
                    gone, alive = psutil.wait_procs([proc], timeout=3)
                    if alive:
                        for p in alive:
                            p.kill()
                self.tree.delete(item)
                success_count += 1
            except psutil.NoSuchProcess:
                self.tree.delete(item)
                success_count += 1
            except Exception as e:
                messagebox.showerror("错误", f"{action}进程 PID={pid} 失败:\n{e}")
        
        self.status_var.set(f"成功{action} {success_count} 个进程")
    
    def clear_list(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
        self.status_var.set("列表已清空")


if __name__ == "__main__":
    if HAS_DND:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()
    app = FileLockerApp(root)
    root.mainloop()

使用方法

先安装 Python(一定要勾选添加到 PATH 变量选项,若不勾选,请自行添加),

然后打开 `CMD` 或 `PowerShell`

运行:

bash
pip install psutil tkinterdnd2

如果下载卡顿、报错,请尝试换源,详见 [pip 换源](https://www.runoob.com/w3cnote/pip-cn-mirror.html)。


最后,运行程序,会显示一个名为 `文件占用管理器` 的窗口。

# 使用教程

---

![运行后出现的图片](https://i-blog.csdnimg.cn/direct/ff4ed1ccf8744f8ca3dbc3dafb429212.png#pic_center)

`选择文件`里可以选择文件路径,也可以直接拖动文件到窗口内任意位置以选择要扫描的文件

选择文件后将会自动开始扫描,等待一会后就会给出结果。

扫描时,左下角四个按钮都会变成灰色,无法点击。

若没有开始扫描,可以手动点击 `扫描占用` 按钮。


---

扫描到后,在占用进程列表里选择进程,点击 `结束进程(优雅)` 或 `强制结束进程` 即可。

注:优雅结束是正常结束,如还不行,就使用强制结束。


---

最后

请尽量不要结束系统进程!

请尽量不要结束系统进程!

请尽量不要结束系统进程!

(若自行结束系统进程,后果自负!!!)


鼠王啊烈的个人博客版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

python