Add directory_tree_analyzer, resource_monitor, port_checker (v1.2.0)

This commit is contained in:
dvs
2026-08-28 11:53:37 +08:00
parent 14402061b5
commit 7119d11687
4 changed files with 595 additions and 20 deletions
+39 -16
View File
@@ -8,6 +8,42 @@
## 🧩 Included Tools
### 🌳 Directory Tree Analyzer — `directory_tree_analyzer.py`
Analyze a directory with a detailed tree view and rich statistics.
- 🌳 Recursive colored directory tree
- 📊 File/dir count, total size
- 📁 Extension stats (Top 15 with size & percentage)
- 📂 Depth distribution (per-level bar charts)
- 📦 Size bucket distribution (<1KB ~ >1GB)
- 🐘 Top 15 largest files
- 🕳️ Deepest directories
```bash
python directory_tree_analyzer.py
```
### 📊 Resource Monitor — `resource_monitor.py`
Monitor the **Top 10 memory & CPU** consumers, refreshing every 30 seconds.
- 🔝 Memory Top 10 & CPU Top 10 (process / PID / usage / bar)
- 💾 Total memory, CPU usage, core counts
- 🎨 Color-coded usage bars
```bash
python resource_monitor.py
```
### 🔍 Port Checker — `port_checker.py`
Check whether a port is occupied, find the occupying process, and optionally force-kill it.
- 🛡️ UAC elevation (run as admin)
- 🔍 Detect port occupancy (bulk query: `80, 443, 8000-8010`)
- 🕵️ Find occupying process (name / PID / command line)
- 💀 Force-kill the process (with confirmation)
- 🚫 System-critical process protection
```bash
python port_checker.py
```
### 🔄 Directory Sync — `directory_sync.py`
A universal directory synchronization tool.
- 🔁 **One-way / Two-way** sync modes
@@ -15,14 +51,6 @@ A universal directory synchronization tool.
- ⏱️ **Periodic** checking (default 5s)
- ⚙️ **Manual input or env vars** configuration (`DIRSYNC_SRC/DST/MODE/INTERVAL`)
```bash
# One-way sync (via env vars)
set DIRSYNC_SRC=D:/src
set DIRSYNC_DST=D:/dst
set DIRSYNC_MODE=oneway
python directory_sync.py
```
### 🎵 Music Downloader — `music_downloader.py`
A simple Tkinter GUI tool to search and download music from an online source.
- 🔍 **Search** songs by keyword
@@ -30,10 +58,6 @@ A simple Tkinter GUI tool to search and download music from an online source.
- 🔥 **Hot songs** list browsing
- 📂 **Custom save path** via file dialog
```bash
python music_downloader.py
```
### 🔐 Argon2 Password Hasher — `argon2_password_hasher.py`
A secure password hashing utility using **Argon2id** (2025 recommended parameters).
- 🧂 **Auto salt** generation
@@ -41,16 +65,15 @@ A secure password hashing utility using **Argon2id** (2025 recommended parameter
- ✔️ **Verify** passwords
- 🔄 **Rehash check** for parameter updates
```bash
python argon2_password_hasher.py
```
---
## 📁 Project Structure
```
Messy-Little-Gadgets/
├── directory_tree_analyzer.py # Directory tree analyzer
├── resource_monitor.py # Memory/CPU Top 10 monitor
├── port_checker.py # Port occupancy checker
├── directory_sync.py # Directory sync tool
├── music_downloader.py # Music download GUI tool
├── argon2_password_hasher.py # Argon2 password hasher
+210
View File
@@ -0,0 +1,210 @@
# -*- 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()
+206
View File
@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
"""
端口占用检测器 v1.0
功能:
1. 先UAC提权到管理员
2. 检测用户输入指定端口是否被占用
3. 如果被占用,找出是哪个进程占用
4. 允许用户强制杀死该进程
5. 支持批量查询(逗号/空格/范围分隔)
"""
import os
import sys
import ctypes
import subprocess
import re
# ============ UAC 提权 ============
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except Exception:
return False
def request_admin():
"""请求 UAC 提权,以管理员身份重新运行"""
if not is_admin():
print("[!] 需要管理员权限,正在请求 UAC 提权...")
# 重新以管理员运行自身
params = ' '.join([f'"{a}"' for a in sys.argv])
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, f'"{sys.argv[0]}" {params}', None, 1)
sys.exit(0)
OK = '\033[1;32m'
WARN = '\033[1;33m'
RED = '\033[1;31m'
HDR = '\033[1;36m'
END = '\033[0m'
def parse_input(s):
"""解析端口输入:支持 '80, 443, 8000-8010, 22' 等格式"""
ports = set()
s = s.replace(',', ',')
for part in s.replace(',', ' ').split():
part = part.strip()
if not part:
continue
if '-' in part: # 范围
a, b = part.split('-')
try:
a, b = int(a), int(b)
for p in range(a, b + 1):
ports.add(p)
except ValueError:
continue
else: # 单个端口
try:
ports.add(int(part))
except ValueError:
pass
return sorted(ports)
def get_netstat():
"""获取所有 TCP 监听/连接状态的端口→PID 映射"""
# 使用 netstat 获取
result = subprocess.run(
['netstat', '-ano'], capture_output=True, text=True, errors='ignore')
port_pid = {} # 端口 -> (状态, pid)
for line in result.stdout.splitlines():
line = line.strip()
# 匹配形如 TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 1234
m = re.match(r'^\s*(TCP|UDP)\s+(\S+):(\d+)\s+(\S+)\s+(\w+)\s*(\d*)\s*$', line)
if m:
proto, local, port, remote, state, pid = m.groups()
try:
port = int(port)
except ValueError:
continue
# 只关心监听和被占用状态
if 'LISTEN' in state.upper() or (remote and remote.split(':')[-1] != '0'):
port_pid[port] = (state, pid)
return port_pid
def get_process_name(pid):
"""通过 tasklist 获取进程名"""
try:
result = subprocess.run(
['tasklist', '/FI', f'PID eq {pid}'], capture_output=True, text=True, errors='ignore')
for line in result.stdout.splitlines():
if pid in line and '.exe' in line.lower():
return line.split()[0]
except Exception:
pass
return '未知'
def get_process_detail(pid):
"""获取进程详细信息(命令行等)"""
try:
result = subprocess.run(
['wmic', 'process', 'where', f'ProcessId={pid}', 'get', 'Name,CommandLine', '/format:list'],
capture_output=True, text=True, errors='ignore')
name = cmd = ''
for line in result.stdout.splitlines():
if line.startswith('Name='):
name = line.split('=', 1)[1].strip()
elif line.startswith('CommandLine='):
cmd = line.split('=', 1)[1].strip()[:120]
return name or get_process_name(pid), cmd
except Exception:
return get_process_name(pid), ''
def kill_process(pid):
"""强制杀死进程"""
result = subprocess.run(['taskkill', '/F', '/PID', str(pid)],
capture_output=True, text=True)
if result.returncode == 0:
return True, "已强制终止"
return False, result.stderr.strip() or "终止失败(可能无权限或进程已退出)"
def check_port(port, port_pid):
"""检查单个端口"""
print(f"\n{'─'*60}")
print(f"{HDR}📡 端口 {port}{END}")
if port < 0 or port > 65535:
print(f"{RED}❌ 无效端口范围 (0-65535){END}")
return
if port not in port_pid:
print(f"{OK}✅ 端口 {port} 未被占用{END}")
return
state, pid = port_pid[port]
pid = pid.strip()
name = get_process_name(pid)
cmd = ""
try:
n2, cmd = get_process_detail(pid)
if n2:
name = n2
except Exception:
pass
print(f"{RED}⚠️ 端口 {port} 已被占用!{END}")
print(f" 占用进程: {name}")
print(f" PID : {pid}")
print(f" 状态 : {state}")
if cmd:
print(f" 命令行 : {cmd}")
if name.lower() in ('system', 'idle', 'svchost.exe') :
print(f"{WARN}⚠️ 这是系统关键进程,不建议强制终止!{END}")
return
# 询问是否杀死
kill_choice = input(f"\n是否强制终止进程 {name} (PID {pid})? [y/N]: ").strip().lower()
if kill_choice in ('y', 'yes'):
ok, msg = kill_process(pid)
if ok:
print(f"{OK}✅ {msg}: {name} (PID {pid}){END}")
else:
print(f"{RED}❌ {msg}{END}")
def main():
# ===== UAC 提权 =====
if os.name == 'nt':
request_admin()
print('=' * 60)
print(f"{HDR}🔍 端口占用检测器 v1.0{END}")
print('=' * 60)
print("支持批量查询: 逗号/空格分隔,支持范围 如 '80, 443, 8000-8010'")
while True:
inp = input("\n请输入要查询的端口 (输入 q 退出): ").strip()
if inp.lower() in ('q', 'quit', 'exit'):
break
if not inp:
continue
ports = parse_input(inp)
if not ports:
print(f"{WARN}⚠️ 无法解析端口输入,请检查格式{END}")
continue
if len(ports) > 50:
print(f"{WARN}⚠️ 单次最多查询 50 个端口{END}")
continue
print(f"{OK}⏳ 正在扫描 {len(ports)} 个端口...{END}")
port_pid = get_netstat()
for port in ports:
check_port(port, port_pid)
print("\n👋 已退出")
if __name__ == "__main__":
main()
+136
View File
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""
资源大户监控器 v1.0
功能:查询内存里面 Top10 的内存大户和 CPU 大户,30s 刷新
- 实时显示内存 Top10 和 CPU Top10
- 每 30 秒自动刷新
- 显示进程名、PID、内存占用、CPU 占用
"""
import os
import time
import sys
try:
import psutil
except ImportError:
print("需要安装 psutil: pip install psutil")
sys.exit(1)
REFRESH = 30 # 刷新间隔(秒)
TOP_N = 10
OK = '\033[1;32m'
WARN = '\033[1;33m'
RED = '\033[1;31m'
HDR = '\033[1;36m'
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 get_top(by='memory'):
"""按内存或CPU取Top N进程"""
procs = []
for p in psutil.process_iter(['pid', 'name', 'memory_percent', 'cpu_percent', 'memory_info']):
try:
procs.append(p.info)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
if by == 'memory':
procs.sort(key=lambda x: x['memory_percent'] or 0, reverse=True)
else:
procs.sort(key=lambda x: x['cpu_percent'] or 0, reverse=True)
return procs[:TOP_N]
def clear():
os.system('cls' if os.name == 'nt' else 'clear')
def show_header():
print('=' * 78)
print(f"{HDR}📊 资源大户监控器 (每30秒自动刷新, Ctrl+C 退出){END}")
print('=' * 78)
vm = psutil.virtual_memory()
try:
cpu = psutil.cpu_percent(interval=None)
except Exception:
cpu = 0
cores = psutil.cpu_count(logical=False)
lcores = psutil.cpu_count()
print(f"💾 总内存: {human_size(vm.total)} | 已用: {human_size(vm.used)} "
f"({vm.percent}%) | 可用: {human_size(vm.available)}")
print(f"⚡ CPU: {cpu}% | 物理核心: {cores} | 逻辑核心: {lcores}")
print('=' * 78)
def show_top(data, by):
if by == 'memory':
label = "内存"
else:
label = "CPU"
print('-' * 78)
print(f"{HDR}🔝 Top{TOP_N} {label}大户{END}")
print('-' * 78)
print(f"{'#':<4}{'进程名':<30}{'PID':<8}{'占用':<14}{'占比':>8}")
print('-' * 78)
for i, d in enumerate(data, 1):
name = (d.get('name') or '?')[:28]
pid = d.get('pid', '?')
if by == 'memory':
mem_b = (d.get('memory_info') or {}).rss or 0
disp = human_size(mem_b)
pct = d.get('memory_percent') or 0
else:
val = d.get('cpu_percent') or 0
disp = f"{val:.1f}%"
pct = val
color = OK if pct < 30 else (WARN if pct < 60 else RED)
bar = '█' * min(int(pct / 2), 20)
print(f"{i:<4}{name:<30}{pid:<8}{disp:<14} {color}{bar}{END}")
def warm_cpu():
for p in psutil.process_iter(['cpu_percent']):
try:
p.cpu_percent(None)
except Exception:
pass
def main():
# 预热 cpu_percent
warm_cpu()
time.sleep(0.5)
try:
while True:
clear()
show_header()
mem_top = get_top('memory')
cpu_top = get_top('cpu')
show_top(mem_top, 'memory')
show_top(cpu_top, 'cpu')
now = time.strftime('%H:%M:%S')
print('=' * 78)
print(f" 最后刷新: {now} 下次刷新: {REFRESH} 秒后...")
try:
time.sleep(REFRESH)
except KeyboardInterrupt:
break
warm_cpu()
except KeyboardInterrupt:
pass
print(f"\n{'='*78}")
print("👋 已退出监控")
if __name__ == "__main__":
main()