# -*- coding: utf-8 -*- """ 目录树分析器 v1.0 功能:指定目录查看详细的目录树信息和统计信息 - 递归目录树展示 - 文件/目录数量统计 - 大小分布分析(按扩展名、按层级、按大小区间) - Top N 大文件 - 深层目录 """ import os import sys from collections import defaultdict from datetime import datetime # ============ 配置 ============ MAX_TREE_DEPTH = 8 # 目录树显示最大深度 MAX_TREE_ITEMS = 5000 # 目录树显示最大条目数(防止爆炸) SHOW_TREE = True # 是否显示目录树 DEFAULT_IGNORE = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.idea', '.vscode'} # ============ 颜色 ============ class C: DIR = '\033[1;34m' # 目录 - 蓝 FILE = '\033[0m' # 文件 - 默认 SIZE = '\033[0;36m' # 大小 - 青 HDR = '\033[1;33m' # 标题 - 黄 WARN = '\033[1;31m' # 警告 - 红 OK = '\033[1;32m' # 成功 - 绿 END = '\033[0m' def human_size(n): for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if n < 1024.0: return f"{n:.1f} {unit}" if unit != 'B' else f"{int(n)} B" n /= 1024.0 return f"{n:.1f} PB" def fmt_num(n): return f"{n:,}" class DirAnalyzer: def __init__(self, root, ignore=None): self.root = os.path.abspath(root) self.ignore = DEFAULT_IGNORE | (ignore or set()) self.total_files = 0 self.total_dirs = 0 self.total_size = 0 self.ext_sizes = defaultdict(lambda: [0, 0]) # ext -> [count, size] self.level_stats = defaultdict(lambda: [0, 0]) # depth -> [count, size] self.size_buckets = defaultdict(int) # size 区间 self.large_files = [] # (size, path) self.deep_dirs = [] # (depth, path) self.max_depth = 0 self.max_width_dir = (0, '') # 最多子项的目录 self.errors = 0 self.dir_children = defaultdict(int) # dir -> 子项数 self.tree_lines = [] self.scanned = 0 # ---- 大小区间 ---- def bucket(self, size): if size < 1*1024: return "<1KB" if size < 10*1024: return "1-10KB" if size < 100*1024: return "10-100KB" if size < 1*1024*1024: return "100KB-1MB" if size < 10*1024*1024: return "1-10MB" if size < 100*1024*1024: return "10-100MB" if size < 1*1024*1024*1024: return "100MB-1GB" return ">1GB" def scan(self): self.tree_lines.append(f"{C.HDR}{self.root}{C.END}") self._walk(self.root, 0) return self def _walk(self, path, depth): self.scanned += 1 try: entries = os.listdir(path) except (PermissionError, OSError) as e: self.errors += 1 self.tree_lines.append(" "*depth + f"{C.WARN}[权限拒绝] {os.path.basename(path)}{C.END}") return self.dir_children[path] = len(entries) if self.dir_children[path] > self.max_width_dir[0]: self.max_width_dir = (self.dir_children[path], path) show_children = (len(self.tree_lines) < MAX_TREE_ITEMS and depth < MAX_TREE_DEPTH) for name in entries: if name in self.ignore: continue full = os.path.join(path, name) try: if os.path.isdir(full): self.total_dirs += 1 self.level_stats[depth+1][0] += 1 if depth+1 > self.max_depth: self.max_depth = depth+1 self.deep_dirs.append((depth+1, full)) if show_children: self.tree_lines.append(" "*depth + f"{C.DIR}├─ {name}/ {C.END}") self._walk(full, depth+1) else: size = os.path.getsize(full) self.total_files += 1 self.total_size += size ext = os.path.splitext(name)[1].lower() or "(no-ext)" self.ext_sizes[ext][0] += 1 self.ext_sizes[ext][1] += size self.level_stats[depth+1][1] += size self.size_buckets[self.bucket(size)] += 1 self.large_files.append((size, full)) if show_children: self.tree_lines.append(" "*depth + f"{C.FILE}├─ {name} {C.SIZE}({human_size(size)}){C.END}") except (PermissionError, OSError): self.errors += 1 # ---- 报表 ---- def summary(self): print(f"\n{'='*60}") print(f"{C.HDR}📊 目录分析报告{C.END}") print(f"{'='*60}") print(f"扫描目录 : {self.root}") print(f"扫描时间 : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"{'='*60}") print(f"{C.OK}总文件数 : {fmt_num(self.total_files)} 个{C.END}") print(f"{C.DIR}总目录数 : {fmt_num(self.total_dirs)} 个{C.END}") print(f"{C.SIZE}总大小 : {human_size(self.total_size)} ({fmt_num(self.total_size)} B){C.END}") print(f"最大深度 : {self.max_depth} 层") print(f"权限错误 : {self.errors} 处") if self.max_width_dir[0] > 0: print(f"\n[C] 子项最多的目录: {self.max_width_dir[0]} 个 → {self.max_width_dir[1]}") def report_ext(self): if not self.ext_sizes: return print(f"\n{'─'*60}\n{C.HDR}📁 扩展名统计 (Top 15){C.END}") sorted_ext = sorted(self.ext_sizes.items(), key=lambda x: -x[1][1]) print(f"{'扩展名':<15}{'数量':>10}{'总大小':>12}{'占比':>8}") print('─'*60) for ext, (cnt, sz) in sorted_ext[:15]: pct = sz / self.total_size * 100 if self.total_size else 0 print(f"{ext:<15}{cnt:>10,}{human_size(sz):>12}{pct:>7.1f}%") def report_levels(self): print(f"\n{'─'*60}\n{C.HDR}📂 层级分布 (深度: 文件/大小){C.END}") for depth in sorted(self.level_stats): cnt, sz = self.level_stats[depth] if cnt == 0 and sz == 0: continue bar = '█' * min(int(sz / (self.total_size/60 + 1)), 60) if self.total_size else '' print(f"深度{depth:<3} {cnt:>8,} 文件 {human_size(sz):>12} {C.SIZE}{bar}{C.END}") def report_buckets(self): print(f"\n{'─'*60}\n{C.HDR}📦 文件大小分布{C.END}") order = ["<1KB","1-10KB","10-100KB","100KB-1MB","1-10MB","10-100MB","100MB-1GB",">1GB"] for b in order: cnt = self.size_buckets.get(b, 0) bar = '█' * min(int(cnt / (self.total_files/50 + 1)), 50) if self.total_files else '' print(f"{b:<12} {cnt:>8,} 个 {C.OK}{bar}{C.END}") def report_large(self, n=15): print(f"\n{'─'*60}\n{C.HDR}🐘 Top {n} 大文件{C.END}") self.large_files.sort(reverse=True) for i, (sz, path) in enumerate(self.large_files[:n], 1): print(f"{i:>2}. {human_size(sz):>12} {path}") def report_deep_dirs(self, n=10): print(f"\n{'─'*60}\n{C.HDR}🕳️ 最深目录 (Top {n}){C.END}") self.deep_dirs.sort(key=lambda x: -x[0]) for i, (depth, path) in enumerate(self.deep_dirs[:n], 1): print(f"{i:>2}. 深度{depth:<3} {C.DIR}{path}{C.END}") def print_tree(self): if SHOW_TREE: print(f"\n{'─'*60}\n{C.HDR}🌳 目录树{C.END}") for line in self.tree_lines[:MAX_TREE_ITEMS]: print(line) if len(self.tree_lines) >= MAX_TREE_ITEMS: print(f"{C.WARN}... (目录树已截断){C.END}") def main(): target = input("请输入要分析的目录(. 表示当前目录): ").strip() or "." if not os.path.exists(target): print(f"{C.WARN}❌ 路径不存在: {target}{C.END}") return if not os.path.isdir(target): print(f"{C.WARN}❌ 不是目录: {target}{C.END}") return print(f"{C.OK}⏳ 正在扫描 {target} ...{C.END}") analyzer = DirAnalyzer(target) analyzer.scan() if SHOW_TREE: analyzer.print_tree() analyzer.summary() analyzer.report_ext() analyzer.report_levels() analyzer.report_buckets() analyzer.report_large() analyzer.report_deep_dirs() print(f"\n{C.OK}✅ 分析完成! 共扫描 {fmt_num(analyzer.scanned)} 个条目{C.END}") if __name__ == "__main__": main()