只对比文件名快速找重复文件:命令行、PowerShell与Python脚本全攻略

发布时间:2026/9/7 5:05:32
只对比文件名快速找重复文件:命令行、PowerShell与Python脚本全攻略 在日常整理和备份中对比两个文件夹里的文件是否重复是出现频率很高的需求。不过很多人第一次动手就把“重复”理解成“内容相同”非要算哈希、比字节结果在几个 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 / macOSfind 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 basenamecomm -12 \ (find /path/to/folderA -type f -exec basename {} \; | sort -u) \ (find /path/to/folderB -type f -exec basename {} \; | sort -u)如果你只想比较第一层目录也可以把find换成ls -1comm -12 (ls -1 /path/to/folderA | sort -u) (ls -1 /path/to/folderB | sort -u)这个方案的好处是零依赖、执行快非常适合在服务器上快速排查问题。缺点是没法直接输出每个文件对应的完整路径而且-printf在 macOS 上不支持跨平台时要注意。2.2 WindowsPowerShell 两行搞定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 rD:\folderA folder_b rD:\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 rD:\folderA folder_b rD:\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, encodingutf-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-sigimport csv with open(duplicates.csv, w, newline, encodingutf-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会带上 BOMExcel 就能正常识别。如果只在程序之间读写用普通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, encodingutf-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开关默认Trueignore_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 fcompare_{timestamp}.txt如果任务是每周定时对比两个归档目录这个习惯尤其重要否则历史记录无法追溯。批量任务最怕的不是慢而是结果被覆盖后没法追溯。输出文件一定带上时间戳。5.2 路径包含空格、中文、括号时不要拼字符串Windows 路径常见D:\My Documents\素材(2024)\这种带空格和括号的写法。Python 直接用字符串没问题但如果你在脚本里用subprocess调外部命令就要注意不要拼接命令字符串import subprocess result subprocess.run([diff, -rq, folder_a, folder_b], capture_outputTrue, textTrue)用列表传参子进程会自动处理好空格和特殊字符。用字符串拼接很容易出现路径被拆开、命令执行失败的问题。5.3 权限不足导致漏扫脚本不会主动告诉你如果目录里存在无读权限的子目录os.walk默认会静默跳过。结果就是名单不完整但你不会第一时间发现。排查方法是扫描结束后把统计到的文件数和系统里显示的总数比对一下。对不上的时候优先考虑权限问题。也可以把权限错误显式打印出来for root, dirs, files in os.walk(folder, onerrorlambda e: print(f权限错误: {e})):这样至少能在日志里看到哪里被跳过了。6. 不想写脚本现成工具怎么选6.1 三款常见工具对比如果你只是偶尔对比一次或者需要图形界面人工判断现成工具更合适。我常用的几款工具平台免费可自动化适合场景PowerShell / commWindows / Linux是可以快速验证、定时任务Python 脚本全平台是可以复杂规则、批量自动化Beyond CompareWindows / macOS / Linux付费有限图形化人工处理WinMergeWindows是有限免费图形化对比Total CommanderWindows付费部分老用户顺手使用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(encodingutf-8)这个细节对 Windows 用户尤其重要。我第一次跑脚本时所有中文文件名在控制台里都是乱码最初以为扫描出问题调试半天才发现只是显示问题。7.5 第五步确认输出文件写入成功有些情况下脚本跑完没有报错但输出文件是空的。这时要检查输出目录是否存在、是否有写权限、文件名是否被占用。尤其是把输出文件放在需要管理员权限的目录比如C:\Program Files时静默失败很常见。让脚本显式打印输出文件的完整路径和写入长度能快速定位问题。只对比文件名本身不复杂真正决定体验的是你对边界场景的处理大小写、中文编码、递归深度、输出格式、权限问题。我的建议是先用 PowerShell 或comm快速验证一轮确定规则没有歧义之后再写成 Python 脚本长期使用。文件对比这类需求最忌讳一上来就追求全功能结果连最小用例都没跑通。先把单次对比跑稳再逐步加上路径明细、时间戳、批量目录对和自动化整个过程会顺很多。踩过几次坑之后你就会发现很多“结果不对”不是工具能力不够而是扫描范围、比较规则和输出条件没有提前确认好。