-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmacclean.py
More file actions
executable file
·323 lines (269 loc) · 11.3 KB
/
macclean.py
File metadata and controls
executable file
·323 lines (269 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import plistlib
import re
import time
import subprocess
import shutil
# ANSI 颜色标记
C_RED = '\033[91m'
C_GREEN = '\033[92m'
C_YELLOW = '\033[93m'
C_BLUE = '\033[94m'
C_MAGENTA = '\033[95m'
C_CYAN = '\033[96m'
C_RESET = '\033[0m'
TARGET_DIRS = [
"~/Library/Application Support",
"~/Library/Caches",
"~/Library/Preferences",
"~/Library/Containers",
"~/Library/Group Containers",
"~/Library/Logs",
"~/Library/HTTPStorages",
"~/Library/Saved Application State",
"~/Library/WebKit",
"~/Library/LaunchAgents"
]
EXACT_MATCH_WHITELIST = {
'apple', 'syncservices', 'default.store', 'default.store-shm', 'default.store-wal',
'corepatch', 'askpermissiond', 'familycircled', 'sharedfilelistd', 'loginwindow',
'mobilemeaccounts', '.globalpreferences_m', 'scopedbookmarkagent', 'minilauncher',
'no_backup', 'mbuseragent', 'icloudmailagent', 'chrome_crashpad_handler', 'animoji'
}
PREFIX_WHITELIST = ('com.apple.', 'group.com.apple.', 'groups.com.apple.', 'com.apple-')
VENDOR_SAFELIST = ('google', 'microsoft', 'adobe', 'jetbrains', 'tencent', 'netease', 'alipay')
# 3天的时间锁参数
TIME_LOCK_SECONDS = 3 * 24 * 3600
CURRENT_TIME = time.time()
def expand_path(p):
return os.path.expanduser(p)
def build_app_index():
installed_bundles = set()
installed_names = set()
app_dirs = ['/Applications', expand_path('~/Applications')]
for adir in app_dirs:
if not os.path.exists(adir):
continue
for root, dirs, files in os.walk(adir, topdown=True):
# 仅在第一层和普通文件夹中寻找 .app,不深入 .app 内部
dirs_to_visit = []
for d in dirs:
if d.endswith('.app'):
app_path = os.path.join(root, d)
app_name = d[:-4].lower()
installed_names.add(app_name)
plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
if os.path.exists(plist_path):
try:
with open(plist_path, 'rb') as f:
plist = plistlib.load(f)
bid = plist.get('CFBundleIdentifier')
if bid and isinstance(bid, str):
installed_bundles.add(bid.lower())
except Exception:
pass
elif not d.endswith('.bundle') and not d.endswith('.framework'):
dirs_to_visit.append(d)
dirs[:] = dirs_to_visit
return installed_bundles, installed_names
def is_whitelisted(original_name):
lower_name = original_name.lower()
# 1. 精确名称拦截
exact_name = lower_name.replace('.plist', '').replace('.savedstate', '')
if exact_name in EXACT_MATCH_WHITELIST:
return True
# 2. 系统前缀拦截
if original_name.startswith(PREFIX_WHITELIST) or original_name == '.DS_Store':
return True
# 3. 厂商共享拦截
for vendor in VENDOR_SAFELIST:
if vendor in lower_name:
return True
return False
def clean_name(name):
# 正则剥离头部的 10位开发者证书ID
name = re.sub(r'^[a-zA-Z0-9]{10}\.', '', name)
# 移除分组前缀
name = name.replace("group.", "").replace("groups.", "")
# 清理常见后缀以增加匹配命中率
name = name.replace('.plist', '').replace('.savedState', '')
return name.lower()
def is_installed_residual(cleaned_name, installed_bundles, installed_names):
# 支持前缀和后缀包含比对
for ib in installed_bundles:
if ib in cleaned_name or cleaned_name in ib:
return True
for iname in installed_names:
if iname in cleaned_name or cleaned_name in iname:
return True
return False
def get_latest_mtime(path):
latest = os.path.getmtime(path)
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
for e in dirs + files:
try:
mtime = os.path.getmtime(os.path.join(root, e))
if mtime > latest:
latest = mtime
except Exception:
pass
return latest
def get_size_kb_du(path):
try:
out = subprocess.check_output(['du', '-sk', path], stderr=subprocess.DEVNULL)
return int(out.decode('utf-8').split()[0])
except Exception:
return 0
def move_to_trash(path):
trash_dir = expand_path("~/.Trash")
if not os.path.exists(trash_dir):
os.makedirs(trash_dir)
base_name = os.path.basename(path)
dest = os.path.join(trash_dir, base_name)
counter = 1
# 处理重名冲突
while os.path.exists(dest):
name, ext = os.path.splitext(base_name)
dest = os.path.join(trash_dir, f"{name}_{counter}{ext}")
counter += 1
shutil.move(path, dest)
def format_size(kb):
mb = kb / 1024.0
return f"{mb:.2f} MB"
def main():
print(f"{C_CYAN}=================================={C_RESET}")
print(f"{C_CYAN} MacClean v7 终极版 {C_RESET}")
print(f"{C_CYAN}=================================={C_RESET}\n")
print(f"{C_YELLOW}[*] 正在扫描已安装应用程序指纹...{C_RESET}")
installed_bundles, installed_names = build_app_index()
print(f"[*] 成功提取了 {len(installed_bundles)} 个强指纹和 {len(installed_names)} 个应用名称。")
print(f"\n{C_YELLOW}[*] 正在扫描目标核心目录寻找残留...{C_RESET}")
residuals = {}
for tdir in TARGET_DIRS:
full_dir = expand_path(tdir)
group_name = os.path.basename(tdir)
if not os.path.exists(full_dir):
continue
is_privilege_dir = group_name in ['LaunchAgents', 'Preferences', 'Saved Application State']
residuals[group_name] = []
for item in os.listdir(full_dir):
item_path = os.path.join(full_dir, item)
# 第一层: 绝对白名单拦截
if is_whitelisted(item):
continue
# 第二层: TeamID 清洗并进行指纹比对
cname = clean_name(item)
if is_installed_residual(cname, installed_bundles, installed_names):
continue
# 第三层: 深入递归获取最新 mtime 激活时间锁 (3 Days)
try:
latest_mtime = get_latest_mtime(item_path)
except OSError:
continue
if (CURRENT_TIME - latest_mtime) < TIME_LOCK_SECONDS:
continue
# 第四层: 真实体积过滤
size_kb = get_size_kb_du(item_path)
if not is_privilege_dir and size_kb <= 50:
continue
residuals[group_name].append({
'name': item,
'path': item_path,
'size_kb': size_kb,
'size_mb': size_kb / 1024.0
})
# 全局按照 TARGET_DIRS 目录顺序排序输出
sorted_groups = []
for tdir in TARGET_DIRS:
gname = os.path.basename(tdir)
if gname in residuals and residuals[gname]:
# 分组内按照 size_mb 降序排列
residuals[gname].sort(key=lambda x: x['size_mb'], reverse=True)
sorted_groups.append(gname)
if not sorted_groups:
print(f"\n{C_GREEN}太棒了!没有发现任何残留文件,你的 Mac 非常干净。{C_RESET}")
return
global_items = []
item_index = 1
print(f"\n{C_MAGENTA}========= 深度扫描完成!发现以下疑似残留 ========={C_RESET}\n")
for gname in sorted_groups:
items = residuals[gname]
total_kb = sum(i['size_kb'] for i in items)
group_title = f"[{gname}]"
# 特殊高亮 LaunchAgents
if gname == 'LaunchAgents':
group_title = f"⚠️ {group_title}"
print(f"{C_CYAN}{group_title} - {len(items)} 个对象, 共 {format_size(total_kb)}{C_RESET}")
for item in items:
item['global_id'] = item_index
global_items.append(item)
color = C_RED if gname == 'LaunchAgents' else C_RESET
print(f" {color}{item_index:>3}. {item['name']} ({format_size(item['size_kb'])}){C_RESET}")
item_index += 1
print("")
while True:
print(f"{C_YELLOW}=============== 请选择操作 ==============={C_RESET}")
print(f" {C_GREEN}[a]{C_RESET} 全选所有垃圾")
print(f" {C_GREEN}[g]{C_RESET} 按组名批量选中 (例如: g Caches)")
print(f" {C_GREEN}[n]{C_RESET} 自由输入编号多选 (例如: 1 3 5)")
print(f" {C_GREEN}[q]{C_RESET} 退出程序")
choice = input(f"\n{C_GREEN}>>> {C_RESET}").strip()
if not choice:
continue
if choice.lower() == 'q':
print("取消清理工作,退出。")
break
to_delete = []
if choice.lower() == 'a':
to_delete = global_items
elif choice.lower().startswith('g '):
group_target = choice[2:].strip().lower()
for item in global_items:
parent_dir = os.path.basename(os.path.dirname(item['path'])).lower()
if parent_dir == group_target:
to_delete.append(item)
else:
# 解析以空格或逗号分隔的数字
parts = choice.replace(',', ' ').split()
valid = True
for p in parts:
if not p.isdigit():
valid = False
break
idx = int(p)
found = next((i for i in global_items if i['global_id'] == idx), None)
if found:
to_delete.append(found)
if not valid and not to_delete:
print(f"{C_RED}包含无效输入,请重新输入编号。{C_RESET}\n")
continue
if not to_delete:
print(f"{C_RED}未能匹配到记录,请检查输入。{C_RESET}\n")
continue
print(f"\n{C_RED}您即将删除以下文件/目录:{C_RESET}")
for d in to_delete:
print(f" - {d['name']}")
confirm = input(f"\n{C_YELLOW}谨慎确认: 上述文件将被移至废纸篓? (y/N): {C_RESET}")
if confirm.lower() == 'y':
success_count = 0
for d in to_delete:
try:
move_to_trash(d['path'])
success_count += 1
except Exception as e:
print(f"{C_RED}操作失败: {d['path']} | 错误详情: {e}{C_RESET}")
print(f"\n{C_GREEN}清理完成!成功安全抹除 {success_count} 个文件/目录对象并释放磁盘。{C_RESET}")
break
else:
print("已取消本次删除操作。\n")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\n已强制中止扫描流程。")
finally:
input(f"\n{C_CYAN}按回车键退出...{C_RESET}")