tkdu - 用Python重新实现的 ncdu
磁盘空间越来越少,可是不知道空间究竟被谁占了?
在Linux下有一个命令行小工具 ncdu,可以列出目录及其子目录所占磁盘空间的大小。首先,du 是一个古老的 UNIX 命令,可以打印一个目录及其子目录和文件所占磁盘空间的大小,它的名字是 disk usage的首字母缩写。ncdu 则使用了图形化界面,每次只列出一级目录中的内容,使用快捷键在目录树中导航。比起du
将所有的子目录和文件一次性地列出来,ncdu
的界面显得更清晰,而且它可以直接删除选定的条目,而不仅仅是查看。ncdu
的界面是用 ncurses 库实现的,这也是它叫做 ncdu 的原因。
ncdu 有方便的快捷键,可以通过方向键右键进入子目录,左键返回上层目录,按 d 键可以直接删除文件或目录。用起来相当顺手。
可惜 ncdu 这么好的东西在 Windows 平台上并不能运行,它只能在类 UNIX 系统上运行。在Windows 上有一大堆号称能够清理垃圾的“大师”其实本身就是垃圾软件。
某天,我突然想,能不能自己实现呢?于是,下面的山寨仿制品就诞生了。
模仿 ncdu 的命名方式,我把自己的作品命名为 tkdu ,因为它是用 Python 写的,界面库使用 Python 内置的 tk (tkinter)。
tkdu 也是一个命令行程序,这意味着,只能打开黑黑的命令行窗口去运行它,但是它有自己独立的窗口,而不是显示在终端中。基本的语法是:
tkdu [dir]
扫描的目标通过 dir 参数指定, dir 参数是可选的,如果省略,它扫描的就是“当前目录”。
例:
tkdu d:\some\dir
tkdu 并没有完全实现 ncdu,它有一些自己的东西。
键盘命令:
上下键导航
右箭头 或者 回车键 - 进入选定的子目录
左箭头 或者 Esc - 返回上一级目录
d - 删除选定的文件或目录
o - 调用默认的程序打开选定的文件,这是 ncdu 没有的功能
用它来扫描系统盘需要以管理员权限运行,不然很多目录没有权限查看。当然,用它来删除系统盘的文件是比较危险的,你需要自行判断什么能删,什么不能删。不小心删掉了系统文件,大不了重装系统。如果删掉了重要的数据,损失就不好估计了。
下面的exe文件是用 pyinstaller 打包而成的, 扔到 PATH 环境变量中的某个目录中就能从命令行运行了。
https://share.i-code.fun/share/iIkp2qI6
代码如下:
import os import sys import shutil import tkinter as tk from tkinter import ttk from tkinter.messagebox import askyesno import subprocess import platform def open_file(path): if platform.system() == 'Darwin': subprocess.call(('open', path)) elif platform.system() == 'Windows': os.startfile(path) else: subprocess.call(('xdg-open', path)) class FSNode: def __init__(self, path, parent=None): self.path = os.path.abspath(path) self.name = os.path.basename(self.path) self.size = 0 self.parent = parent try: if os.path.isdir(path): self.subdir = [] self.files = [] for child in os.listdir(path): child_node = FSNode(os.path.join(path, child), self) self.insert(child_node) self.size = self.size + child_node.getsize() else: self.size = os.path.getsize(path) except Exception as e: print(path, 'skiped:', e) def getsize(self): return self.size def getpath(self): return self.path def getname(self): return self.name def isdir(self): return hasattr(self, 'subdir') def isfile(self): return not self.isdir() def get_children(self): if self.isdir(): return self.subdir + self.files return None # insert node into the tree def insert(self, node): target = self.files if node.isdir(): target = self.subdir inserted = False # the list is pre sorted for i in range(len(target)): if target[i].size < node.size: target.insert(i, node) inserted = True break if not inserted: target.append(node) # delete the specified node from the tree def remove(self, node): target = self.files if node.isdir(): target = self.subdir self.reduce_parent_size(node) target.remove(node) # recursively update parent's size after deleting the child def reduce_parent_size(self, node): if node.parent: node.parent.size -= node.size self.reduce_parent_size(node.parent) def make_node_title(node, max_size): title = '{size:>11}'.format(size=psize(node.getsize())) scale = 0 if max_size > 0: scale = int(20 * node.size / max_size) progress = ' [' + '#' * scale progress += ' ' * (20 - scale) + '] ' title += progress title += '/' if node.isdir() else ' ' title += node.getname() return title # display size in a human readable format def psize(size): kb = 1024 mb = kb * 1024 gb = mb * 1024 if size > gb: return '%.2f GB' % (size / gb) if size > mb: return '%.2f MB' % (size / mb) if size > kb: return '%.2f KB' % (size / kb) return '%d Byte' % size class App(tk.Tk): def __init__(self, tree): super().__init__() self.tree = tree self.path_label = ttk.Label(self, text=self.tree.getpath()) self.path_label.pack() self.node_list = tk.Listbox(self, font='monospace') self.node_list.pack(fill='both', expand=True, side='left') self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.node_list.yview) self.scrollbar.pack(fill='y', expand=False, side='left') self.node_list['yscrollcommand'] = self.scrollbar.set self.geometry('700x480') self.title('TKDU') self.load_tree() self.node_list.focus() self.bind('q', self.exit) self.node_list.bind('<Return>', self.enter_sub) self.node_list.bind('<Right>', self.enter_sub) self.node_list.bind('<Escape>', self.pre_level) self.node_list.bind('<Left>', self.pre_level) self.node_list.bind('d', self.del_node) self.node_list.bind('o', self.openfile) def exit(self, event): sys.exit() # refresh the Listbox's items from tree def load_tree(self): self.node_list.delete(0, 'end') self.node_list.insert('end', ' ' * 35 + '..') nodes = self.tree.get_children() if nodes: max_size = max(map(lambda n: n.getsize(), nodes)) for node in nodes: title = make_node_title(node, max_size) self.node_list.insert('end', title) self.node_list.select_set(0) self.path_label.config(text=self.tree.getpath()) # return to the previous level directory def pre_level(self, evt=None): if self.tree.parent: self.tree = self.tree.parent self.load_tree() # enter the selected directory def enter_sub(self, event=None): statu, node = self.get_selected() if statu == 'parent': self.pre_level() if statu == 'success' and node.isdir(): self.tree = node self.load_tree() def get_selected(self): selected = self.node_list.curselection() if selected: i = selected[0] if i == 0: node = self.tree.parent return ('parent', node) if i > 0: node_list = self.tree.get_children() return ('success', node_list[i-1]) return ('failed', None) def openfile(self, event=None): statu,node = self.get_selected() if statu == 'success' and node.isfile(): open_file(node.path) # delete selected node (and file|directory) def del_node(self, event=None): statu, node = self.get_selected() if statu == 'success': answer = askyesno(title='Warning', message='"{path}" \nwill be deleted! Are you sure?'.format(path=node.getpath())) if answer: try: if node.isdir(): shutil.rmtree(node.getpath()) else: os.remove(node.getpath()) self.tree.remove(node) self.load_tree() except Exception as e: print('delete {path} failed'.format(path=node.getpath()), e) if __name__ == '__main__': if len(sys.argv) < 2: p = os.getcwd() else: p = sys.argv[1] tree = FSNode(p) app = App(tree) app.mainloop()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?