diff --git a/README.md b/README.md index 4406ce8..e7fbe35 100644 --- a/README.md +++ b/README.md @@ -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,20 +65,19 @@ 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_sync.py # Directory sync tool -├── music_downloader.py # Music download GUI tool -├── argon2_password_hasher.py # Argon2 password hasher -└── README.md # This document +├── 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 +└── README.md # This document ``` --- diff --git a/directory_tree_analyzer.py b/directory_tree_analyzer.py new file mode 100644 index 0000000..e282583 --- /dev/null +++ b/directory_tree_analyzer.py @@ -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() diff --git a/port_checker.py b/port_checker.py new file mode 100644 index 0000000..b1ac568 --- /dev/null +++ b/port_checker.py @@ -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() diff --git a/resource_monitor.py b/resource_monitor.py new file mode 100644 index 0000000..856bdc8 --- /dev/null +++ b/resource_monitor.py @@ -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()