吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2474|回复: 47
收起左侧

[Windows] 文件夹查重工具

  [复制链接]
MXDZRB 发表于 2025-4-29 08:58
本帖最后由 MXDZRB 于 2025-4-29 08:59 编辑

[Python] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
import os
import collections
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox
from tkinter import ttk # 引入ttk
 
def find_duplicate_filenames(directory):
    """查找指定目录及其子目录中重复的文件名,并返回包含重复文件信息的字典。"""
    file_paths = collections.defaultdict(list)
    try:
        for root, _, files in os.walk(directory):
            for filename in files:
                full_path = os.path.join(root, filename)
                # 规范化路径以处理大小写不敏感的文件系统(如Windows)
                file_paths[filename.lower()].append(full_path)
    except Exception as e:
        messagebox.showerror("错误", f"扫描过程中发生错误: {e}")
        return None
 
    # 过滤出实际重复的文件(基于小写文件名)
    duplicates_data = {filename: paths for filename, paths in file_paths.items() if len(paths) > 1}
 
    # 重新组织数据,以原始文件名(保留大小写)为键,路径列表为值
    duplicates_final = collections.defaultdict(list)
    original_filenames = {} # 存储小写文件名到第一个遇到的原始文件名的映射
    for root, _, files in os.walk(directory):
        for filename in files:
            lower_filename = filename.lower()
            if lower_filename in duplicates_data:
                if lower_filename not in original_filenames:
                    original_filenames[lower_filename] = filename
                # 确保只添加属于重复项列表的路径
                full_path = os.path.join(root, filename)
                if full_path in duplicates_data[lower_filename]:
                     duplicates_final[original_filenames[lower_filename]].append(full_path)
 
    # 确保每个文件名下的路径列表长度大于1
    duplicates_final = {filename: paths for filename, paths in duplicates_final.items() if len(paths) > 1}
 
    return duplicates_final
 
def browse_directory():
    """打开文件夹选择对话框并更新路径输入框。"""
    directory = filedialog.askdirectory()
    if directory:
        entry_path.delete(0, tk.END)
        entry_path.insert(0, directory)
 
# 全局变量存储复选框和对应的文件路径
checkbox_vars = {}
checkbox_widgets = {}
# 全局变量存储选择模式(默认为删除模式)
selection_mode = "delete"
 
def toggle_selection_mode():
    """切换选择模式(保留/删除)并更新所有复选框的状态。"""
    global selection_mode
     
    # 切换模式
    if selection_mode == "delete":
        selection_mode = "keep"
        btn_toggle_mode.config(text="当前模式:保留所选(点击切换)")
    else:
        selection_mode = "delete"
        btn_toggle_mode.config(text="当前模式:删除所选(点击切换)")
     
    # 重新应用选择逻辑到当前显示的所有复选框
    apply_selection_to_checkboxes()
 
def apply_selection_to_checkboxes():
    """根据当前选择模式,自动选择或取消选择复选框。"""
    # 按文件名分组整理路径
    paths_by_filename = collections.defaultdict(list)
    for path in checkbox_vars.keys():
        filename = os.path.basename(path)
        paths_by_filename[filename].append(path)
     
    # 对每组重复文件应用选择逻辑
    for filename, paths in paths_by_filename.items():
        if len(paths) <= 1# 跳过非重复文件
            continue
             
        # 根据模式选择要保留或删除的文件
        if selection_mode == "delete":
            # 删除模式:选中除第一个外的所有文件(默认保留第一个)
            for i, path in enumerate(paths):
                checkbox_vars[path].set(i > 0# 第一个设为False,其余为True
        else:
            # 保留模式:只选中第一个文件(默认删除其余文件)
            for i, path in enumerate(paths):
                checkbox_vars[path].set(i == 0# 第一个设为True,其余为False
 
def start_scan():
    """开始扫描选定的文件夹并显示结果。"""
    global checkbox_vars, checkbox_widgets
    target_directory = entry_path.get()
    if not target_directory or target_directory == "(当前脚本目录)":
        target_directory = os.path.dirname(os.path.abspath(__file__))
        entry_path.delete(0, tk.END)
        entry_path.insert(0, target_directory) # 显示实际路径
 
    if not os.path.isdir(target_directory):
        messagebox.showerror("错误", f"指定的路径 '{target_directory}' 不是一个有效的文件夹。")
        return
 
    # 清空旧的结果和复选框
    for widget in result_frame.winfo_children():
        widget.destroy()
    checkbox_vars.clear()
    checkbox_widgets.clear()
    btn_delete.pack_forget() # 隐藏删除按钮
    btn_toggle_mode.pack_forget() # 隐藏模式切换按钮
 
    status_label.config(text=f"开始扫描文件夹: {target_directory}\n请稍候...")
    app.update_idletasks() # 强制更新UI
 
    duplicates = find_duplicate_filenames(target_directory)
 
    status_label.config(text="扫描完成。")
 
    if duplicates is None: # 扫描出错
        return
 
    if duplicates:
        row_num = 0
        for filename, paths in duplicates.items():
            # 显示文件名标签
            lbl_filename = ttk.Label(result_frame, text=f"文件名: {filename}", style='Header.TLabel') # 使用ttk Label和新样式
            lbl_filename.grid(row=row_num, column=0, columnspan=2, sticky='w', padx=5, pady=(8, 2))
            row_num += 1
            # 为每个路径创建复选框,确保文本不会被截断
            for path in paths:
                var = tk.BooleanVar()
                # 创建一个Frame来容纳复选框,确保它可以水平扩展
                path_frame = ttk.Frame(result_frame)
                path_frame.grid(row=row_num, column=0, columnspan=2, sticky='w', padx=20, pady=1)
                 
                # 在Frame中创建复选框,设置wraplength=0确保不换行
                cb = ttk.Checkbutton(path_frame, text=path, variable=var, style='TCheckbutton')
                cb.pack(side=tk.LEFT, anchor='w')
                 
                checkbox_vars[path] = var
                checkbox_widgets[path] = cb
                row_num += 1
                 
        # 自动选择除第一个外的所有重复文件
        apply_selection_to_checkboxes()
         
        # 显示模式切换按钮
        btn_toggle_mode.config(text="当前模式:删除所选(点击切换)" if selection_mode == "delete" else "当前模式:保留所选(点击切换)")
        btn_toggle_mode.pack(side=tk.BOTTOM, pady=(0, 5))
         
        # 将删除按钮放在主窗口底部,而不是滚动区域内
        btn_delete.pack(side=tk.BOTTOM, pady=(5, 10))
    else:
        lbl_no_duplicates = ttk.Label(result_frame, text="未找到重复的文件名。")
        lbl_no_duplicates.grid(row=0, column=0, padx=5, pady=5)
 
def delete_selected_files():
    """根据当前模式删除或保留选中的文件。"""
    selected_files = [path for path, var in checkbox_vars.items() if var.get()]
 
    if not selected_files:
        messagebox.showinfo("提示", "请先选择要处理的文件。")
        return
         
    # 根据当前模式显示不同的确认信息
    if selection_mode == "delete":
        confirm_message = f"确定要删除选中的 {len(selected_files)} 个文件吗?\n此操作不可恢复!"
    else# keep模式
        # 计算将被删除的文件数量(未选中的文件)
        files_to_delete = [path for path, var in checkbox_vars.items() if not var.get()]
        confirm_message = f"确定要保留选中的 {len(selected_files)} 个文件,删除其余 {len(files_to_delete)} 个文件吗?\n此操作不可恢复!"
     
    confirm = messagebox.askyesno("确认操作", confirm_message)
 
    if confirm:
        # 在保留模式下,实际要删除的是未选中的文件
        if selection_mode == "keep":
            selected_files = [path for path, var in checkbox_vars.items() if not var.get()]
        deleted_count = 0
        failed_count = 0
        errors = []
        remaining_duplicates = collections.defaultdict(list)
        all_paths_after_delete = set(checkbox_vars.keys())
 
        for file_path in selected_files:
            try:
                os.remove(file_path)
                deleted_count += 1
                # 从界面和数据中移除
                checkbox_widgets[file_path].destroy()
                del checkbox_widgets[file_path]
                del checkbox_vars[file_path]
                all_paths_after_delete.remove(file_path)
            except Exception as e:
                failed_count += 1
                errors.append(f"删除 {file_path} 失败: {e}")
                # 保留删除失败的文件信息
                remaining_duplicates[os.path.basename(file_path)].append(file_path)
 
        # 更新剩余文件列表
        for path in list(all_paths_after_delete): # 使用副本迭代,因为可能修改
            if path in checkbox_vars: # 检查是否已被删除
                 remaining_duplicates[os.path.basename(path)].append(path)
 
        # 清理界面并重新显示剩余的重复项
        for widget in result_frame.winfo_children():
            widget.destroy()
 
        if remaining_duplicates:
            row_num = 0
            # 过滤掉只剩一个文件的组
            final_remaining = {name: paths for name, paths in remaining_duplicates.items() if len(paths) > 1}
            if final_remaining:
                for filename, paths in final_remaining.items():
                    lbl_filename = ttk.Label(result_frame, text=f"文件名: {filename}", style='Header.TLabel') # 使用ttk Label
                    lbl_filename.grid(row=row_num, column=0, columnspan=2, sticky='w', padx=5, pady=(8, 2))
                    row_num += 1
                    for path in paths:
                        # 重新创建剩余文件的复选框,确保文本不会被截断
                        var = tk.BooleanVar()
                        # 创建一个Frame来容纳复选框,确保它可以水平扩展
                        path_frame = ttk.Frame(result_frame)
                        path_frame.grid(row=row_num, column=0, columnspan=2, sticky='w', padx=20, pady=1)
                         
                        # 在Frame中创建复选框,确保文本完整显示
                        cb = ttk.Checkbutton(path_frame, text=path, variable=var, style='TCheckbutton')
                        cb.pack(side=tk.LEFT, anchor='w')
                         
                        checkbox_vars[path] = var # 更新 checkbox_vars
                        checkbox_widgets[path] = cb # 更新 checkbox_widgets
                        row_num += 1
            else:
                 lbl_no_duplicates = ttk.Label(result_frame, text="所有选中的重复文件已被处理或不再重复。")
                 lbl_no_duplicates.grid(row=0, column=0, padx=5, pady=5)
                 btn_delete.pack_forget() # 没有重复项了,隐藏删除按钮
        else:
            lbl_no_duplicates = ttk.Label(result_frame, text="所有选中的重复文件已被处理。")
            lbl_no_duplicates.grid(row=0, column=0, padx=5, pady=5)
            btn_delete.pack_forget() # 没有重复项了,隐藏删除按钮
 
        # 显示删除结果
        result_message = f"删除完成:成功 {deleted_count} 个"
        if failed_count > 0:
            result_message += f",失败 {failed_count} 个。\n失败详情:\n" + "\n".join(errors)
            messagebox.showerror("删除失败", result_message)
        else:
            messagebox.showinfo("删除成功", result_message)
        status_label.config(text="删除操作完成。")
 
# --- GUI 设置 ---
app = tk.Tk()
app.title("查找并删除重复文件名工具")
app.geometry("700x550") # 调整窗口大小
app.configure(bg='#f0f0f0') # 设置窗口背景色
 
# 配置ttk样式
style = ttk.Style(app)
style.theme_use('clam') # 使用一个现代主题
 
style.configure('TFrame', background='#f0f0f0')
style.configure('TLabel', background='#f0f0f0', font=('Microsoft YaHei UI', 10))
style.configure('TButton', font=('Microsoft YaHei UI', 10), padding=5)
style.configure('TEntry', font=('Microsoft YaHei UI', 10), padding=5)
style.configure('Header.TLabel', font=('Microsoft YaHei UI', 11, 'bold')) # 文件名标签样式
style.configure('TCheckbutton', background='#f0f0f0', font=('Microsoft YaHei UI', 9))
# 配置水平滚动条样式,使其更加明显
style.configure('Horizontal.TScrollbar', gripcount=0, background='#c1c1c1', troughcolor='#f0f0f0', borderwidth=1, arrowsize=13)
 
# 文件夹路径输入
frame_input = ttk.Frame(app, padding="10 10 10 5") # 使用ttk Frame并增加内边距
frame_input.pack(fill=tk.X)
 
lbl_path = ttk.Label(frame_input, text="扫描文件夹:")
lbl_path.pack(side=tk.LEFT, padx=(0, 5))
 
entry_path = ttk.Entry(frame_input) # 使用ttk Entry
entry_path.pack(side=tk.LEFT, expand=True, fill=tk.X)
# 设置默认提示文本 (如果为空)
entry_path.insert(0, "(当前脚本目录)")
entry_path.config(foreground='grey')
def on_entry_click(event):
    if entry_path.get() == "(当前脚本目录)":
       entry_path.delete(0, "end") # 删除提示文本
       entry_path.insert(0, '') # 插入空字符串
       entry_path.config(foreground='black')
def on_focusout(event):
    if entry_path.get() == '':
        entry_path.insert(0, "(当前脚本目录)")
        entry_path.config(foreground='grey')
entry_path.bind('<FocusIn>', on_entry_click)
entry_path.bind('<FocusOut>', on_focusout)
 
btn_browse = ttk.Button(frame_input, text="浏览...", command=browse_directory, style='TButton') # 使用ttk Button
btn_browse.pack(side=tk.LEFT, padx=(5, 0))
 
# 扫描按钮
btn_scan = ttk.Button(app, text="开始扫描", command=start_scan, style='TButton') # 使用ttk Button
btn_scan.pack(pady=5)
 
# 状态标签
status_label = ttk.Label(app, text="请选择要扫描的文件夹。\n提示:按住Shift键+鼠标滚轮可以左右滚动查看完整路径。", wraplength=680, justify=tk.LEFT) # 使用ttk Label
status_label.pack(pady=(0, 5), padx=10, fill=tk.X)
 
# 模式切换按钮(初始隐藏)
btn_toggle_mode = ttk.Button(app, text="当前模式:删除所选(点击切换)", command=toggle_selection_mode, style='TButton') # 使用ttk Button
# btn_toggle_mode 将在找到重复项后通过 pack() 显示
 
# 结果显示区域 (使用Canvas和Frame实现滚动,支持垂直和水平滚动)
# 创建一个Frame作为滚动区域的容器
scroll_container = ttk.Frame(app)
scroll_container.pack(fill="both", expand=True, padx=10, pady=(0, 5))
 
# 创建Canvas和滚动条
result_canvas = tk.Canvas(scroll_container, borderwidth=0, background="#ffffff")
result_frame = ttk.Frame(result_canvas, style='TFrame') # 结果放在ttk Frame中
 
# 垂直滚动条
vsb = ttk.Scrollbar(scroll_container, orient="vertical", command=result_canvas.yview)
# 水平滚动条
hsb = ttk.Scrollbar(scroll_container, orient="horizontal", command=result_canvas.xview, style="Horizontal.TScrollbar")
 
# 配置Canvas的滚动命令
result_canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
 
# 放置滚动条和Canvas
vsb.pack(side="right", fill="y")
hsb.pack(side="bottom", fill="x")
result_canvas.pack(side="left", fill="both", expand=True)
 
# 创建窗口,并确保它可以水平扩展
result_canvas.create_window((4,4), window=result_frame, anchor="nw", tags="result_frame")
 
 
# 检测操作系统类型
import platform
system = platform.system().lower()
 
# 绑定鼠标滚轮事件
# 定义滚轮事件处理函数
def _on_mousewheel(event):
    # Windows平台的鼠标滚轮事件处理
    # 调整滚动速度,使其更加平滑
    # 确保滚动量适中,不会太快或太慢
    scroll_amount = int(-1*(event.delta/120))
    # 默认垂直滚动
    result_canvas.yview_scroll(scroll_amount, "units")
     
    # 按住Shift键时进行水平滚动
    if event.state & 0x1: # Shift键被按下
        result_canvas.xview_scroll(scroll_amount, "units")
 
def _on_mousewheel_linux(event):
    # Linux平台的鼠标滚轮事件处理
    if event.num == 4# 向上滚动
        if event.state & 0x1: # Shift键被按下
            result_canvas.xview_scroll(-1, "units") # 水平滚动
        else:
            result_canvas.yview_scroll(-1, "units") # 垂直滚动
    elif event.num == 5# 向下滚动
        if event.state & 0x1: # Shift键被按下
            result_canvas.xview_scroll(1, "units") # 水平滚动
        else:
            result_canvas.yview_scroll(1, "units") # 垂直滚动
 
# 为macOS定义滚轮事件处理函数
if system == 'darwin':
    def _on_mousewheel_macos(event):
        if event.state & 0x1: # Shift键被按下
            result_canvas.xview_scroll(int(-1*event.delta), "units") # 水平滚动
        else:
            result_canvas.yview_scroll(int(-1*event.delta), "units") # 垂直滚动
 
# 绑定和解绑鼠标滚轮事件的函数
def _bind_to_mousewheel(event=None):
    # 绑定滚轮事件到整个应用程序
    if system == 'windows':
        app.bind_all("<MouseWheel>", _on_mousewheel)
    elif system == 'darwin'# macOS
        app.bind_all("<MouseWheel>", _on_mousewheel_macos)
    else# Linux和其他平台
        app.bind_all("<Button-4>", _on_mousewheel_linux)
        app.bind_all("<Button-5>", _on_mousewheel_linux)
 
def _unbind_from_mousewheel(event=None):
    # 当需要解绑滚轮事件时使用
    if system == 'windows':
        app.unbind_all("<MouseWheel>")
    elif system == 'darwin'# macOS
        app.unbind_all("<MouseWheel>")
    else# Linux和其他平台
        app.unbind_all("<Button-4>")
        app.unbind_all("<Button-5>")
 
# 初始绑定滚轮事件(确保程序启动时就能响应滚轮)
# 不再需要绑定鼠标进入/离开事件,保持滚轮事件始终绑定
_bind_to_mousewheel()
 
# 确保滚动区域能够适应内容的宽度和高度
def _configure_frame(event):
    # 更新Canvas的滚动区域以适应Frame的大小
    # 获取Frame的完整大小,包括所有子组件
    width = result_frame.winfo_reqwidth()
    height = result_frame.winfo_reqheight()
    # 设置Canvas的滚动区域,确保能够水平和垂直滚动
    result_canvas.configure(scrollregion=(0, 0, width, height))
    # 设置最小宽度,确保水平滚动条能够正常工作
    result_frame.update_idletasks()
    result_canvas.config(width=min(width, app.winfo_width()-30))
 
result_frame.bind("<Configure>", _configure_frame)
 
# 删除按钮 (初始隐藏)
btn_delete = ttk.Button(app, text="删除选中文件", command=delete_selected_files, style='TButton') # 使用ttk Button
# btn_delete 将在找到重复项后通过 pack() 显示
 
if __name__ == "__main__":
    app.mainloop()



蓝奏:https://wwvu.lanzouq.com/iJv7z2ut9zof
wechat_2025-04-29_084101_633.png

免费评分

参与人数 8吾爱币 +5 热心值 +8 收起 理由
info99 + 1 这个对强迫症来需要分类来说太友好了,感谢博主!支持!
sunqf + 1 热心回复!
winner2014 + 1 + 1 热心回复!
SHJHALBE + 1 谢谢@Thanks!
zhangpengyu318 + 1 + 1 我很赞同!
sht281 + 1 + 1 谢谢@Thanks!
yuyangtai + 1 + 1 谢谢@Thanks!
linksym + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

yujinsheng 发表于 2025-4-29 10:17
感谢大佬,希望追加那种重名文件,包含那种重名后面追加序号的那种文件筛选,然后选择性删除的功能。。。
k11838959 发表于 2025-4-29 09:24
zhixinyang2010 发表于 2025-4-29 09:03
时间长了,电脑文件好多重复了,谢谢楼主试试看看。
tb612443 发表于 2025-4-29 09:11
好的,感谢分享,用来清理下重复文件试试
JSX 发表于 2025-4-29 09:13
感谢大佬分享原创代码!
alexniudada 发表于 2025-4-29 09:16
原创,支持。
linksym 发表于 2025-4-29 09:21
感谢分享~
gts5122 发表于 2025-4-29 09:24
开头的一片黑,看着蒙了,只适合看可视界面
vgdnooks 发表于 2025-4-29 09:27
感谢大佬分享,回头试试看
 楼主| MXDZRB 发表于 2025-4-29 09:32
k11838959 发表于 2025-4-29 09:24
提示有病毒,还是算了

用的python  
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-5-25 09:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表