Python脚本对比两个文件夹,快速找出重复文件名
拿到两个装满文件的文件夹想快速找出里面有哪些文件名是重复的你会怎么做很多人第一反应是把两个目录合并到一起等资源管理器提示是否覆盖也有同学想到用diff -r、rsync --dry-run或者图形化的 Beyond Compare。但这些工具解决的重点通常是“内容差异”或“增量同步”如果需求被严格限定为“只对比文件名找出重复项”它们的输出反而不好直接使用甚至会把同名但内容不同的文件也当成需要处理的冲突对象。这篇文章的核心判断是文件名对比的本质是集合运算。把两个目录树递归遍历成两个集合之后交集就是两侧同时存在的文件差集就是各自独有的文件。真正容易出问题的地方并不是遍历本身而是比较规则——用纯文件名还是相对路径、要不要忽略大小写、要不要跟随符号链接、结果要输出成什么格式。读完这篇文章你可以获得一个完整可运行的 Python 脚本在实际目录上直接运行输出三类结果两个文件夹中都存在的文件、仅在文件夹 A 中存在的文件、仅在文件夹 B 中存在的文件。文章还会解释常见坑比如中文文件名乱码、文件数量太大导致内存占用过高、同名但内容完全不同却被误判等问题并在最后给出基于文件大小和哈希的内容校验扩展思路。1. 为什么要单独写“只对比文件名”的脚本先看几个典型场景。你从旧电脑拷贝了十年照片到一个新目录现在想看看到底哪些文件已经在新电脑里存在你手头有几个 U 盘和移动硬盘想确认素材是否重复归并过你管理着一批项目文档需要找出配置文件名冲突或者你只想在合并目录前先输出一份“两边都有哪些同名文件”的清单而不是直接合并覆盖。这些场景有一个共同点你并不关心文件内容是否一致只关心“文件名是否同时出现在两个文件夹树下”。文件名重复可能代表备份重复也可能只是同名但内容不同的正常文件。先把候选名单找出来再由人工判断或后续内容校验处理这才是工程上更稳妥的做法。已有工具在这个需求上各有短板。diff -r dirA dirB比较的是文件内容某一边有同名但内容不同的文件时它会输出一堆Files ... differ而不是清晰告诉你“哪些文件名两侧都存在”。rsync -n -av dirA/ dirB/会列出待同步文件但输出信息偏多而且目标是同步而不是生成重复清单。Windows 下用资源管理器全盘搜索同名文件效率低且难以递归对比两个指定目录。图形工具能用但你是奔着一个明确的小问题去的打开一个重型双栏对比器反而显得笨重。所以一个几十行的小脚本实现“递归遍历两个目录、求文件名集合的交集和差集、输出报告”是这个场景下性价比最高的解决方案。这也是我把它写成 CSDN 教程的原因需求足够常见方案足够简单但细节坑也足够多值得完整拆解一遍。2. 基础概念文件名对比、重复检测、目录同步的区别要理解这个任务先分清三个容易混淆的概念。2.1 文件名对比文件名对比只看文件的名字不关心文件内容。只要目录树 A 中存在readme.txt目录树 B 中也存在readme.txt就认为这是一个候选重复项。哪怕两个文件内容完全无关比如一个只有几个字节另一个是几十 MB 的完整文档只要文件名相同就会被标记出来。这种比较适合做“合并前的风险预判”或“备份完整性粗筛”。它速度快、开销小但会带来很多“误报”因为同名文件在实际项目中非常常见。2.2 重复文件检测重复文件检测通常指“内容完全相同”的文件去重常用于清理照片、视频、文档中的冗余副本。它的核心手段包括文件大小、修改时间、MD5/SHA-1 等哈希校验以及更高效的分块哈希。工具如 fdupes、dupeGuru、rmlint 都属于这一类。文件名对比是内容去重的前置步骤。如果两个文件连文件名都不一样内容却完全相同传统“按文件名找重复”的方式就看不到它。反过来如果文件名相同但内容不同你也不能直接删除或覆盖。2.3 目录同步目录同步以rsync、robocopy、FreeFileSync为代表。它不只比较文件名还会比较文件大小、修改时间或哈希值目标是让两个目录最终内容一致。它关心的是“哪些需要复制”“哪些需要删除”而不是单纯输出一份重复清单。三者的区别可以用一个简单表格说明类别比较依据典型目标适合场景文件名对比文件名找出同名文件目录合并前预判、迁移时检查重名重复文件检测内容哈希 / 大小清理内容重复的文件照片、视频、文档去重目录同步大小 / 时间 / 哈希让两个目录保持一致备份、同步、发布本文实现的脚本属于第一类但我会在第 9 章给出向第二类扩展的思路。2.4 文件名集合的数学含义把两个文件夹分别看成一个集合集合 A { 文件夹 A 中所有文件的文件名 } 集合 B { 文件夹 B 中所有文件的文件名 }那么A ∩ B两个文件夹中都存在的文件即重复候选 A - B仅在文件夹 A 中存在的文件 B - A仅在文件夹 B 中存在的文件Python 的set类型天然支持这三种运算这也是脚本能写得非常简洁的原因。真正需要设计的是“如何把一个文件路径变成一个合适的集合元素”。3. 环境准备与前置条件脚本使用 Python 3 标准库实现不需要安装第三方依赖。Python 3.6 及以上版本推荐使用 3.9 或更高版本。操作系统Windows、macOS、Linux 均可。工具命令行终端Windows 下可以是 PowerShell、CMD也可以使用 VS Code 终端。先确认 Python 版本python --version如果提示找不到命令可以尝试python3python3 --version我的建议是日常开发机直接创建项目目录把脚本保存为compare_folders.py后续所有命令都基于这个文件运行。脚本只用到os、pathlib、sys、argparse四个标准库模块所以在离线环境或内网机器上也能直接执行这对很多负责运维和文件管理的场景来说是个明显优势。4. 核心流程拆解递归遍历、集合运算、结果输出整个脚本可以拆成四个步骤每一步都有值得注意的细节。4.1 递归遍历目录Python 遍历目录树有两个常用选择os.walk和pathlib.Path.rglob。os.walk每次返回当前目录路径、子目录列表、文件列表适合边遍历边处理也容易控制是否进入子目录、是否跟随符号链接。Path.rglob写法更简洁但如果你需要同时拿到相对路径、文件大小等信息还是os.walk更直接。本例使用os.walk。一个容易踩坑的地方是os.walk默认不跟随符号链接但有些需求可能希望跟随如果强行followlinksTrue在存在链接环的目录树里又可能造成死循环。稳妥的做法是默认不跟随并在文档中明确说明。4.2 确定比较单位纯文件名还是相对路径这是整个任务里最重要的设计决策。如果只需要“一般文件名是否重复”集合元素就是filename例如readme.txt、photo.jpg。如果需要“相同路径层级下的文件才算重复”集合元素应该是相对路径例如docs/guide/readme.txt此时docs/a/readme.txt和docs/b/readme.txt不算重复。很多人一开始想的是“只对比文件名”但实际需求往往是“对比整个目录结构下的文件是否一致”。两种模式都要支持所以脚本通过函数参数来控制。用纯文件名比较时如果两个目录里都存在config.ini哪怕它们在不同的子目录中也会被标记为重复。这在“把多个项目合并到一个总目录”的场景下非常有用因为你要找的正是可能发生覆盖冲突的同名文件。但如果只是在做备份对齐希望细化到每个相对路径就应该使用相对路径模式。4.3 大小写与规范化Windows 和 macOS 默认文件系统不区分大小写Linux 默认区分。同一个Readme.md和readme.md在 Windows 下可能被视为同一个文件在 Linux 下则完全互不影响。因此脚本必须提供一个可选的忽略大小写开关。启用后所有文件名先执行.lower()再存入集合。这样在不同操作系统之间迁移场景下可以减少重复判断的偏差。除了大小写还要注意路径分隔符。Windows 使用反斜杠\Linux/macOS 使用正斜杠/。如果后续要把结果写入文件或做跨平台比对最好让相对路径统一使用正斜杠避免同一个路径在不同平台上被解析成不同字符串。4.4 集合运算与结果输出遍历完成后分别得到两个集合。接下来common names_a names_b only_a names_a - names_b only_b names_b - names_a输出分为两部分控制台打印便于即时查看。写入独立的文本文件便于后续处理、排序、导入表格或交给同事复核。写入文件时要注意编码。Python 3 的open(..., encodingutf-8)可以保证 Windows 记事本等工具正确读取中文文件名。如果保存为 GBK 或系统默认编码很容易在另一台机器上乱码。4.5 海量文件的流式处理意识如果目录里有几十万甚至上百万个文件一次性把所有文件名读进内存集合会产生明显内存压力。为极端场景考虑可以采用分批读取例如按目录逐个处理把中间结果写入临时文件最后用外部排序或数据库进行集合运算。不过对大多数整理照片、合并文档的场景内存在几千万个字符串级别才会成为瓶颈普通用户不需要一开始就做这种优化但设计时保留这个意识是好的。5. 完整示例一个可直接运行的 Python 脚本下面给出一个完整脚本支持纯文件名模式和相对路径模式并支持忽略大小写。将代码保存为compare_folders.py。# 文件路径compare_folders.py import argparse import os import sys from pathlib import Path def collect_files_by_name(root: str, ignore_case: bool False, followlinks: bool False): 递归遍历目录返回纯文件名的集合。 names set() for dirpath, dirnames, filenames in os.walk(root, followlinksfollowlinks): for filename in filenames: key filename.lower() if ignore_case else filename names.add(key) return names def collect_files_by_path(root: str, ignore_case: bool False, followlinks: bool False): 递归遍历目录返回相对路径的集合。 base Path(root) paths set() for dirpath, dirnames, filenames in os.walk(root, followlinksfollowlinks): for filename in filenames: abs_path os.path.join(dirpath, filename) rel_path os.path.relpath(abs_path, base) rel_path rel_path.replace(\\, /) key rel_path.lower() if ignore_case else rel_path paths.add(key) return paths def write_result(file_path: str, title: str, items) - None: 把结果集合写入文本文件统一使用 UTF-8 编码。 with open(file_path, w, encodingutf-8) as f: f.write(title \n) f.write( * 60 \n) if not items: f.write(无\n) for item in sorted(items): f.write(item \n) def main() - None: parser argparse.ArgumentParser( description对比两个文件夹中的文件找出重复文件名 ) parser.add_argument(folder_a, help文件夹 A 的路径) parser.add_argument(folder_b, help文件夹 B 的路径) parser.add_argument( --mode, choices[name, path], defaultname, help比较模式name 只比较文件名path 比较相对路径默认 name, ) parser.add_argument( --ignore-case, actionstore_true, help忽略文件名大小写, ) parser.add_argument( --output, -o, defaultresult, help结果文件前缀默认 result, ) args parser.parse_args() for folder in (args.folder_a, args.folder_b): if not os.path.isdir(folder): print(f错误目录不存在或不可访问{folder}, filesys.stderr) sys.exit(1) followlinks False if args.mode name: names_a collect_files_by_name(args.folder_a, args.ignore_case, followlinks) names_b collect_files_by_name(args.folder_b, args.ignore_case, followlinks) else: names_a collect_files_by_path(args.folder_a, args.ignore_case, followlinks) names_b collect_files_by_path(args.folder_b, args.ignore_case, followlinks) common names_a names_b only_a names_a - names_b only_b names_b - names_a print( 两个文件夹中都存在的文件 ) for item in sorted(common): print(item) print(\n 仅在文件夹 A 中存在的文件 ) for item in sorted(only_a): print(item) print(\n 仅在文件夹 B 中存在的文件 ) for item in sorted(only_b): print(item) write_result(f{args.output}_common.txt, 两个文件夹中都存在的文件, common) write_result(f{args.output}_only_a.txt, 仅在文件夹 A 中存在的文件, only_a) write_result(f{args.output}_only_b.txt, 仅在文件夹 B 中存在的文件, only_b) print(\n结果文件已生成) print(f {args.output}_common.txt) print(f {args.output}_only_a.txt) print(f {args.output}_only_b.txt) if __name__ __main__: main()这段代码的关键逻辑有三个collect_files_by_name只保留文件名的字符串同名的文件无论放在哪个子目录都算同一个集合元素。collect_files_by_path计算相对路径并把反斜杠统一替换成斜杠这样 Windows 和 Linux 下的路径表示可以保持一致。write_result始终用 UTF-8 编码输出规避中文乱码问题。运行方式python compare_folders.py /path/to/folderA /path/to/folderB如果希望忽略大小写python compare_folders.py /path/to/folderA /path/to/folderB --ignore-case如果希望按相对路径精确比较python compare_folders.py /path/to/folderA /path/to/folderB --mode path注意这里路径如果是 Windows 风格且包含空格建议用双引号包裹例如python compare_folders.py D:\我的资料\旧目录 D:\我的资料\新目录6. 运行结果与效果验证用模拟目录演示为了验证脚本是否正确我建议在测试环境先构造一个小目录结构。下面以 Linux/macOS 终端为例。mkdir -p /tmp/test_a /tmp/test_b echo hello /tmp/test_a/readme.txt echo world /tmp/test_b/readme.txt echo aaa /tmp/test_a/only_a.txt echo bbb /tmp/test_b/only_b.txt mkdir -p /tmp/test_a/sub echo conf /tmp/test_a/sub/config.ini echo conf /tmp/test_b/sub/config.ini此时/tmp/test_a包含readme.txt、only_a.txt、sub/config.ini/tmp/test_b包含readme.txt、only_b.txt、sub/config.ini运行python compare_folders.py /tmp/test_a /tmp/test_b预期输出 两个文件夹中都存在的文件 config.ini readme.txt 仅在文件夹 A 中存在的文件 only_a.txt 仅在文件夹 B 中存在的文件 only_b.txt这里有一个值得注意的细节config.ini在两个目录下的相对路径都是sub/config.ini所以在纯文件名模式下它会被识别为重复在--mode path模式下的结果也一样。但如果我在 A 目录的根目录新建另一个config.ini让它同时存在于根目录和sub子目录纯文件名模式会因为集合去重而丢失这个细节而相对路径模式可以精确区分。再测试一下相对路径模式mkdir -p /tmp/test_b/other echo conf2 /tmp/test_b/other/config.ini python compare_folders.py /tmp/test_a /tmp/test_b --mode path预期输出中sub/config.ini仍然出现在两侧共有清单里但other/config.ini只会出现在仅在文件夹 B 中存在的文件中。这个例子说明了模式选择的实际意义。验证标准很简单检查控制台打印的三类结果是否和find命令得到的文件列表一致。如果你在 Windows 上可以在 PowerShell 里用Get-ChildItem -Recurse -File -Name快速列出一个目录下的所有文件名再和脚本输出交叉核对确认没有遗漏同名文件。7. 常见问题与排查思路问题现象可能原因排查方式解决方案运行时报“目录不存在或不可访问”路径中带空格但没有加引号或者使用的是相对路径且当前工作目录不对打印当前工作目录确认目录是否存在使用绝对路径路径含空格时用双引号包裹中文文件名在终端乱码Windows 控制台默认编码不是 UTF-8执行chcp 65001或者用cmd查看输出脚本输出文件固定encodingutf-8终端切换 UTF-8 后重试同名但内容不同也被当成重复脚本本身只比较文件名没有校验内容确认需求边界查看结果文件前先人工抽查改用相对路径模式如需内容校验参考第 9 章扩展文件数量大时脚本慢、内存占用高所有文件名一次性加载到内存 set用系统工具统计文件总数估算内存按目录分批处理或直接使用第 9 章提到的哈希过滤思路Linux 下--ignore-case似乎没起作用目录中的文件名大小写确实不同脚本默认区分大小写检查是否传入了--ignore-case参数运行命令末尾追加--ignore-case符号链接目录导致结果异常os.walk默认不跟随链接但链接本身仍可能作为文件名出现先用ls -l检查目录中是否存在符号链接保持默认followlinksFalse不要盲目开启跟随输出 txt 为空目录为空或者脚本没有访问权限检查控制台输出用find / 目录名确认文件存在确认目录有读取权限必要时以管理员身份运行两个目录下同名文件很多想按子目录归类纯文件名模式无法体现目录层级改用--mode path使用相对路径模式结果文件里会显示完整相对路径一个容易被忽视的点是不要在一个目录里同时放readme.txt和根目录的另一个readme.txt副本又期望纯文件名模式能精确告诉你“哪些子目录分别拥有哪个文件”。集合本身会去重一旦遇到重复元素仅靠set无法记录出现次数和具体位置。如果你需要在两棵目录树中统计同名文件的出现次数应该改用collections.Counter而不是set。8. 最佳实践与工程建议8.1 先输出报告再执行删除或移动脚本只负责生成清单绝不要在拿到清单后直接批量删除。实际项目中建议先检查_common.txt里的文件确认哪些确实需要合并、哪些可以忽略再手动执行操作。如果一定要自动化也要先增加一个--dry-run模式只打印将要执行的操作不真正修改文件。8.2 明确比较规则避免跨平台歧义在 Linux 服务器上处理文件时注意文件系统区分大小写在 Windows 上处理时注意路径分隔符和长路径问题。团队协作时建议在脚本头部注释里写明默认规则或者在项目 README 中附上运行参数示例。8.3 为输出文件设计可读的命名规范我的建议是结果文件使用前缀_common.txt、前缀_only_a.txt、前缀_only_b.txt的形式。这样当你要把结果分发给同事或导入到表格时文件名一目了然不容易混淆。8.4 生产环境一定要限定权限边界脚本在管理员权限下运行时可以读取系统目录和敏感目录。为避免意外扫描到不该扫描的路径建议在命令行参数之外增加一个可选的配置文件固定“允许扫描的目录列表”。对大量文件的操作坚持最小权限原则只读取有明确授权的目录。8.5 面对超大目录改用“先大小过滤再哈希校验”的策略如果目标不是单纯找同名文件而是找内容重复的大文件不要用文件名集合作为最终结论。更高效的流程是先按文件大小分组大小不同的文件不可能内容相同。对同大小的文件计算 MD5 或 SHA-1。哈希相同再定位为重复文件。这样可以把计算量控制在很小的范围。文件名对比只是这个流程里速度最快的第一层过滤。8.6 使用配置文件固化参数当脚本需要固定对比两组目录并每周运行一次时建议在脚本里读取一个简单的配置文件# compare.ini folder_a D:\backup\media folder_b D:\backup\media_old mode path ignore_case false output media_compare然后让脚本默认从配置文件读取参数命令行参数作为覆盖项。这样既保留灵活性又减少误操作。9. 扩展方向与后续学习9.1 扩展一加入文件大小和哈希校验最常见的扩展是在文件名相同的基础上再比较文件大小。只需要在遍历时同时保存fileName - size交集判断后再次筛选大小不同的文件。如果大小也相同再对文件进行分块读取并计算 MD5就可以把“文件名重复”升级为“内容重复”接近专业重复文件检测工具的效果。9.2 扩展二用 PyInstaller 打包成 exe如果目标机器没有 Python 环境可以把脚本打包成 Windows 可执行文件pip install pyinstaller pyinstaller -F -n compare_folders compare_folders.py打包完成后dist/compare_folders.exe可以在没有 Python 的 Windows 机器上直接运行。这个方式适合分发给不熟悉命令行的同事运行参数和脚本一致。9.3 扩展三对比其他命令行工具的适用性在部分场景下你并不需要写脚本。只想知道两个目录中同名文件的集合可以用find加sort加comm完成find /tmp/test_a -type f -printf %f\n | sort /tmp/list_a.txt find /tmp/test_b -type f -printf %f\n | sort /tmp/list_b.txt comm -12 /tmp/list_a.txt /tmp/list_b.txt这个命令输出的就是两侧文件名的交集。优势是不依赖 Python缺点是难以处理相对路径比较、忽略大小写、编码等问题适合快速临时验证不适合成为长期维护的工具。9.4 后续学习建议如果你想深入这个方向可以按以下顺序学习熟练掌握os.walk、pathlib.Path的目录遍历方式。了解set、Counter、defaultdict在文件整理场景中的实际应用。学习文件哈希计算理解hashlib.md5、hashlib.sha1提高校验效率的方法。了解 rsync、robocopy 的同步语义避免重复造轮子。如果经常做跨平台文件迁移建议研究不同文件系统的命名规则差异比如 NTFS、APFS、ext4 对大小写和特殊字符的处理方式。文件名对比看起来是最简单的文件整理需求但真正落地时比较规则、编码、权限、性能边界往往比预想复杂。这篇文章给出的脚本和排查思路可以作为你日常处理文件对比需求时的一个基础工具。建议收藏备用下次遇到两个满当当的文件夹时直接复制脚本改好路径就能用。