Python脚本对比两个文件夹中的重复文件名:从原理到实战
平时整理文件的时候最容易遇到的一种情况就是把两个文件夹合并到一起结果发现里面有不少文件名是一样的又不知道哪些该留、哪些该删。手动去对比小文件夹还可以文件一多就完全看不过来。这篇文章就围绕“对比两个文件夹中的文件只按文件名找出重复项”这个需求完整拆解实现思路并给出可以直接复用的 Python 脚本。整个实现不依赖第三方库Windows、macOS、Linux 都能跑适合文件整理、备份迁移、资料归档等场景。1. 为什么要对比两个文件夹里的重复文件名1.1 典型场景痛点很多人电脑里都有这种状态有一个旧的备份文件夹里面是半年前的数据又有一个新的工作文件夹里面是这段时间陆续补充的文件两边内容高度重叠但目录结构、文件存放位置都不一样完全没法直接合并。还有一类更常见的场景是下载目录和网盘同步目录。下载软件默认保存到C:\Users\xxx\Downloads网盘客户端同步目录又在另一个盘时间一长两边就会堆积大量同名文件。占空间还在其次关键是容易混淆版本搞不清哪个文件是最新的也不敢随便删。如果文件数量只有几十个用眼睛扫一遍还能接受。但如果是几百个甚至上千个文件人工对比就不现实了。这时候需要写一个脚本让程序去把两个文件夹里的文件名分别收集起来做个集合运算一次性找出两边都有的名字。1.2 为什么先按文件名对比判断两个文件是否“重复”其实有很多维度文件名是否相同文件大小是否相同文件内容是否相同哈希值是否一致修改时间是否接近。其中内容对比最准确因为不管文件叫什么名字只要内容一致本质上就是同一个文件。但内容对比的代价也比较高需要对每个文件做哈希计算遇到大文件时会明显变慢。按文件名对比是最快的粗筛方式。它能帮我们快速回答一个问题“这两个文件夹里有没有同名文件”如果文件名都不一样那基本不需要再担心重复问题如果名字相同再决定要不要进一步做内容校验。这个思路在实际工程里叫“先粗筛再精查”效率是最高的。2. 环境准备与思路拆解2.1 环境说明本文示例使用 Python 3没有引入任何第三方库全部使用标准库实现所以不需要安装额外依赖。项目说明操作系统Windows / macOS / Linux 均可Python 版本Python 3.8 或更高版本第三方库无IDEPyCharm、VS Code 或记事本均可文件编码源码文件保存为 UTF-8如果你之前没在电脑上安装过 Python可以去官网下载安装包。安装时记得勾选Add Python to PATH这样在命令行里才能直接执行python命令。2.2 核心思路这个需求的核心逻辑并不复杂可以拆成三步遍历文件夹 A拿到里面所有文件的文件名遍历文件夹 B拿到里面所有文件的文件名求两个文件名集合的交集交集里的名字就是重复文件名。用集合来做是最自然的思路因为 Python 的set天生支持交集、并集、差集运算文件夹 A 的文件名集合 ──┐ ├── 交集 → 重复文件名 文件夹 B 的文件名集合 ──┘举个例子。假设文件夹 A 里有这些文件a.txt b.txt c.log data.xlsx文件夹 B 里有这些文件b.txt c.log readme.md那么两个集合的交集就是b.txt c.log这两个文件名在两边都存在属于重复项。如果后续要合并文件夹至少需要对这两个文件做进一步确认。2.3 需要处理的边界情况实现之前最好先把边界情况想清楚否则脚本很容易在个别特殊文件上“翻车”。第一要区分文件和目录。遍历目录的时候os.listdir返回的是目录下的所有条目包括子文件夹。如果直接把子文件夹的名字也当成文件名加入集合最后的结果就不准确了。所以必须判断每个条目是不是文件。第二大小写问题。Windows 和 macOS 默认文件系统不区分大小写Readme.txt和readme.txt会被视为同一个文件但 Linux 严格区分大小写。如果你的脚本要在不同系统之间通用需要提供“忽略大小写”的选项。第三文件名里的空格和多余字符。比如新建 文本文档.txt和新建文本文档.txt肉眼看起来差不多但程序会把它们当成两个不同的名字。要不要忽略这种差异取决于你的使用场景。第四子目录中的同名文件。两个文件夹如果都有子目录子目录里可能存在同名文件。本文第一版示例只处理顶层文件后面会给出递归遍历子目录的扩展方式。第五中文文件名和编码。Windows 下 Python 读取文件时如果遇到编码问题或者终端输出乱码通常需要调整输出方式。比如把报告写入 CSV 文件时指定utf-8-sig编码Excel 打开就不会乱码。3. 从零实现基础版本3.1 使用 os.listdir 遍历目录先来看最简单的遍历写法。os.listdir(path)会返回指定目录下的所有条目名称类型是字符串列表import os folder_a rC:\Users\test\folder_a names os.listdir(folder_a) for name in names: print(name)这段代码会把folder_a下的所有文件和子文件夹名字都打印出来。路径前面的r表示原始字符串避免 Windows 路径中的反斜杠被当成转义符处理。运行后输出类似下面这样a.txt b.txt data.xlsx subfolder注意最后一项subfolder是一个目录不是文件。直接把它加入“文件名集合”会污染结果所以下一步要过滤。3.2 判断文件而不是目录使用os.path.isfile(path)可以判断一个路径是不是文件import os folder_a rC:\Users\test\folder_a names_a set() for name in os.listdir(folder_a): full_path os.path.join(folder_a, name) if os.path.isfile(full_path): names_a.add(name) print(names_a)这里有个细节os.path.join(folder_a, name)的作用是把文件夹路径和文件名拼接成完整路径。直接用name去调用os.path.isfile是不行的因为相对路径依赖当前工作目录容易出问题。3.3 第一版完整代码把上面的逻辑扩展到两个文件夹再求交集就得到了第一个可用版本import os folder_a rC:\Users\test\folder_a folder_b rC:\Users\test\folder_b # 收集文件夹 A 中的文件名 names_a set() for name in os.listdir(folder_a): full_path os.path.join(folder_a, name) if os.path.isfile(full_path): names_a.add(name) # 收集文件夹 B 中的文件名 names_b set() for name in os.listdir(folder_b): full_path os.path.join(folder_b, name) if os.path.isfile(full_path): names_b.add(name) # 取交集 duplicates names_a names_b print(两个文件夹中文件名相同的文件有) for name in sorted(duplicates): print(name) print(f\n文件夹 {folder_a} 共有 {len(names_a)} 个文件) print(f文件夹 {folder_b} 共有 {len(names_b)} 个文件) print(f重复文件名共有 {len(duplicates)} 个)这段代码虽然简单但已经能解决最基本的需求。执行后会列出两边都存在的文件名以及文件数量统计。4. 进阶让对比更实用基础版能跑但离“好用”还有距离。实际文件整理中还需要考虑大小写、扩展名、独有文件输出、报告导出等问题。下面逐项优化。4.1 文件名归一化所谓“归一化”就是把文件名转换成一种统一的、可比较的形式。常见处理包括去掉首尾空格全部转成小写可选地去掉扩展名把全角字符转成半角字符。实现一个normalize_filename函数import os import unicodedata def normalize_filename(name, ignore_caseTrue, ignore_extFalse): # 如果忽略扩展名只取主文件名 if ignore_ext: name os.path.splitext(name)[0] # 去掉首尾空格 name name.strip() # 忽略大小写时统一转小写 if ignore_case: name name.lower() # 使用 NFC 规范化统一 Unicode 编码形式 return unicodedata.normalize(NFC, name)unicodedata.normalize(NFC, name)这一步容易被忽略但对中文文件名很重要。因为 macOS 的文件系统倾向于使用 NFD 编码Windows 和 Linux 更常用 NFC同一个“中文文件名”在不同系统下可能存储为不同的 Unicode 序列。直接比较字符串会认为它们不相同规范化之后才能正确匹配。如果你还需要处理全角半角的问题比如把全角空格\u3000和全角字母转成半角可以加一个辅助函数def full_to_half(text): result [] for ch in text: code ord(ch) if code 0x3000: code 0x20 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) return .join(result)全角半角的差异在中文用户文件名里其实挺常见。比如有人习惯用全角括号1有人用半角括号(1)不处理这两者在字符串比较时永远不相等。4.2 三种对比模式根据实际需求可以设计三种对比模式模式说明适用场景完全匹配文件名必须一模一样才算重复Linux 环境或明确区分大小写忽略大小写大小写不同也算同名Windows / macOS 环境忽略大小写和扩展名主文件名相同就算重复清理.txt和.bak等衍生文件模式通过参数控制ignore_case和ignore_ext两个布尔值组合即可def compare_folders(folder_a, folder_b, ignore_caseTrue, ignore_extFalse): names_a collect_filenames(folder_a, ignore_case, ignore_ext) names_b collect_filenames(folder_b, ignore_case, ignore_ext) duplicates names_a names_b only_a names_a - names_b only_b names_b - names_a return duplicates, only_a, only_b注意collect_filenames里需要把原始文件名和归一化后的文件名做一个映射否则最后打印结果时会丢失用户真正看到的名字。4.3 同时输出两边独有的文件很多时候用户不只是想知道“哪些重复”还想知道“哪些只在某个文件夹里有”。集合差集运算可以顺便解决only_a names_a - names_b only_b names_b - names_aonly_a表示只在文件夹 A 中存在的文件名only_b表示只在文件夹 B 中存在的文件名。这个信息在做合并操作时非常有用only_a和only_b可以安全复制或迁移duplicates需要人工确认后再处理。4.4 导出 CSV 报告文件数量一多终端输出就不方便查看了。把对比结果导出成 CSV 文件可以在 Excel 里排序、筛选、标记处理状态。import csv def write_report(report_path, duplicates, only_a, only_b): with open(report_path, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([类型, 文件名]) for name in sorted(duplicates): writer.writerow([重复, name]) for name in sorted(only_a): writer.writerow([仅在左目录, name]) for name in sorted(only_b): writer.writerow([仅在右目录, name])这里使用了utf-8-sig编码是因为 Excel 打开 UTF-8 文件时如果没有 BOM 头中文容易乱码。utf-8-sig会在文件开头写入 BOMExcel 就能正确识别编码。5. 完整实战脚本5.1 功能设计把上面的模块整合成一个命令行工具支持以下功能指定左右两个文件夹路径可选忽略大小写可选忽略扩展名输出重复文件名、明文独有文件可选导出 CSV 报告。命令行参数使用argparse标准库实现这样脚本既可以在 IDE 里运行也可以在命令行里传参运行。5.2 完整代码把下面代码保存为find_duplicate_names.py# -*- coding: utf-8 -*- find_duplicate_names.py 按文件名对比两个文件夹找出重复文件名。 用法示例 python find_duplicate_names.py -a D:\\folder_a -b D:\\folder_b python find_duplicate_names.py -a D:\\folder_a -b D:\\folder_b --ignore-case python find_duplicate_names.py -a D:\\folder_a -b D:\\folder_b --ignore-ext --report result.csv import os import csv import argparse import unicodedata def normalize_filename(name, ignore_caseTrue, ignore_extFalse): 对文件名做归一化处理便于比较。 if ignore_ext: name os.path.splitext(name)[0] name name.strip() if ignore_case: name name.lower() return unicodedata.normalize(NFC, name) def collect_filenames(root_dir, ignore_caseTrue, ignore_extFalse): 遍历 root_dir 顶层目录收集所有文件的文件名。 返回一个字典key 是归一化后的文件名value 是原始文件名。 file_map {} try: with os.scandir(root_dir) as entries: for entry in entries: try: if entry.is_file(): normalized normalize_filename( entry.name, ignore_case, ignore_ext ) if normalized not in file_map: file_map[normalized] entry.name except OSError as e: print(f处理文件 {entry.path} 时出错: {e}) except PermissionError as e: print(f无法访问目录 {root_dir}: {e}) return file_map def compare_folders(folder_a, folder_b, ignore_caseTrue, ignore_extFalse): 对比两个文件夹返回重复文件名、仅 A 有、仅 B 有。 files_a collect_filenames(folder_a, ignore_case, ignore_ext) files_b collect_filenames(folder_b, ignore_case, ignore_ext) names_a set(files_a.keys()) names_b set(files_b.keys()) duplicates names_a names_b only_a names_a - names_b only_b names_b - names_a return duplicates, only_a, only_b, files_a, files_b def write_report(report_path, duplicates, only_a, only_b, files_a, files_b): 把对比结果写入 CSV 报告。 with open(report_path, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([类型, 文件名, 原始文件名]) for name in sorted(duplicates): writer.writerow([重复, name, files_a.get(name, )]) for name in sorted(only_a): writer.writerow([仅在左目录, name, files_a.get(name, )]) for name in sorted(only_b): writer.writerow([仅在右目录, name, files_b.get(name, )]) def main(): parser argparse.ArgumentParser( description对比两个文件夹中的文件按文件名查找重复项 ) parser.add_argument(-a, --folder-a, requiredTrue, help左目录路径) parser.add_argument(-b, --folder-b, requiredTrue, help右目录路径) parser.add_argument( --ignore-case, actionstore_true, help忽略文件名大小写Windows/macOS 推荐, ) parser.add_argument( --ignore-ext, actionstore_true, help忽略扩展名只比较主文件名, ) parser.add_argument( --report, help可选导出 CSV 报告路径, ) args parser.parse_args() duplicates, only_a, only_b, files_a, files_b compare_folders( args.folder_a, args.folder_b, ignore_caseargs.ignore_case, ignore_extargs.ignore_ext, ) print( * 60) print(f左目录: {args.folder_a}) print(f右目录: {args.folder_b}) print( * 60) print(f重复文件名数量: {len(duplicates)}) for name in sorted(duplicates): print(f [重复] {files_a.get(name, name)}) print(f\n仅在左目录中的文件数量: {len(only_a)}) for name in sorted(only_a): print(f [仅左] {files_a.get(name, name)}) print(f\n仅在右目录中的文件数量: {len(only_b)}) for name in sorted(only_b): print(f [仅右] {files_b.get(name, name)}) if args.report: write_report(args.report, duplicates, only_a, only_b, files_a, files_b) print(f\n详细报告已写入: {args.report}) if __name__ __main__: main()这段代码做了几个关键设计collect_filenames返回的是一个字典而不是普通集合。这样在处理交集后还能通过归一化后的名字找到原始文件名输出时对用户更友好。使用os.scandir而不是os.listdir性能更好特别是在目录里文件特别多的时候。对单个条目的OSError做了捕获避免因为某个文件权限不足导致整个脚本崩溃。5.3 运行与验证假设有两个文件夹D:\backup\2024 D:\backup\2025在命令行运行python find_duplicate_names.py -a D:\backup\2024 -b D:\backup\2025 --ignore-case --report result.csv预期输出大致如下 左目录: D:\backup\2024 右目录: D:\backup\2025 重复文件名数量: 3 [重复] 项目总结.docx [重复] 预算表.xlsx [重复] README.md 仅在左目录中的文件数量: 5 [仅左] 旧版需求.txt 仅在右目录中的文件数量: 7 [仅右] 新版需求.txt 详细报告已写入: result.csv如果加上了--ignore-ext那么report.docx和report.pdf也会被识别为重复因为只看主文件名report。6. 只对比文件名的局限性与补救6.1 同名文件不代表内容相同按文件名对比虽然快但它有一个天然缺陷文件名相同内容不一定相同。举几个实际例子同事发给你一份项目排期.xlsx过几天又发了一个同名文件但内容是修改过的版本下载目录里有软件安装包.zip旧备份里也有一个同名的但一个是 1.0 版本一个是 2.0 版本README.md这种通用命名更常见两个文件内容可能完全不同。所以脚本跑出来的“重复文件名”只是候选清单不等于可以直接删除的重复文件。这一点在使用时一定要保持清醒。6.2 结合哈希校验的双阶段方案更稳妥的做法是分两步走第一步用上面的脚本按文件名筛选得到同名文件列表第二步对同名文件做内容哈希校验确认内容是否真的相同。哈希校验可以用hashlib标准库实现import hashlib def file_md5(file_path, chunk_size8192): 计算文件的 MD5 值。 h hashlib.md5() with open(file_path, rb) as f: while chunk : f.read(chunk_size): h.update(chunk) return h.hexdigest()使用:海象运算符需要 Python 3.8 以上版本。计算哈希时分块读取避免大文件一次性读入内存导致卡顿。先按文件名粗筛、再按哈希精查的组合方式在性能上比直接遍历所有文件做哈希要快得多同时又能避免误删不同内容的同名文件。7. 常见问题与排查思路问题现象常见原因解决思路脚本报错找不到路径路径写错或使用了相对路径确认绝对路径检查盘符、大小写输出乱码终端编码不支持中文Windows 下设置PYTHONIOENCODINGutf-8或把报告导出到 CSV明明有相同文件结果却是空的大小写不一致或文件名包含空格、全角字符开启--ignore-case检查是否有多余空格文件名中包含中文比较结果错误macOS 的 Unicode 编码与 Windows 不同使用unicodedata.normalize(NFC, name)统一编码提示权限不足某个子文件夹没有访问权限以管理员/root 身份运行或排除该目录脚本只对比了顶层文件没有递归遍历子目录改用os.walk遍历所有子目录find_duplicate_names不是内部或外部命令没有把脚本放到当前目录或 Python 未加入 PATH检查 Python 安装状态使用完整路径执行脚本单说几个容易踩坑的细节。第一个坑路径中带有空格。命令行传参时路径必须用双引号包起来否则程序会把路径拆成多个参数。比如python find_duplicate_names.py -a D:\My Backups\2024 -b D:\My Backups\2025第二个坑Windows 路径反斜杠。在 Python 代码里写 Windows 路径建议使用r前缀folder_a rD:\backup\2024如果不加r\b会被转义成退格符导致路径错误。在命令行里传参则不存在这个问题。第三个坑递归遍历子目录。基础版脚本只处理文件夹顶层的文件。如果两个文件夹有多层子目录你需要改成os.walk递归遍历。核心思路是在每一层子目录中收集文件名并记录文件所在目录这样遇到同名文件时还能知道它分别在哪个子目录下def collect_filenames_recursive(root_dir): file_map {} for current_dir, _, files in os.walk(root_dir): for file_name in files: normalized normalize_filename(file_name) if normalized not in file_map: file_map[normalized] os.path.join(current_dir, file_name) return file_map这里用os.walk会自动递归所有子目录_用来忽略子目录列表。收集时记录完整路径后续输出会更清晰。8. 工程建议与使用场景扩展8.1 不要直接删除重复文件很多人在拿到重复文件名清单后的第一反应是“赶紧把重复的删掉”。这是比较危险的操作。正确的做法是先把报告导出成 CSV在 Excel 里按文件大小、修改时间排序逐个确认保留哪一个版本把要删除的路径移动到“待删除”文件夹而不是直接rm观察几天确认没有影响后再清空“待删除”文件夹。这样做的好处是即使误判了也能在回收站或待删除文件夹里找回文件。尤其在处理备份数据、项目资料时一时手滑造成的损失可能无法挽回。8.2 结合批处理与自动化这个脚本不只可以手动执行也可以集成到自动化流程里。比如配合 Windows 任务计划程序每周自动对比下载目录和网盘同步目录在 NAS 上配置定时任务定期扫描备份目录中的重复文件名把对比结果通过邮件发送给自己提醒哪些文件需要人工处理。脚本本身保持“报告先行、不做破坏性操作”的设计原则天然适合放在自动化任务里。8.3 一个 PowerShell 替代方案如果你不想装 PythonWindows 下也可以直接用 PowerShell 快速对比。下面的命令会输出两个文件夹中文件名完全相同的文件$folderA Get-ChildItem -Path D:\folder_a -File | Select-Object -ExpandProperty Name $folderB Get-ChildItem -Path D:\folder_b -File | Select-Object -ExpandProperty Name $duplicates $folderA | Where-Object { $_ -in $folderB } $duplicates这段命令相当于基础版脚本的 PowerShell 实现。优点是无需安装任何环境缺点是功能比较简单不支持忽略大小写、忽略扩展名、导出报告等扩展功能。文件量不大的时候应急用一下还是可以的。9. 一些实际使用经验这个脚本在我自己整理文件的过程中确实帮了不少忙。有一次需要把旧电脑的整盘数据迁移到新电脑新旧目录结构完全不同文件数以千计。直接复制会浪费大量磁盘空间手动排查又几乎不可能。最后就是用类似的脚本先生成了一份 CSV 报告再结合文件大小和修改时间筛选才把真正需要保留的文件挑了出来。如果你要处理的是照片这类文件建议不要只用“文件名对比”。因为两个不同文件夹里的照片文件名往往都由相机自动编号比如IMG_0001.JPG看起来重名但内容可能完全不同。这种情况下最好配合文件大小或 MD5 值做二次校验避免误删。还有一个小建议脚本输出不要只打到终端养成把结果导出成文件的习惯。终端内容滚动之后就没法回溯了但 CSV 报告可以随时查看、筛选也算是一份处理记录。后面碰到用户质疑“你凭什么删了这个文件”时报告就是最直接的说明。对比两个文件夹里的重复文件是个很小但特别高频的需求花一点时间把它做成可复用的脚本长期来看是值得的。如果这篇文章解决了你的问题可以收藏备用也欢迎在实践时根据自己的场景调整参数。