拍了一堆照片、下了一堆资料、拷了一堆项目文件,打开文件夹一看,DSC_0234.JPG、dsc_0234.jpg、ReadMe.txt、README.txt混在一起,强迫症当场就犯了。更麻烦的是,有些程序只认固定大小写的文件名,名字里大写字母一多,传上服务器或者扔给同事,直接就“文件不存在”。
把文件名里的大写字母批量转成小写,听起来是小需求,真做起来还真有讲究。Windows 和 Linux 的大小写规则不一样,批处理和 PowerShell 的写法完全不同,直接手动改几百个文件又太愚蠢。这篇就把我实测过、并且至今还在用的 4 个方法整理出来,从系统自带命令到跨平台脚本都有,普通用户和程序员都能抄作业。
在动手之前,我建议先把下面三个背景知识过一遍。搞清楚之后你再选择方法,会少踩很多坑。
1. 动手前先想清楚:这些文件到底该不该动
1.1 大小写混乱的常见场景
我遇到过最典型的三类情况。第一类是相机和手机导出的照片,DSC_0234.JPG、IMG_0025.Png这种大写扩展名非常常见,素材一多,同一天拍的照片就会出现两种扩展名写法。
第二类是开发部署场景。项目在 Windows 上开发,文件名随手写成UserInfo.js,部署到 Linux 服务器后,Nginx 或者后端代码却用小写userinfo.js去访问。Linux 文件名是严格区分大小写的,这种问题查起来极其痛苦,页面白屏一整天,最后发现只是大小写不一致。
第三类是批量下载的资源,解压出来命名混乱,大小写全凭上传者心情。如果你也有类似需求,这篇文章里的方法可以直接解决。
1.2 动手前的两道保险:备份与预览
批量改名的第一步永远是备份。Windows 上我习惯用 robocopy:
robocopy D:\素材 D:\素材_备份 /ELinux 下直接:
cp -r /data/素材 /data/素材_备份别嫌占磁盘空间。几百个文件念错一个字符,可能就是几个小时的工作量白费。
第二步是预览。后面给的每个方案我都会带上“只看不改”的命令行参数。你先跑一遍,看它打算把哪些文件改成什么名字,确认清单没问题,再真正执行。这个习惯比任何备份都重要,因为我踩过太多次“以为改对了,其实规则写错了”的坑。
1.3 三个平台的大小写规则,完全不一样
先看一张对比表,后面所有方法的注意事项都跟这张表有关。
| 平台 | 是否区分文件名大小写 | 同一目录能否同时存在A.txt和a.txt |
|---|---|---|
| Windows | 默认不区分,但保留大小写 | 不能,系统视为同一文件 |
| Linux | 严格区分 | 可以,是完全不同的两个文件 |
| macOS | 默认不区分(可开启区分) | 默认不能,取决于磁盘格式 |
这个差异会造成一个非常迷惑的现象:在 Windows 上把ABC.TXT改成abc.txt,系统会提示“目标文件已存在”,但事实上你要改的就是它自己。后面 PowerShell 那节我会专门讲这个坑。
2. 方法一:Windows 自带 PowerShell,复制就能用
2.1 为什么不用记事本写 bat
你搜“批量改文件名”,大概率会搜到一堆.bat批处理脚本。我劝你直接跳过。批处理没有内置的字符串大小写转换函数,想实现逐字符判断,代码会写成好几层 for 循环嵌套,别说新手,我看着都头大。
PowerShell 是 Windows 10 和 11 自带的,右键开始菜单或者 Shift+鼠标右键就能打开终端。它内置了.ToLower()字符串方法,配合管道,两行命令就能解决问题。不需要装任何额外软件,这一点很方便。
2.2 单层目录:三条命令直接跑
打开 PowerShell,cd 到文件所在目录,执行:
Get-ChildItem -File | ForEach-Object { Rename-Item -LiteralPath $_.FullName -NewName $_.Name.ToLower() }解释一下:Get-ChildItem -File列出当前目录下所有文件,ForEach-Object逐个处理,$_.Name.ToLower()生成小写新文件名,Rename-Item执行重命名。-LiteralPath是为了防止文件名里包含[、]这类特殊字符时被通配符解析。
在真正执行之前,我强烈建议先跑一遍预览:
Get-ChildItem -File | Where-Object { $_.Name -cne $_.Name.ToLower() } | ForEach-Object { Write-Host $_.Name "->" $_.Name.ToLower() }-cne是区分大小写的比较,只有真的包含大写字母的文件才会被列出来。你看着这份清单确认没问题,再去执行正式命令。
2.3 递归处理全部子文件夹
几层子目录套着?加-Recurse就行:
Get-ChildItem -Path . -File -Recurse | ForEach-Object { $new = $_.Name.ToLower() if ($_.Name -cne $new) { Rename-Item -LiteralPath $_.FullName -NewName $new } }这段代码会自动遍历当前目录下的所有子文件夹,把每个文件都检查一遍。有点需要注意的是,如果某些子文件夹的路径权限不够,或者文件正被另一个程序占用,重命名会报错。报错的提示是红色的,不要慌,把正在开着的那几个文件关掉再跑一次就行。
2.4 Windows 特有的“文件已存在”坑
如果你只执行了第一条命令却没有加判断,很容易看到这样的报错:Rename-Item : 无法创建文件“abc.txt”,因为已存在同名的文件“abc.txt”。
原因就是开头那张表里写的,Windows 不区分文件名大小写。ABC.TXT改成abc.txt,系统认为目标已经存在,但实际上目标就是自己。不同版本的 PowerShell 对这种情况的处理不完全一致,所以我推荐你使用带Where-Object或if ($_.Name -cne $new)判断的版本,只改名真正变化的文件,并且在Rename-Item后面追加-ErrorAction SilentlyContinue来忽略那些“假冲突”的错误。
还有一个常见问题是有中文文件名。.ToLower()只处理英文字母,对中文没有任何影响,我的文件_v2.TXT会被改成我的文件_v2.txt,中文原样保留,放心跑。
3. 方法二:Linux/macOS 终端,一条命令批量搞定
3.1 rename 命令的版本陷阱
很多教程会告诉你 Linux 下用 rename 一行搞定,但这条命令在不同发行版上完全是两个物种。Ubuntu/Debian 自带的是 Perl 版 rename,支持完整的正则表达式,写法是:
rename 'y/A-Z/a-z/' *.JPG而 CentOS/RHEL/Fedora 自带的是 util-linux 版 rename,它只做字符串替换,不支持字符范围映射。如果直接照抄上面的命令,大概率会得到报错或者完全没有生效。
建议你先执行rename --version看是哪个版本。如果是 util-linux 版,或者你根本不想区分这些,直接用 3.2 节的通用方案。
3.2 最通用的 for + tr 组合
这个方法不依赖任何特定版本的 rename,只要是个 Bash 环境就能跑。最简单的单目录版本:
for f in *; do low=$(printf '%s' "$f" | tr 'A-Z' 'a-z') [ "$f" = "$low" ] && continue mv -n -- "$f" "$low" done逐行拆解:for f in *遍历当前目录的每个条目;printf '%s' "$f" | tr 'A-Z' 'a-z'把文件名中的大写字母替换成小写,tr命令的本质是字符映射;[ "$f" = "$low" ]判断有没有变化,没有变化就跳过;mv -n表示如果目标名字已经存在就不覆盖,--是为了防止文件名以-开头时被当成命令参数。
实测下来,这段代码对空格、中文、括号都能正确处理,因为变量都加了双引号。
3.3 递归处理子目录并防止覆盖
如果要在子目录里递归处理所有文件,用 find 组合:
find . -type f -print0 | while IFS= read -r -d '' f; do dir=$(dirname -- "$f") base=$(basename -- "$f") low=$(printf '%s' "$base" | tr 'A-Z' 'a-z') [ "$base" = "$low" ] && continue mv -n -- "$f" "$dir/$low" done这里用了-print0配合read -d '',专门应对带空格、换行符的文件名。dirname和basename把路径和文件名拆开,转小写得新名后,仍然拼回原来的目录。
mv -n这个参数极其重要。在 Linux 上,Foo.txt和foo.txt可以同时存在,如果你把一个转小写后的名字撞上了已有的另一个文件,mv默认会直接覆盖掉目标。加上-n后遇到冲突就跳过,绝不静默覆盖,这个细节救过我一次。
3.4 中文文件名在终端里的边界情况
如果你要处理的目录里有中文文件名,终端本身一般没问题,前提是系统 locale 是 UTF-8。执行locale看输出,只要带上UTF-8就行。一旦发现中文变成乱码,多半是 SSH 客户端或者终端编码设置不对,跟改名命令无关。
还有一个细节:macOS 默认的文件系统(APFS)是不区分大小写的,所以跟 Windows 一样,在同一目录里不会出现“重名”。你在 mac 上跑完 for+tr 后,如果提示mv: rename ... to ...: File exists,大概率也是遇到了大小写变体是指向同一个文件的“假冲突”,可以忽略,或者像我一样先列一下预览,把真正改名的文件筛选出来。
4. 方法三:Python 脚本,全平台通用还带安全预览
4.1 为什么还需要一个脚本
PowerShell 只在 Windows 上能用,for+tr 在 Windows 上要装 Git Bash 才能跑。如果你平时要在系统之间来回切换,或者需要定制规则,比如只改扩展名、只处理某个后缀、预览后确认再执行,那 Python 脚本是最省心的。
我不推荐直接去下载网上的“批量改名神器”,原因有两个:一是不明来源的 exe 有安全风险;二是这种软件往往限制多,文件夹数量一大就收费。Python 脚本完全免费,自己写的代码自己心里有底。
4.2 完整脚本:字典动作、递归、预览、执行一应俱全
把下面这段保存为tolower.py,放在任何目录都能用:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 批量把文件名中的大写字母改为小写。 用法示例: python tolower.py # 预览当前目录 python tolower.py --exec # 真正执行当前目录 python tolower.py -r --exec D:\素材 # 递归处理 D:\素材 下所有文件 """ from pathlib import Path import argparse def convert(path: Path, keep_ext: bool, only_file: bool, dry_run: bool): if only_file and path.is_dir(): return if keep_ext and path.is_file(): new_name = path.stem.lower() + path.suffix else: new_name = path.name.lower() if new_name == path.name: return target = path.with_name(new_name) if dry_run: print(f"[预览] {path} -> {target}") return try: path.rename(target) print(f"[已改] {path} -> {target}") except FileExistsError: print(f"[跳过] 目标已存在(不同文件): {target}") except Exception as e: print(f"[失败] {path}: {e}") def main(): parser = argparse.ArgumentParser(description="批量把大写文件名转小写") parser.add_argument("path", nargs="?", default=".", help="要处理的目录,默认当前目录") parser.add_argument("-r", "--recursive", action="store_true", help="递归处理子目录") parser.add_argument("--dirs", action="store_true", help="文件夹名也一并转小写") parser.add_argument("--keep-ext", action="store_true", help="只转主文件名,扩展名保持不变") parser.add_argument("--dry-run", action="store_true", default=True, help="预览模式,默认开启") parser.add_argument("--exec", action="store_true", help="真正执行改名,必须显式加上这一步") args = parser.parse_args() root = Path(args.path).resolve() if not root.is_dir(): print(f"路径不存在或不是目录: {root}") return dry_run = not args.exec # 默认只预览,除非你明确传入 --exec if args.recursive: # 按路径深度降序:先处理深层文件,再处理浅层目录, # 这样即使目录名也要改,也不会因为路径失效而中断 items = sorted(root.rglob("*"), key=lambda p: len(p.relative_to(root).parts), reverse=True) else: items = sorted(root.iterdir()) for p in items: convert(p, args.keep_ext, not args.dirs, dry_run) if __name__ == "__main__": main()4.3 关键逻辑说明:预览设计、深度排序和跨平台“假冲突”
这个脚本最核心的设计是“默认预览,强制显式执行”。--dry-run默认值为 True,你直接运行python tolower.py,它只会列出一堆[预览]行,不会动任何文件。只有当你明确加上--exec,它才会真正执行。我故意这么设计的,因为批量操作最怕的就是“我以为会 A,结果它做了 B”。
第二个关键点是sorted(..., reverse=True)按路径深度降序。如果文件夹名称也要转小写,你先处理文件夹本身再处理子文件,路径就断了,程序会报错找不到文件。按深度从深到浅处理,先把所有文件改名,最后再动文件夹,就不会出问题。
第三个关键点是except FileExistsError。这里处理的正是 Windows 上的“假冲突”:当ABC.txt改成abc.txt,系统可能认为目标已存在,但实际上目标就是自己。不同版本的 Python 在 Windows 上对这种操作的行为有差异,有的能正常执行,有的会抛出 FileExistsError,捕获一下更安全。如果冲突的是另一个真正的文件,这个异常也能拦住,防止意外覆盖。
4.4 顺带排查“文件名乱码”的问题
这个话题经常被搜到,但我在这里必须泼一盆冷水:批量转大小写的脚本解决不了文件名乱码。
乱码是编码问题,常见表现是中文显示成浣犲ソ或者ä¸Â。这种问题通常是因为文件名的编码和当前系统解释的编码不一致,比如本来是 GBK 编码的名字被按 UTF-8 解析,或者反过来。遇到这种情况,Python 脚本可以做一次编码猜测并转换,但成功率不一:
try: fixed = name.encode('latin-1').decode('gbk') except Exception: fixed = nameencode('latin-1').decode('gbk')这类补救方式只对特定场景有效,不是万能钥匙。我的建议是:先把乱码问题单独拎出来处理,不要跟大小写转换混在一起。文件名一旦出现乱码,先用第三方小工具或者系统自带的编码转换确认恢复效果,再决定是否继续改大小写。
5. 方法四:图形化改名工具,拖出来点几下就完事
5.1 微软 PowerToys 的 PowerRename,Windows 上最省心
如果你不想碰代码,Windows 下我首推 PowerToys 里的 PowerRename。PowerToys 是微软官方出品的一组实用工具合集,免费开源,Microsoft Store 里直接搜“PowerToys”就能装。
装完后,在资源管理器里选中一堆文件,右键菜单多了一个“PowerRename”。打开后的界面非常直观,主要的操作方式是:
- 在 Search 框中输入
(.*)(意思是匹配任意文件名)。 - 在 Replace 框中输入
$1(意思是保留匹配到的完整原文件名)。 - 在右上角的 Case 下拉菜单里选择 Lowercase。
- 预览列表会实时显示每个文件改后的效果。
- 确认无误,点击 Rename。
这个工具的好处是预览是实时的,而且支持正则,如果你顺带想去掉文件名里的括号、统一把abc - 001.jpg改成abc_001.jpg,都在同一个界面里完成。它还能只作用于文件名主体而不动扩展名,勾选对应的选项就行。
5.2 老牌工具 Advanced Renamer,规则更丰富
另一款我实测过多次的工具是 Advanced Renamer,老牌免费软件,Windows 平台。它强在规则可以叠加,比如先给文件名加前缀,再把扩展名转小写,再插入日期,一套流程自动跑完。
操作流程:
- 把文件拖进主窗口。
- 左侧选择 “Case / Upper / Lower” 规则,设置成 Lowercase。
- 上方实时预览新文件名。
- 点击 Start Batch 执行。
如果你跟热搜词“批量修改文件名前缀”“批量去除文件名里的括号”打过交道,Advanced Renamer 的 Replace 规则也能一并处理。选好规则后预览,再执行,和 PowerRename 的体验差不多,但它支持的规则类型更多。
5.3 图形工具的适用与边界
图形化工具最适合两类人:一是不习惯命令行的普通用户,二是要处理海量文件并且需要视觉确认结果的人。
需要接受它的两个边界:第一是只有 Windows 版本,Linux/macOS 上的替代品要么质量参差,要么要折腾 Wine;第二是它对大量文件夹的递归处理效果不如命令行灵活,虽然 PowerRename 和 Advanced Renamer 都能操作子文件夹,但规则一复杂,界面上的状态反而比代码更难排查。
另外提醒一句,下载任何改名工具时,尽量去官网或者微软官方渠道,别在下载站随便点“高速下载”。市面上有不少打包了推广软件的同名工具,装完会附带一堆全家桶。
6. 实操心得与避坑清单
6.1 我踩过的四个坑
第一个坑是处理目录名顺序不对。有一次我用最早的 Python 脚本,先改了外层文件夹名,再去处理里面的子文件,脚本直接抛 FileNotFoundError,因为路径引用已经失效。后来一律先深后浅,彻底解决。
第二个坑是 Windows 上判断“目标是否已存在”的假阳性。我在脚本里写if target.exists(): continue,跑了一遍发现所有文件全被跳过,因为 Windows 把ABC.txt和abc.txt视为同一个文件,target.exists()一直返回 True。解决方案是别用 exists 判断,直接 try rename,用异常处理兜底。
第三个坑是 Linux 上mv静默覆盖。有一次改Foo.txt成foo.txt,而目标目录里恰好已有一个内容不同的foo.txt,我不加-n直接 mv,旧文件被覆盖。血的教训,从此所有批量改名一律带-n。
第四个坑是改完名后项目引用全乱。开发项目里,文件名大小写一变,所有 import、图片路径、配置引用全部失效。这个问题的解法不是改名后看运气,而是改名之前先全局搜索一遍引用。如果项目是 git 仓库,用git mv而不是mv,这样 Git 才能正确识别成重命名而不是删除加新增。
6.2 速查表:哪个方法适合你
| 你的情况 | 推荐使用 |
|---|---|
| Windows 用户,不想安装任何东西 | 方法一:PowerShell |
| Linux/macOS 用户,喜欢命令行 | 方法二:for + tr |
| 需要跨平台复用,或要自定义规则 | 方法三:Python 脚本 |
| 不习惯命令行,想可视化预览 | 方法四:PowerRename / Advanced Renamer |
| 只改扩展名,不动文件主体 | 方法三加--keep-ext或工具里的对应选项 |
| 需要递归处理深层目录 | 方法一、二、三都支持,优先方法三 |
6.3 特殊软件场景:别乱改项目依赖文件名
最后特别提醒一种情况:涉及软件工程类项目引用文件的时候,比如 SolidWorks 装配体、PS 素材、AE 工程、代码仓库里的资源文件,这类文件之间是通过文件名和路径相互引用的。如果你把某个零件文件从Part_001.SLDPRT改成part_001.sldprt,同时没有通过软件自身的打包功能,装配体很可能打不开,报“文件名无效或被锁住”。
这类场景下,正确做法是使用软件自带的打包/另存为功能,让它同步更新引用关系,而不是在资源管理器里直接改。如果只是在做素材归档、照片整理、编码规范统一这类“文件本身是独立个体”的场景,那随便用上面的哪种方法都行。
我自己的习惯是,每次批量改名都先用预览模式输出一个清单,人眼扫一遍,再执行。改完之后,把项目重新打开、素材重新加载一遍,确认没有红叉、没有 404,才算真正完成。文件名大小写这件事,本身不复杂,复杂的是改完之后的连锁反应——但只要你始终记得“备份加预览”这两个词,基本就不会出大问题。