恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
使用Python对比文件夹,快速找出同名文件的工具化实践
首页
资讯中心
/
使用Python对比文件夹,快速找出同名文件的工具化实践
使用Python对比文件夹,快速找出同名文件的工具化实践
发布时间:2026/9/7 15:49:55
整理资料时我经常遇到一个很朴素的需求对比两个文件夹找出文件名重复的文件。听起来像是一分钟内能解决的事真动手时会发现如果目录里嵌套了几层子目录文件总数上千靠肉眼或者系统自带搜索就很难搞清楚。这个问题的本质并不复杂但处理方式的选择会直接影响效率。按文件名对比属于轻量方案不读取文件内容速度快适合用来做初步排查如果文件名相同但内容不同或者文件名不同但内容相同那又是另一类问题。这篇文章我会从一个最小可用的 Python 脚本讲起逐步把它升级成一个可以反复使用的文件对比工具。重点不是给你一个“跑完就删”的脚本而是把这类文件管理需求的判断思路讲清楚什么时候只需要对比文件名什么时候还要校验内容以及落地到真实目录时最容易在哪里翻车。1. 先分清按文件名对比不等于内容去重1.1 为什么“同名文件”会成为一个独立需求很多人一听到“找出重复文件”第一反应是“那就比较文件内容”。但实际场景里按文件名对比本身就有很大的应用空间。比如你从网上下载了几个资料包整理后发现一个文件夹里有项目说明.pdf另一个备份盘里也有项目说明.pdf你不确定它们是不是同一份。再比如你有一个工作目录每天同步到移动硬盘时间久了积累了很多版本你想知道移动硬盘上哪些文件在工作目录里已经存在。这类问题最关心的不是“内容是否完全一致”而是“有没有同名文件”先把同名文件找出来才有下一步人工判断的基础。另一个常见场景是数据归集。你要把两个团队各自维护的文件合并到一个共享目录如果两边都有同一个文件名直接合并会产生覆盖。这种情况下先按文件名扫一遍生成一份同名清单再决定是保留一个还是手动重命名远比直接全部覆盖安全。按文件名对比的价值不是判断“内容是否一样”而是快速建立两个目录之间的同名映射让你在合并、同步、清理之前先看到潜在冲突。1.2 什么时候应该用文件名对比什么时候该上哈希我见过不少同学一上来就写 MD5 校验结果文件特别多的时候跑得又慢又没重点。真正稳妥的处理顺序应该是分层的需求推荐方案是否读取文件内容速度适用边界只找同名文件文件名索引否极快不判断内容是否一致找内容重复文件文件大小 哈希是较慢文件多时先按大小过滤确认两个文件完全相同大小 分块哈希是中最可靠但开销最高你可以把整个过程理解成三步第一步按文件名建立索引找出所有“同名候选”第二步对候选集合按文件大小做一次过滤把大小明显不一样的文件从“内容重复”的候选中排除第三步只有大小也一致的文件才值得用哈希进一步确认。这个框架在绝大多数文件整理场景里都适用。先扩大召回再用成本递增的方式缩小范围避免一开始就做最贵的操作。文件名对比是第一步也是最便宜的一步。如果你只需要“哪些文件重名”走到第一步就足够了。2. 用 Python 写一个最小可用的同名文件扫描脚本2.1 环境准备和核心思路Python 3 自带的标准库就能完成这个任务不需要安装第三方模块也不需要引入额外的文件对比工具。核心思路很简单用os.walk递归遍历目录把文件名作为字典的 key把文件的完整路径收集到同一个 key 下面的列表里。这样如果某个文件名在两个目录里都出现了它对应的路径列表就会至少有两条记录。这里有一个关键点不要用“文件完整路径”作为对比依据因为两个目录的路径前缀本身就不一样直接比路径没有任何意义。要以“文件名”为 key才能把不同目录下的同名文件关联起来。另外一个容易被忽略的点是如果你要递归扫描所有子目录os.walk是最顺手的工具。如果你只需要比较两个目录各自顶层直接放的文件不考虑子目录那用os.scandir会更简单。两种需求在实际中都有我建议脚本默认走递归模式同时留一个开关控制。2.2 核心代码示例这是一个最小可用的版本import os from collections import defaultdict def scan_files_by_name(root_dir): 递归扫描目录返回 dict: 文件名 - [完整路径列表] name_to_paths defaultdict(list) for current_dir, subdirs, filenames in os.walk(root_dir): for filename in filenames: full_path os.path.join(current_dir, filename) name_to_paths[filename].append(full_path) return name_to_paths def find_same_name_files(dir1, dir2): 找出两个目录中文件名重复的文件 map1 scan_files_by_name(dir1) map2 scan_files_by_name(dir2) same_names set(map1.keys()) set(map2.keys()) result {} for name in sorted(same_names): result[name] { dir1_paths: map1[name], dir2_paths: map2[name], } return result if __name__ __main__: folder_a input(请输入第一个文件夹路径: ).strip() folder_b input(请输入第二个文件夹路径: ).strip() if not os.path.isdir(folder_a) or not os.path.isdir(folder_b): print(路径不存在请确认文件夹路径后重试。) else: duplicates find_same_name_files(folder_a, folder_b) if not duplicates: print(没有找到同名文件。) else: print(f发现 {len(duplicates)} 个同名文件\n) for name, paths in duplicates.items(): print(f文件名: {name}) for label in (dir1_paths, dir2_paths): for p in paths[label]: print(f {p}) print(- * 50)这段代码做的事情很清楚分别扫描两个目录建好“文件名 - 路径列表”的映射。找两个映射的 key 交集也就是同时出现在两个目录里的文件名。把每个同名文件在两个目录里的完整路径都打印出来方便你直接打开目录核对。2.3 运行结果示例假设你有两个文件夹D:\素材整理下面有一个文件说明.txtD:\备份\素材备份下面也有一个文件说明.txt运行脚本后输出大概是发现 1 个同名文件 文件名: 说明.txt D:\素材整理\说明.txt D:\备份\素材备份\说明.txt --------------------------------------------------看到这个结果你就知道这两个目录之间存在同名文件的冲突点。至于这两个说明.txt内容是否一样需要进入下一步判断仅凭文件名无法确定。2.4 如果只想对比顶层目录怎么办有些场景下你不需要递归子目录只想看两个文件夹第一层直接放的文件。这种情况下把os.walk换成os.scandir更合适import os def scan_top_files(root_dir): name_to_paths {} try: with os.scandir(root_dir) as entries: for entry in entries: if entry.is_file(): name_to_paths[entry.name] entry.path except OSError as e: print(f扫描目录失败: {e}) return name_to_pathsentry.is_file()只判断当前条目是不是文件不会进入子目录。用在只需要顶层对比的场景里逻辑更清晰速度也更快。3. 处理细节中文文件名、长路径、权限和符号链接3.1 中文文件名在 Windows 和 Linux 下的差异中文文件名是这个需求里很常见的输入尤其是国内用户的下载目录、网盘同步目录和办公文档目录基本躲不开中文名。Python 3 默认字符串就是 Unicodeos.walk返回的文件名在 Windows 和 Linux 下一般都能正确处理。真正容易出问题的是“控制台显示乱码”尤其是 Windows 的 cmd 默认编码如果不是 UTF-8打印中文路径时可能显示成乱码。但注意乱码只是显示问题脚本内部的匹配逻辑通常不受影响。如果你在 Windows 上跑脚本输出中文路径乱码可以先在命令行执行chcp 65001把控制台代码页切到 UTF-8再运行脚本。不要为了显示问题去改系统级编码设置那样会带来更多不必要的麻烦。在 Linux 下文件名本质上是一串字节中文文件名通常以 UTF-8 存储。Python 读取时只要能正常解码就不会出错。但要注意如果文件名包含特殊字符比如换行符、不可见字符打印时容易造成输出格式错乱这在医院或企业生成的夹杂特殊字符的文件名里偶尔会遇到。3.2 权限和异常处理不能省真实目录和测试目录最大的区别就是真实目录里总有一些你没权限访问的子目录、正在被占用的文件、或者已经断开的快捷方式。如果不加异常处理扫描过程可能因为一个无权限的目录直接中断前面扫描的结果全部作废。更稳妥的写法是在遍历时保护每一步def scan_files_by_name(root_dir): name_to_paths defaultdict(list) for current_dir, subdirs, filenames in os.walk(root_dir): for filename in filenames: full_path os.path.join(current_dir, filename) try: # 这里可以顺带拿到文件大小后续按大小过滤会用到 stat_result os.stat(full_path) name_to_paths[filename].append((full_path, stat_result.st_size)) except OSError as e: print(f无法访问文件: {full_path}, 错误: {e}) return name_to_paths我一般建议在扫描阶段只做“文件名 大小”的收集不要急着读取文件内容。这样即使遇到无权限文件也只是跳过或记录下来不会影响整体扫描。注意不要看到某个同名文件就立刻删除。先导出清单确认两个文件在你心里的定位再决定保留哪一个。脚本自动化删除是最后一步而且必须加人工确认机制。3.3 符号链接和目录循环os.walk默认不会递归跟随后续出现的符号链接目录这是一个安全设计。如果你显式传入followlinksTrue那就要注意目录循环的问题一个目录可能通过符号链接指向它的上级目录导致无限递归。处理办法也很简单for current_dir, subdirs, filenames in os.walk(root_dir, followlinksFalse): ...保持默认的followlinksFalse既不容易遇到循环也能避免重复扫描同一批文件。如果你确实需要跟随符号链接就一定不要同时开启“自动合并去重”而是让脚本记录链接路径人工确认后再处理。4. 从“找重复”到“判内容”按大小过滤和分块哈希4.1 为什么不能只信文件名“同名”这个信号只能说明两个文件在名字上冲突不能说明内容一致。同一个周报.docx很可能是不同人编辑的不同版本同一个photo.jpg可能是同名但完全不同的照片。反过来另一种情况也需要留意两个文件内容完全相同但文件名不一样。比如从网页下载的图片经常被命名成image_01.jpg、image_02.jpg很难通过文件名判断重复。按文件名对比对这种场景无能为力。所以如果你的目标不是“找出同名文件”而是“找出内容重复的文件”就要把判断依据从文件名升级到文件内容。但这个过程成本更高不是所有场景都需要。4.2 先按文件大小粗筛有一种很实用的折中方案在按文件名找到同名候选之后再对比它们的大小。如果两个同名文件大小不一致那它们的内容大概率是不同的只有大小一致时才值得继续做内容校验。代码上只需要在收集文件时把文件大小一起存下来def scan_files_with_size(root_dir): name_to_sizes defaultdict(list) for current_dir, subdirs, filenames in os.walk(root_dir): for filename in filenames: full_path os.path.join(current_dir, filename) try: size os.path.getsize(full_path) name_to_sizes[filename].append((full_path, size)) except OSError: continue return name_to_sizes然后在比对时把同一个文件名下大小不同的路径区分出来。大小不同基本可以判定为“同名但可能不同内容”不进入哈希阶段。这样能省掉大量不必要的读取。文件大小为 0 的文件要单独留意。两个空文件同名时它们的内容其实都是空的可以视为内容一致但这通常不是你需要关心的重复问题。4.3 分块读取文件做哈希当你需要对一批“同名且同大小”的文件做最终确认时再使用哈希。最稳妥的方式是分块读取避免一次性把大文件整个读进内存import hashlib def file_hash(path, chunk_size1024 * 1024): 分块计算文件哈希默认每次读 1MB h hashlib.sha256() with open(path, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break h.update(chunk) return h.hexdigest()MD5 也可以速度更快但如果你比较的是重要业务文件我更推荐sha256。原因很简单MD5 已经不适用于需要防碰撞的安全场景。但如果你只是在本地整理照片、文档MD5 的性能优势会明显一些也足够排查重复。哈希这一步不要轻易并发。很多人觉得文件多就要用多线程但磁盘 IO 往往是瓶颈线程开太多反而会让磁盘一直在寻道速度变得更慢。如果确实要处理上万个大文件建议先用大小过滤掉大多数候选再对剩下的小批量文件用单线程分块哈希。注意哈希只能告诉你“内容是否一致”不能告诉你“哪个应该保留”。判断保留哪一个要看你需要哪个版本、哪个文件时间更新、哪个目录是权威目录。这些业务判断是脚本替代不了的。5. 从一次性脚本到可复用工具参数化、输出和 dry-run5.1 用 argparse 封装成一个命令行工具上面那种input()交互式脚本适合临时跑一次。但如果这个操作以后会反复出现就应该把它变成一个可以带参数执行的脚本。一个比较合理的参数设计是python compare_folders.py --dir1 D:\素材整理 --dir2 D:\备份\素材备份 --output report.txt --non-recursive对应的argparse代码结构如下import argparse def parse_args(): parser argparse.ArgumentParser(description对比两个文件夹找出文件名重复的文件) parser.add_argument(--dir1, requiredTrue, help第一个文件夹路径) parser.add_argument(--dir2, requiredTrue, help第二个文件夹路径) parser.add_argument(--output, default, help输出报告文件路径) parser.add_argument(--non-recursive, actionstore_true, help只对比顶层目录) parser.add_argument(--check-hash, actionstore_true, help对同名且同大小的文件做哈希校验) return parser.parse_args()这里把“是否递归”和“是否做哈希校验”都做成了开关。默认递归扫描、只比较文件名加--check-hash时才会进入内容校验阶段。这样做的好处是日常快查和深度清理可以共用一套脚本不需要维护两份代码。5.2 用 dry-run 方式输出清单而不是直接操作文件真正要放进工作流里的工具应该具备一个很重要的特性默认不修改任何文件只输出报告。这种模式通常叫 dry-run也就是预览模式。脚本可以把重复文件清单写到文本文件或 CSV 文件里你再打开检查。CSV 输出可以这样设计import csv def write_csv_report(duplicates, csv_path): with open(csv_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([文件名, 目录1路径, 目录2路径, 大小是否一致]) for name, item in duplicates.items(): for p1 in item[dir1_paths]: for p2 in item[dir2_paths]: writer.writerow([name, p1, p2, item.get(size_match, )])utf-8-sig编码生成的 CSV 用 Excel 打开时中文不会乱码这是一个很实用的小细节。操作上建议遵循这个顺序先生成报告。人工确认哪些文件暂时不能动。需要清理时再写一个单独的删除或移动脚本并要求二次确认。不要在一个脚本里同时完成“扫描、统计、删除”那样风险太高。尤其是涉及移动硬盘、共享目录、服务器目录时误删一个文件可能带来灾难性后果。5.3 长期维护把脚本固化到工作流里如果这个对比操作不是一次性的而是每个月都要做一次你可以考虑把脚本固定到一个目录甚至加入定时任务。但长期运行和临时跑一次有几点明显不同第一路径要用绝对路径不要依赖当前工作目录。定时任务的工作目录经常和你手动执行时不一样用相对路径早晚会出问题。第二输出报告要带日期。比如report-20250601.txt或者放在以日期命名的目录里这样你才能回溯某次整理时的决策依据。第三可以考虑维护“历史索引缓存”。第一次扫描时把目录里的“文件名 大小 哈希”保存到一个 JSON 文件里。下次再扫描只对新目录生成索引然后和历史索引比对。这样文件量变大之后脚本依然能保持较快速度因为你不必每次都全量哈希所有文件。第四如果文件数量极大那么哈希阶段要考虑增量处理。先处理最近修改或新增的文件把成本控制在一个可接受的范围内。不要试图一次性把所有文件都读完要给自己留出分轮处理的空间。6. 排查链路脚本没结果、漏掉文件、输出乱码怎么办6.1 先看现象实际跑脚本时最常见的几类问题没有任何输出但直觉告诉你两个目录里明明有同名文件。有输出但漏掉了一部分文件。中文路径在控制台里显示乱码。扫描过程中报权限错误然后中断。文件对比结果和你期望的“内容一致”对不上。遇到问题不要急着改代码先按下面这个顺序排查。6.2 排查顺序第一步确认两个路径确实存在而且是你以为的那两个目录。有时候路径写错了脚本没报错是因为你输错成了其他目录。第二步确认递归设置是否符合预期。如果你用os.walk它会递归所有子目录如果你改成了os.scandir顶层模式文件名在深层子目录时自然不会被发现。先明确你要的是哪种。第三步检查权限异常是否被吞掉。很多脚本习惯except OSError: continue虽然不会中断但也会让文件悄悄被跳过。建议在跳过时至少打印一条日志方便排查。第四步检查中文乱码。Windows 命令行先把代码页切到 UTF-8看输出是否恢复正常。如果输出文件是 CSV用utf-8-sig编码Excel 就能正常显示。第五步如果你加了哈希校验确认读文件的逻辑是分块读取的。如果一次性读整个大文件内存占用会很高速度也慢还可能在文件特别大时直接报错。第六步检查符号链接和重复目录。比如你对比的两个文件夹中一个本身就是另一个的子目录或者里面存在指向外部目录的符号链接扫描结果就会和直觉不一致。6.3 如何验证脚本没有漏文件一个很简单的验证方法手工挑几个你事先知道“一定同名”的文件把它们放到两个测试目录里然后运行脚本确认结果包含它们。之后再逐步增加真实目录的复杂度。我一般会在正式处理前先用一个小测试目录跑一遍test_a/ 说明.txt 图片/ a.jpg test_b/ 说明.txt 图片/ a.jpg 新建文档.txt如果脚本能正确找出说明.txt和图片/a.jpg这两个同名文件说明基本逻辑没问题。之后再处理真实目录能节省大量排查时间。结尾回到最初的问题对比两个文件夹找出文件名重复的文件真正困难的地方不是写脚本而是判断你要的到底是“同名”还是“内容重复”。按文件名对比是极其划算的第一步它能用很小的开销帮你快速定位冲突点但如果要做内容级去重就必须引入大小过滤和分块哈希同时控制好并发和异常处理。我建议你从那个最小脚本开始先在自己的测试目录里跑通再逐步加上参数化、CSV 输出、dry-run 和哈希校验。这个过程本身就是在把一次性的临时需求变成一个可以反复复用的文件管理小工具。文件管理这件事靠肉眼永远不是长久之计真正值得投资的是那套能让你从重复劳动里脱身的处理流程。