Files
Messy-Little-Gadgets/directory_tree_analyzer.py
T

211 lines
8.4 KiB
Python

# -*- 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()