终于到了最后一天!前13天我们学完了Python的核心语法、面向对象、异常处理、模块、装饰器等。今天我们用它们来干一件实事 :写一个命令行工具,批量重命名文件。
这个需求在工作中太常见了:比如把一堆
IMG_001.jpg改成2025-04-16-001.jpg,或者把文件名中的空格替换成下划线。我们用Python来实现,然后对比一下:如果用Java写同样的工具,要写多少行? 看完你就知道为什么Python被称为"胶水语言"了。
1. 项目需求:批量重命名工具
我们要实现一个命令行工具 rename.py,支持以下功能:
python rename.py --pattern "IMG_{}.jpg" --start 1→ 将当前目录所有jpg文件按IMG_1.jpg,IMG_2.jpg... 重命名python rename.py --replace " " "_"→ 将所有文件名中的空格替换为下划线python rename.py --prefix "vacation_"→ 给所有文件添加前缀python rename.py --suffix "_backup"→ 给所有文件添加后缀(在扩展名前)python rename.py --lower→ 将所有文件名转为小写- 支持组合使用,比如同时替换空格并转小写
- 支持预览模式 (
--dry-run):只打印将要执行的操作,不真正重命名 - 支持递归子目录 (
--recursive) - 交互式确认(
--yes跳过确认)
技术点覆盖:
os模块:文件操作、路径处理glob或os.listdir:获取文件列表argparse:命令行参数解析(替代手动解析sys.argv)- 异常处理:文件不存在、权限错误、重名冲突
- 列表推导式、生成器
- 装饰器(可选:计时、日志)
- 字符串方法、正则(可选)
2. Python实现(完整代码)
python
#!/usr/bin/env python3
# rename.py - 批量重命名工具
import os
import re
import sys
import argparse
from pathlib import Path
from datetime import datetime
from functools import wraps
# ---------- 装饰器:计时 ----------
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = datetime.now()
result = func(*args, **kwargs)
end = datetime.now()
print(f"⏱️ 耗时: {(end-start).total_seconds():.3f}秒", file=sys.stderr)
return result
return wrapper
# ---------- 核心重命名逻辑 ----------
def get_files(directory, recursive, pattern=None):
"""获取待处理的文件列表"""
if recursive:
# 递归获取所有文件(包括子目录)
if pattern:
return list(Path(directory).rglob(pattern))
else:
return [p for p in Path(directory).rglob('*') if p.is_file()]
else:
if pattern:
return list(Path(directory).glob(pattern))
else:
return [p for p in Path(directory).iterdir() if p.is_file()]
def apply_transformations(name, args):
"""根据命令行参数对文件名应用一系列变换"""
original = name
# 1. 替换空格
if args.replace:
old, new = args.replace.split(',') if ',' in args.replace else (args.replace, '')
if old:
name = name.replace(old, new)
# 2. 添加前缀
if args.prefix:
name = args.prefix + name
# 3. 添加后缀(在扩展名前)
if args.suffix:
base, ext = os.path.splitext(name)
name = base + args.suffix + ext
# 4. 转小写/大写
if args.lower:
name = name.lower()
if args.upper:
name = name.upper()
# 5. 使用序号模式(会覆盖其他变换?模式通常独立使用,我们单独处理)
# 注意:模式序号模式会在外部循环中单独处理,此处不应用
return name
def generate_sequential_name(original_name, pattern, start, index):
"""根据模式生成带序号的名称,例如 pattern = 'IMG_{}.jpg',则第1个变成 IMG_1.jpg"""
# 从原文件提取扩展名
base, ext = os.path.splitext(original_name)
# 如果pattern中没有包含扩展名,自动加上
if not pattern.endswith(ext):
# 用户可能写了带扩展名的pattern,也可能没写
# 简单处理:如果pattern不含点,则自动添加原扩展名
if '.' not in pattern:
new_name = pattern.format(index) + ext
else:
new_name = pattern.format(index)
else:
new_name = pattern.format(index)
return new_name
@timer
def main():
parser = argparse.ArgumentParser(description='批量重命名工具')
parser.add_argument('--path', default='.', help='目标目录(默认当前目录)')
parser.add_argument('--recursive', '-r', action='store_true', help='递归处理子目录')
parser.add_argument('--pattern', help='序号模式,如 "IMG_{}.jpg",{}会被序号替换')
parser.add_argument('--start', type=int, default=1, help='序号起始值(配合--pattern使用)')
parser.add_argument('--replace', help='替换字符,格式 "旧,新" 或 "旧"(新为空)')
parser.add_argument('--prefix', help='添加前缀')
parser.add_argument('--suffix', help='添加后缀(在扩展名前)')
parser.add_argument('--lower', action='store_true', help='转为小写')
parser.add_argument('--upper', action='store_true', help='转为大写')
parser.add_argument('--dry-run', action='store_true', help='预览模式,不实际重命名')
parser.add_argument('--yes', '-y', action='store_true', help='跳过确认提示')
parser.add_argument('--verbose', '-v', action='store_true', help='显示详细信息')
args = parser.parse_args()
# 检查至少有一个变换操作
if not (args.pattern or args.replace or args.prefix or args.suffix or args.lower or args.upper):
print("❌ 请至少指定一种重命名规则(--pattern, --replace, --prefix, --suffix, --lower, --upper)")
sys.exit(1)
# 获取文件列表
try:
# 如果指定了pattern,只匹配对应扩展名的文件?我们简单处理:获取所有文件,然后应用变换
files = get_files(args.path, args.recursive, pattern='*')
except Exception as e:
print(f"❌ 读取目录失败: {e}", file=sys.stderr)
sys.exit(1)
if not files:
print("⚠️ 没有找到任何文件")
return
# 准备重命名映射
rename_pairs = []
if args.pattern:
# 按序号模式重命名,只对匹配pattern中扩展名的文件生效?简化:对所有文件
# 但更好的做法:只对具有相同扩展名的文件进行序号重命名
# 我们按文件扩展名分组,分别生成序号
ext_groups = {}
for f in files:
ext = f.suffix.lower()
ext_groups.setdefault(ext, []).append(f)
for ext, group in ext_groups.items():
for idx, f in enumerate(sorted(group), start=args.start):
new_name = generate_sequential_name(f.name, args.pattern, args.start, idx)
new_path = f.parent / new_name
rename_pairs.append((f, new_path))
else:
# 其他变换规则,每个文件独立
for f in files:
new_name = apply_transformations(f.name, args)
if new_name == f.name:
continue
new_path = f.parent / new_name
rename_pairs.append((f, new_path))
# 去重:如果有多个文件要重命名为同一个目标,冲突检测
target_map = {}
conflicts = []
for src, dst in rename_pairs:
if dst in target_map:
conflicts.append((src, dst, target_map[dst]))
else:
target_map[dst] = src
if conflicts:
print("❌ 检测到重名冲突:")
for src, dst, existing_src in conflicts:
print(f" {src} 和 {existing_src} 都想改成 {dst}")
print("请调整规则后重试。")
sys.exit(1)
# 显示将要执行的操作
if not rename_pairs:
print("✅ 没有需要重命名的文件(变换后名称未变)")
return
print(f"📋 将重命名 {len(rename_pairs)} 个文件:")
for src, dst in rename_pairs[:10]: # 最多显示10个
print(f" {src.name} -> {dst.name}")
if len(rename_pairs) > 10:
print(f" ... 以及其他 {len(rename_pairs)-10} 个")
if args.dry_run:
print("🏁 预览模式,未实际执行")
return
# 确认
if not args.yes:
confirm = input("确认执行?(y/n): ")
if confirm.lower() != 'y':
print("已取消")
return
# 执行重命名
success_count = 0
for src, dst in rename_pairs:
try:
src.rename(dst)
if args.verbose:
print(f"✅ {src.name} -> {dst.name}")
success_count += 1
except Exception as e:
print(f"❌ 重命名 {src.name} 失败: {e}", file=sys.stderr)
print(f"🎉 完成!成功重命名 {success_count} / {len(rename_pairs)} 个文件")
if __name__ == "__main__":
main()
使用示例:
bash
# 将所有jpg文件重命名为 photo_1.jpg, photo_2.jpg ...
python rename.py --pattern "photo_{}.jpg" --start 1
# 将所有文件名中的空格替换为下划线(预览)
python rename.py --replace " " "_" --dry-run
# 给所有txt文件添加前缀 "note_"
python rename.py --prefix "note_" --path ./docs --recursive
# 组合:转小写并替换空格
python rename.py --lower --replace " " "_" --yes
3. 如果要用Java实现......
Java实现类似工具需要:
- 使用
java.io.File或java.nio.file.Files、Path - 递归遍历目录需要自己写栈或递归函数
- 命令行参数解析:推荐用
picocli或Apache Commons CLI,或者手写switch - 字符串操作(replace、lowercase等)
- 重命名冲突检测、异常处理
- 编译打包成JAR
代码量估算 :Python约200行,Java至少需要 500~600行(含依赖配置、主类、参数解析类、工具类、异常处理等)。
更重要的是,Java没有像 pathlib 这样优雅的路径操作库,也没有 argparse 这样开箱即用的参数解析。开发效率和代码可读性差距巨大。
4. 知识点回顾(前13天在本次项目中的应用)
| 天数 | 知识点 | 在rename工具中的体现 |
|---|---|---|
| Day1 | 变量与类型 | 动态存储文件名、路径 |
| Day2 | 列表 | files 列表、rename_pairs 列表 |
| Day3 | 字典 | 分组扩展名、冲突检测使用字典 |
| Day4 | 控制流 | if args.pattern、for循环处理文件 |
| Day5 | 函数与参数 | apply_transformations、generate_sequential_name |
| Day6 | 字符串与正则 | 替换、大小写转换、扩展名提取 |
| Day7 | 推导式与生成器 | 列表推导式获取文件列表 |
| Day8 | 类与对象 | 可以使用类封装(虽然这里函数式也够) |
| Day9 | 继承 | 未使用,但可扩展不同策略 |
| Day10 | @property |
未直接使用 |
| Day11 | 异常与with |
try/except 捕获重命名失败 |
| Day12 | 模块与包 | argparse、pathlib 标准库导入 |
| Day13 | 装饰器 | @timer 计时 |
| Day14 | 综合实战 | 全部 |
5. 总结与下一步
恭喜你完成了14天的Java转Python系列!你已经具备了用Python独立开发命令行工具的能力。下一步可以:
- 发布你的工具 :将
rename.py打包成可执行文件(用pyinstaller),或上传到PyPI。 - 增加更多功能:支持正则表达式替换、支持按文件修改时间排序等。
- 学习Web框架:用Flask或FastAPI将你的工具变成Web服务。
- 数据分析:用pandas处理Excel/CSV,替代VBA。
最终对比:
| 维度 | Python | Java |
|---|---|---|
| 开发时间 | 0.5天 | 2天 |
| 代码行数 | ~200 | ~600 |
| 可读性 | 极高 | 中等 |
| 依赖管理 | 标准库足够 | 需要第三方库 |
| 运行便利性 | 有Python即可 | 需要JRE或打包 |
送给你一句话:
"Java让你成为一个工程师,Python让你成为一个创造者。"
现在,去用Python创造更多有趣的东西吧!别忘了把你的14天系列文章整理成专栏,分享给更多Java转Python的朋友。
(本文代码基于Python 3.14,在VSCode中测试通过。如果你觉得这个系列对你有帮助,请点赞、收藏、转发,让更多人看到!)