在日常整理和备份中,对比两个文件夹里的文件是否重复,是出现频率很高的需求。不过很多人第一次动手,就把“重复”理解成“内容相同”,非要算哈希、比字节,结果在几个 GB 的目录上跑半天,最后发现其实只需要按文件名对比,就能解决大部分问题。这里直接聚焦在“只对比文件名”这个需求上,适合正在整理素材、合并备份、迁移归档目录的读者。我会把命令行、PowerShell、Python 脚本、图形工具四条路线都拆一遍,并说清楚每条路线的边界和踩坑点。
1. 先想清楚:你要的是“文件名重复”,不是“文件内容重复”
1.1 只对比文件名解决哪些实际问题
只对比文件名的场景其实很常见。比如:
- 从旧移动硬盘往 NAS 整理照片时,想知道哪些文件名已经存在,避免重复拷贝。
- 两个同事各自维护一份项目素材目录,合并前先找出两边同名的文件。
- 软件安装包、视频素材、模板资源在多个备份盘之间同步,先按文件名筛查一遍。
- 归档目录越积越乱时,快速列出哪些名字出现了多次。
这类需求有一个共同特征:你要的是“名字撞了没有”,而不是“内容一样不一样”。所以核心操作就是收集文件夹 A 的文件名集合、收集文件夹 B 的文件名集合,然后求交集。换句话说,这一步是纯名字匹配,不读文件内容,不做哈希校验,速度比内容对比快很多。
1.2 同名不代表同内容,这个前提必须记住
只按文件名对比最大的坑,就是同名文件内容可能完全不一样。report.pdf在 A 目录里是上月报表,在 B 目录里是本月的,文件名一样,内容不同。如果你直接按文件名合并,很可能会覆盖掉其中一份。
所以我做这类脚本时,从来不会只输出文件名列表,而是在命中情况下尽量带上文件大小、修改时间、完整路径。这样后续人工判断就有了依据。真要确认内容是否一致,可以再对“同名命中名单”做一次哈希校验,通常数量已经比全量小很多,开销完全可控。
注意:只按文件名找到的“重复”,准确说法是“文件名重复”,不是“文件内容重复”。合并前最好再核对大小或修改时间。
1.3 先确定递归范围,再动手写代码
还有一个容易漏掉的点:要不要递归子目录。
只比两个文件夹的第一层文件,和包含所有子目录的全量对比,写法差别很大。只比第一层,用os.listdir()就够了;要递归,就得用os.walk()。实际项目里,素材往往按年份、项目、类型分了好几层,大部分需求其实是递归对比。下面的代码默认支持递归,我也会说明改成单层的方法。
2. 最快的一条路:系统自带命令,零安装零依赖
很多场景根本不需要写 Python。系统自带命令的效率不低,功能也够用。
2.1 Linux / macOS:find + comm 组合
Linux 和 macOS 自带的comm命令,专门用来对比两个有序文件列表。配合find提取文件名,一行命令就能拿结果:
comm -12 \ <(find /path/to/folderA -type f -printf "%f\n" | sort -u) \ <(find /path/to/folderB -type f -printf "%f\n" | sort -u)-12表示只输出两边都有的项,也就是交集。-printf "%f\n"只取文件名,不带目录前缀。sort -u会排序并去重,因为comm要求输入必须排序。
macOS 的find不支持-printf,需要换成-exec basename:
comm -12 \ <(find /path/to/folderA -type f -exec basename {} \; | sort -u) \ <(find /path/to/folderB -type f -exec basename {} \; | sort -u)如果你只想比较第一层目录,也可以把find换成ls -1:
comm -12 <(ls -1 /path/to/folderA | sort -u) <(ls -1 /path/to/folderB | sort -u)这个方案的好处是零依赖、执行快,非常适合在服务器上快速排查问题。缺点是没法直接输出每个文件对应的完整路径,而且-printf在 macOS 上不支持,跨平台时要注意。
2.2 Windows:PowerShell 两行搞定
Windows 自带 PowerShell,不需要装 Python 也能做。用Get-ChildItem列出文件名,再用Where-Object求交集:
$filesA = Get-ChildItem -Path "D:\folderA" -Recurse -File | Select-Object -ExpandProperty Name $filesB = Get-ChildItem -Path "D:\folderB" -Recurse -File | Select-Object -ExpandProperty Name $duplicates = $filesA | Where-Object { $_ -in $filesB } | Sort-Object -Unique $duplicates-Recurse控制是否递归,-File表示只要文件不要目录。如果只想比第一层,把-Recurse去掉即可。
需要看“只在 A 中”和“只在 B 中”时,用Compare-Object更直观:
Compare-Object $filesA $filesB输出里<=表示只在左侧,=>表示只在右侧。PowerShell 的缺点是文件量很大时管道比较慢,但对几千个文件完全没压力。
2.3 diff -rq 可以目录对比,但它不是纯文件名对比
经常有人问:为什么不直接用diff -rq?它能递归对比两个目录,并输出差异:
diff -rq /path/to/folderA /path/to/folderB注意,diff -rq对比的是内容,不是单纯文件名。如果两边存在同名但内容不同的文件,它也会报Files ... differ。所以它适合“检查两个目录是否完全相同”的场景,不适合“只按文件名找重复”。如果你只是想快速判断两个目录一不一样,它是最快的;但你要的是同名命中名单,还是用comm或 PowerShell 更准确。
3. Python 脚本:能处理中文、能输出明细、能长期复用
命令行适合一次性验证,真要落到工程环境里,我建议把对比逻辑写成一个 Python 脚本。原因有三个:一是能同时输出“重复”“只在 A”“只在 B”多份名单;二是中文文件名和特殊字符路径处理更稳定;三是方便后期加参数、加自动化和导出报表。
3.1 第一个版本:递归扫描并输出同名名单
import os def collect_filenames(folder): file_names = set() for root, dirs, files in os.walk(folder): for file in files: file_names.add(file) return file_names folder_a = r"D:\folderA" folder_b = r"D:\folderB" names_a = collect_filenames(folder_a) names_b = collect_filenames(folder_b) duplicates = names_a & names_b print(f"文件夹A文件数: {len(names_a)}") print(f"文件夹B文件数: {len(names_b)}") print(f"同名文件数: {len(duplicates)}") print("\n===== 重复文件名 =====") for name in sorted(duplicates): print(name)os.walk会递归遍历整个目录树。每个文件名放进set,最后求交集,就是两边都出现的名字。想改成单层,只需要在第一次for root, dirs, files循环后break掉:
def collect_filenames_single(folder): return set(os.listdir(folder))注意os.listdir会把子目录名也算进去,如果你的目录第一层里有子文件夹,这个差异会影响结果。
3.2 第二个版本:把同名文件的完整路径也列出来
只有文件名看不出位置。实际使用中,我会用字典保存“文件名 -> 路径列表”的映射:
import os def collect_file_map(folder): file_map = {} for root, dirs, files in os.walk(folder): for file in files: file_map.setdefault(file, []).append(os.path.join(root, file)) return file_map folder_a = r"D:\folderA" folder_b = r"D:\folderB" map_a = collect_file_map(folder_a) map_b = collect_file_map(folder_b) common_names = set(map_a.keys()) & set(map_b.keys()) print(f"同名文件数: {len(common_names)}\n") for name in sorted(common_names): print(f"[文件名] {name}") for path in map_a[name]: print(f" A: {path}") for path in map_b[name]: print(f" B: {path}") print()setdefault(key, [])的作用是:如果 key 不存在,先初始化空列表,再把当前路径追加进去。这样一个文件名即使在一个目录里出现多次,也能全部列出来。
3.3 第三个版本:结果写文件,避免控制台刷屏
文件量一大,控制台输出就成了噪音。我建议把结果写到文本文件或 CSV。写文本文件最简单:
with open("compare_result.txt", "w", encoding="utf-8") as f: f.write(f"同名文件数: {len(common_names)}\n") for name in sorted(common_names): f.write(f"{name}\n")导出 CSV 时要特别注意编码。Windows 下 Excel 打开 UTF-8 CSV 会乱码,要写成utf-8-sig:
import csv with open("duplicates.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["文件名", "A目录路径", "B目录路径"]) for name in sorted(common_names): for path_a in map_a[name]: for path_b in map_b[name]: writer.writerow([name, path_a, path_b])utf-8-sig会带上 BOM,Excel 就能正常识别。如果只在程序之间读写,用普通utf-8就行。这个细节我踩过一次,导出的 CSV 在 Excel 里全乱,后来统一用utf-8-sig才解决。
3.4 第四个版本:改成命令行参数,换目录不用改代码
脚本写死目录,换一次目录就要改一次代码,很麻烦。用sys.argv接收两个目录:
import os import sys def collect_file_map(folder): file_map = {} for root, dirs, files in os.walk(folder): for file in files: file_map.setdefault(file, []).append(os.path.join(root, file)) return file_map if __name__ == "__main__": if len(sys.argv) != 3: print("用法: python compare_names.py <文件夹A> <文件夹B>") sys.exit(1) folder_a = sys.argv[1] folder_b = sys.argv[2] map_a = collect_file_map(folder_a) map_b = collect_file_map(folder_b) common_names = set(map_a.keys()) & set(map_b.keys()) only_a = set(map_a.keys()) - set(map_b.keys()) only_b = set(map_b.keys()) - set(map_a.keys()) with open("compare_result.txt", "w", encoding="utf-8") as f: f.write(f"=== 同名文件 ({len(common_names)}) ===\n") for name in sorted(common_names): f.write(f"{name}\n") f.write(f"\n=== 只在A ({len(only_a)}) ===\n") for name in sorted(only_a): f.write(f"{name}\n") f.write(f"\n=== 只在B ({len(only_b)}) ===\n") for name in sorted(only_b): f.write(f"{name}\n")用法:
python compare_names.py "D:\素材A" "D:\素材B"跑完会在当前目录生成compare_result.txt,三部分内容都在里面。这个脚本已经够日常使用。如果要对比多个目录对,可以再包一层循环,把目录对列表写进配置文件,实现真正的批量对比。
4. 边界场景:中文文件名、大小写、扩展名、海量文件
命令行和脚本能跑通之后,真正拉开差距的是边界处理能力。
4.1 中文文件名:Windows 基本无障碍,Linux 要留意编码
在 Windows 上,Python 处理中文文件名基本无障碍,os.walk拿到的是正确字符串,直接比较就行。麻烦的是 Linux 场景。有些文件从旧设备或旧系统拷贝出来后,文件名编码可能不是 UTF-8,而是 GBK 或其他编码。很多人遇到“linux识别文件名中的汉字”出问题,本质是文件系统层就存了错误的字节序列。
这种情况下,Python 拿到的也是乱码字符串,它没法知道你原本想要的名字是什么。所以正确思路是在拷贝阶段就统一编码,用convmv这类工具把文件名转换成 UTF-8。脚本阶段能做的只是把问题暴露出来,而不是修复。
4.2 大小写:Windows 不敏感,Linux 敏感
Windows 文件系统默认不区分大小写,Photo.JPG和photo.jpg会被系统当成同一个文件。Linux 则相反。写对比脚本时,要明确业务规则。大部分素材整理场景希望大小写不同的同名文件也算重复,这时候统一转小写再比较:
normalized_a = {name.lower(): paths for name, paths in map_a.items()} normalized_b = {name.lower(): paths for name, paths in map_b.items()}但如果对比的是代码文件,.c和.C是不同源文件,不该合并。我建议在脚本里放一个ignore_case开关,默认True:
ignore_case = True if ignore_case: map_a = {k.lower(): v for k, v in map_a.items()} map_b = {k.lower(): v for k, v in map_b.items()}这样切换业务规则时,只需要改一个变量。
大小写是否忽略,由业务决定。脚本里最好放一个开关,不要写死在代码里。
4.3 扩展名大小写:要不要当同类文件
扩展名大小写本质上是上面大小写问题的延伸。IMG_001.JPG和IMG_001.jpg是同一张照片在不同设备里被改过扩展名大小写的典型情况。做素材合并时,应该把它们视为重复;做代码工程对比时,则不该合并。
结论还是那句话:大小写要不要忽略,由业务决定。脚本提供开关,是最灵活的做法。
4.4 海量文件:几十万是分水岭
文件量在几千到几万时,Python 的set和字典性能非常好,内存占用也不高。真正要担心的是几十万、上百万文件的场景。这时候有两个瓶颈:
- 扫描耗时:
os.walk要遍历整个目录树,文件越多越慢。 - 内存占用:存储几十万个完整路径字符串,内存可能到几百 MB 甚至几个 GB。
我自己处理百万级目录时,会把任务拆成按子目录分批跑,每次只处理一个顶层子目录,再把结果合并。这样即使中途失败,也只需要重跑当前批次,不会全军覆没。
5. 批量场景下最容易出问题的三个环节
5.1 输出结果没有时间戳,后一次覆盖前一次
批量处理最常见的坑,是输出文件命名。脚本放在桌面上,每次跑都生成同一个compare_result.txt,第二次跑就把第一次结果覆盖了。想回看历史对比记录时,什么都找不到。
我一般会在输出文件名里带上时间戳:
from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_file = f"compare_{timestamp}.txt"如果任务是每周定时对比两个归档目录,这个习惯尤其重要,否则历史记录无法追溯。
批量任务最怕的不是慢,而是结果被覆盖后没法追溯。输出文件一定带上时间戳。
5.2 路径包含空格、中文、括号时不要拼字符串
Windows 路径常见D:\My Documents\素材(2024)\这种带空格和括号的写法。Python 直接用字符串没问题,但如果你在脚本里用subprocess调外部命令,就要注意不要拼接命令字符串:
import subprocess result = subprocess.run(["diff", "-rq", folder_a, folder_b], capture_output=True, text=True)用列表传参,子进程会自动处理好空格和特殊字符。用字符串拼接,很容易出现路径被拆开、命令执行失败的问题。
5.3 权限不足导致漏扫,脚本不会主动告诉你
如果目录里存在无读权限的子目录,os.walk默认会静默跳过。结果就是名单不完整,但你不会第一时间发现。排查方法是:扫描结束后,把统计到的文件数和系统里显示的总数比对一下。对不上的时候,优先考虑权限问题。
也可以把权限错误显式打印出来:
for root, dirs, files in os.walk(folder, onerror=lambda e: print(f"权限错误: {e}")):这样至少能在日志里看到哪里被跳过了。
6. 不想写脚本,现成工具怎么选
6.1 三款常见工具对比
如果你只是偶尔对比一次,或者需要图形界面人工判断,现成工具更合适。我常用的几款:
| 工具 | 平台 | 免费 | 可自动化 | 适合场景 |
|---|---|---|---|---|
| PowerShell / comm | Windows / Linux | 是 | 可以 | 快速验证、定时任务 |
| Python 脚本 | 全平台 | 是 | 可以 | 复杂规则、批量自动化 |
| Beyond Compare | Windows / macOS / Linux | 付费 | 有限 | 图形化人工处理 |
| WinMerge | Windows | 是 | 有限 | 免费图形化对比 |
| Total Commander | Windows | 付费 | 部分 | 老用户顺手使用 |
6.2 Beyond Compare:交互式对比的代表
Beyond Compare 是文件夹对比的经典工具。它能同时展示左右两个面板,文件名冲突、只在左边、只在右边用不同颜色标记。默认会做内容对比,但可以通过设置改成只比较文件名,速度会快很多。
它的缺点是收费,界面信息密度偏高。如果只是偶尔比对一次文件,不一定值得为此付费。
6.3 WinMerge:免费开源,中文环境兼容性好
WinMerge 是免费开源的文件夹比较工具,支持只按文件名过滤。它把两个目录的差异列出来,点击某个文件还能查看内容差异。对中文 Windows 用户来说,WinMerge 的开箱体验比较省心。
它的劣势是不能自动化,也没法处理百万级文件的极端场景。但日常轻量对比完全够用。
6.4 Total Commander:内置同步功能
Total Commander 的“同步文件夹”功能可以直接对比两个目录,并支持只看文件名差异。如果你平时就用它管理文件,完全不用再装额外软件。Windows 11 自带的文件资源管理器也能并排比较,但只适合文件量小、一眼能看完的场景。
6.5 什么时候还是该写脚本
现成工具适合“偶尔用一次、文件量不大、需要人工判断”。但当你遇到下面这些情况,脚本是更好的选择:
- 两个目录相隔很远,需要定时自动对比。
- 结果要写进数据库、Excel 或再加工。
- 文件量特别大,图形界面打开就卡。
- 需要在对比结果基础上继续重命名、移动、归档。
我的习惯是:先用手边最快的命令行工具做一轮验证,确认规则没有歧义之后,再花十分钟写成 Python 脚本。这个顺序能避免一开始就被工具牵着走。
7. 脚本结果不对,按这个顺序排查
7.1 第一步:确认扫描范围
先问自己三个问题:
- 两个目录路径写对了吗?用的是绝对路径还是相对路径?
- 有没有包含隐藏目录、符号链接或网络挂载目录?
- 是只比第一层,还是要递归全量?
很多人把路径写成相对路径,脚本在别的目录下执行时,扫描的根本不是你以为的目录。这一步能排除一半的“结果不对”。
7.2 第二步:确认扫描出来的文件列表本身
执行前先打印两个目录各自的文件数,和系统里显示的数量对照。数量对不上,说明扫描不完整,先解决扫描问题,再讨论比对。然后再打印前 20 个文件名观察一下,判断有没有乱码、截断、重复。列表本身不正常,比对结果一定不正常。
7.3 第三步:用最小用例验证比较规则
文件列表正常之后,再看比较规则。可以用最笨也最可靠的方法验证:在两个目录里各放一个名字完全相同的测试文件,比如test_abc_123.txt,跑完看结果里有没有它。没有的话,基本可以锁定是大小写、扩展名或路径问题。
需要验证大小写影响时,可以再放一个TEST_ABC_123.TXT测试,看ignore_case开关是否生效。
7.4 第四步:检查输出编码和终端显示
在 Windows 上直接print中文文件名,控制台有时会显示乱码。这不是脚本逻辑错误,而是终端编码问题。解决办法是把结果写到文件,再用支持 UTF-8 的编辑器打开。也可以设置输出编码:
import sys sys.stdout.reconfigure(encoding="utf-8")这个细节对 Windows 用户尤其重要。我第一次跑脚本时,所有中文文件名在控制台里都是乱码,最初以为扫描出问题,调试半天才发现只是显示问题。
7.5 第五步:确认输出文件写入成功
有些情况下脚本跑完没有报错,但输出文件是空的。这时要检查输出目录是否存在、是否有写权限、文件名是否被占用。尤其是把输出文件放在需要管理员权限的目录(比如C:\Program Files)时,静默失败很常见。让脚本显式打印输出文件的完整路径和写入长度,能快速定位问题。
只对比文件名本身不复杂,真正决定体验的是你对边界场景的处理:大小写、中文编码、递归深度、输出格式、权限问题。我的建议是先用 PowerShell 或comm快速验证一轮,确定规则没有歧义之后,再写成 Python 脚本长期使用。文件对比这类需求,最忌讳一上来就追求全功能,结果连最小用例都没跑通。先把单次对比跑稳,再逐步加上路径明细、时间戳、批量目录对和自动化,整个过程会顺很多。踩过几次坑之后你就会发现,很多“结果不对”不是工具能力不够,而是扫描范围、比较规则和输出条件没有提前确认好。