<think>我们将编写一个Python程序,用于在Ubuntu 18.04中批量重命名文件。
功能要求:
1. 可以指定目录(默认为当前目录)
2. 可以按照指定的规则重命名文件,例如:添加前缀、后缀、替换字符串、使用正则表达式等。
3. 程序应该提供安全选项,例如预览更改而不实际执行,或者提供备份。
为了灵活性,我们将设计一个函数,它接受目录路径、重命名规则函数以及是否执行重命名(或预览)等参数。
我们将支持以下几种常见的重命名方式(可以通过传递不同的规则函数来实现):
- 添加前缀
- 添加后缀
- 替换文件名中的字符串
- 使用正则表达式替换
此外,为了安全起见,我们将先获取所有文件,然后根据规则生成新文件名,并显示更改前后的对比。如果用户确认,则执行重命名。
步骤:
1. 遍历指定目录下的文件(不包括子目录,除非特别指定)。
2. 对每个文件名应用规则函数,生成新文件名。
3. 显示更改预览(旧文件名 -> 新文件名)。
4. 询问用户是否确认执行。
5. 如果确认,则进行重命名。
注意:为了避免重名冲突,我们需要确保新文件名在目录中唯一。如果生成的新文件名有重复,我们将提示错误并中止。
我们将设计一个交互式的命令行程序。
但是,为了灵活性,我们也可以设计成非交互式,通过参数控制。
我们先设计一个核心函数,然后提供几种规则函数的例子。
由于用户可能想要不同的规则,我们可以让用户自己编写规则函数,但为了简便,我们提供几个常用的规则函数工厂。
例如:
- prefix_rule(prefix): 返回一个函数,该函数在文件名前添加前缀。
- suffix_rule(suffix): 返回一个函数,该函数在文件名后添加后缀(在扩展名之前)。
- replace_rule(old, new): 返回一个函数,该函数替换文件名中的old字符串为new。
- regex_rule(pattern, replacement): 使用正则表达式替换。
注意:我们可能希望保留文件扩展名。所以在添加后缀时,我们会在主文件名后面加后缀,扩展名保持不变。
我们将文件名分为两部分:主文件名和扩展名(最后一个点之后的部分)。但注意有些文件可能没有扩展名。
实现:
首先,我们写一个函数来分割文件名(不含路径)为主文件名和扩展名。
例如:'document.txt' -> ('document', '.txt')
注意:如果文件名以点开头(如'.gitignore'),我们将其视为没有主文件名,只有扩展名?但通常,我们会将整个字符串视为文件名,而扩展名是最后一个点之后的部分。不过,对于以点开头的文件,我们可能希望保留开头的点。
我们可以使用os.path.splitext,它会正确处理以点开头的文件。
但是,我们也要注意:有些文件可能有多个点,比如'a.b.c',我们希望保留除最后一个点之外的部分作为主文件名吗?通常,是的。
所以,我们使用:
base, ext = os.path.splitext(filename)
base 包含路径中最后一个点之前的所有部分,ext包含点以及之后的部分。
对于没有扩展名的文件,ext为空字符串。
在添加后缀时,我们只修改base部分,然后重新组合为 base + suffix + ext。
同样,添加前缀:prefix + base + ext
替换和正则替换则对整个文件名(包括扩展名)进行操作?这取决于用户。我们可以提供选项,但为了简单,我们的规则函数将作用于整个文件名(不包括路径,但包括扩展名)。如果用户只想修改主文件名,可以在规则函数内部先分割再操作。
但是,为了灵活性,我们让规则函数接受整个文件名(不带路径),并返回新的文件名(不带路径)。这样用户可以实现任何规则。
因此,我们的核心批量重命名函数如下:
设计:
def batch_rename(directory, rule_function, recursive=False, preview=True, execute=False):
# 如果recursive为True,则递归遍历子目录,否则只处理当前目录。
# preview: 是否预览(显示更改前后的对比)
# execute: 是否实际执行重命名。如果preview为True,则无论execute如何都会先预览,然后询问是否执行(除非execute为True,则直接执行而不询问?)
为了安全,我们这样设计流程:
- 先收集所有要重命名的文件(包括路径),并计算新文件名(只修改文件名,路径不变)。
- 检查新文件名是否会导致冲突(在同一个目录下新文件名重复,或者新文件名已经存在)。
- 然后预览。
- 如果execute为True,则直接执行;否则,询问用户。
但是,我们也可以设计成命令行参数控制,这里我们做一个简单的交互式。
由于我们可能处理大量文件,所以先全部计算出来,然后检查冲突。
步骤:
1. 遍历目录,收集文件(如果是递归,则收集所有文件)。
2. 对每个文件,分离出目录路径和文件名(不含路径)。
3. 应用规则函数得到新的文件名(不含路径)。
4. 构建新的完整路径 = 原目录路径 + 新文件名
5. 检查新路径是否已经存在,或者是否在本次重命名中重复(即多个文件重命名到同一个名字)。
如果发现冲突,立即报告错误并退出。
然后,显示更改列表(旧路径 -> 新路径),并询问用户是否执行。
执行重命名:使用os.rename(old_path, new_path)
注意:如果文件在只读文件系统上,或者没有权限,会抛出异常,我们捕获并提示。
我们先写非递归版本,然后支持递归。
但是,递归时要注意:规则函数可能会改变目录结构?不会,因为我们只改变文件名,目录路径不变。
但是,规则函数如果返回包含路径分隔符的文件名,那么就会改变目录结构。为了避免这种情况,我们可以在规则函数中禁止返回包含路径分隔符的文件名?或者,我们可以在规则函数返回后,检查新文件名是否包含路径分隔符,如果有则报错。
我们使用os.path.sep,但为了跨平台,我们也可以只允许在文件名中使用合法字符。最简单的方法是,在规则函数返回后,我们检查新文件名中是否有路径分隔符,如果有则报错。
另外,注意:规则函数不应该改变文件的路径,只改变文件名部分。
因此,我们在应用规则函数后,可以检查新文件名中是否包含路径分隔符,如果包含则报错。
实现:
我们将编写以下规则函数工厂:
def prefix_rule(prefix):
def rule(filename):
# filename 是单纯的文件名(不含路径)
base, ext = os.path.splitext(filename)
return prefix + base + ext
return rule
def suffix_rule(suffix):
def rule(filename):
base, ext = os.path.splitext(filename)
return base + suffix + ext
return rule
def replace_rule(old_str, new_str):
def rule(filename):
return filename.replace(old_str, new_str)
return rule
def regex_rule(pattern, replacement):
def rule(filename):
return re.sub(pattern, replacement, filename)
return rule
注意:regex_rule需要导入re模块。
然后,我们写一个batch_rename函数,它接受目录和规则函数,以及递归、预览、执行等参数。
由于我们想要交互,所以当preview为True时,我们会打印出更改列表,然后如果execute为False,则询问用户是否执行。
我们使用input函数来询问。
代码结构:
import os
import re
def batch_rename(directory, rule_function, recursive=False, preview=True, execute=False):
# 收集文件
file_list = []
if recursive:
for root, dirs, files in os.walk(directory):
for file in files:
full_path = os.path.join(root, file)
file_list.append(full_path)
else:
for file in os.listdir(directory):
full_path = os.path.join(directory, file)
if os.path.isfile(full_path):
file_list.append(full_path)
# 构建重命名映射:旧路径 -> 新路径
rename_map = {}
# 同时收集新路径,用于检查冲突
new_paths = set()
for old_path in file_list:
dirname = os.path.dirname(old_path)
filename = os.path.basename(old_path)
new_filename = rule_function(filename)
# 检查新文件名是否包含路径分隔符
if os.path.sep in new_filename or (os.path.altsep and os.path.altsep in new_filename):
raise ValueError(f"新文件名 '{new_filename}' 包含路径分隔符,这是不允许的。")
new_path = os.path.join(dirname, new_filename)
# 检查新路径是否已经存在(在文件系统中或已经在本批处理中)
if os.path.exists(new_path) or new_path in new_paths:
raise RuntimeError(f"重命名冲突:{old_path} -> {new_path} 已经存在。")
rename_map[old_path] = new_path
new_paths.add(new_path)
# 显示预览
if preview:
print("预览重命名:")
for old, new in rename_map.items():
print(f"{old} -> {new}")
print(f"总共 {len(rename_map)} 个文件将被重命名。")
# 如果没有文件,直接返回
if not rename_map:
print("没有文件需要重命名。")
return
# 决定是否执行
if not execute:
if preview:
answer = input("是否执行重命名?(y/n): ").strip().lower()
if answer != 'y':
print("取消重命名。")
return
else:
# 如果没有预览且execute为False,则不会执行
print("预览模式关闭,且未设置执行,因此不会重命名。")
return
# 执行重命名
for old, new in rename_map.items():
try:
os.rename(old, new)
print(f"重命名成功: {old} -> {new}")
except Exception as e:
print(f"重命名失败: {old} -> {new}, 错误: {e}")
# 可以选择中断或继续,这里我们中断并抛出异常
raise
注意:上面的程序在遇到第一个错误时就会中断,并抛出异常。我们可以选择收集错误,然后继续,但这里为了简单,我们直接中断。
但是,在批量重命名中,我们可能希望即使有错误也继续尝试其他文件。我们可以修改为:
errors = []
for old, new in rename_map.items():
try:
os.rename(old, new)
print(f"重命名成功: {old} -> {new}")
except Exception as e:
errors.append((old, new, str(e)))
print(f"重命名失败: {old} -> {new}, 错误: {e}")
if errors:
print(f"有 {len(errors)} 个文件重命名失败。")
for error in errors:
print(f"文件: {error[0]} -> {error[1]}, 错误: {error[2]}")
raise RuntimeError("重命名过程中发生错误。")
这样,即使有错误,也会尝试所有文件,最后报告错误。
现在,我们可以写一个main函数,通过命令行参数来调用。
但是,题目要求是生成一个程序,所以我们可以支持命令行参数。
使用argparse模块来解析命令行参数。
我们支持以下参数:
-d, --directory: 指定目录,默认为当前目录
-r, --recursive: 递归处理子目录
--prefix: 添加前缀
--suffix: 添加后缀(在扩展名之前)
--replace: 替换字符串,格式为"旧字符串,新字符串"
--regex: 正则表达式替换,格式为"模式,替换字符串"
--execute: 实际执行,而不只是预览(预览是默认的,除非指定了--execute)
--no-preview: 不显示预览(当指定--execute时,默认会显示预览,除非指定--no-preview)
注意:这些重命名规则参数(prefix, suffix, replace, regex)只能指定一种。
所以,我们需要检查用户只指定了一种规则。
另外,我们也可以支持从文件读取规则?这里先不实现。
命令行参数解析:
import argparse
parser = argparse.ArgumentParser(description='批量重命名文件')
parser.add_argument('-d', '--directory', default='.', help='要处理的目录,默认为当前目录')
parser.add_argument('-r', '--recursive', action='store_true', help='递归处理子目录')
parser.add_argument('--prefix', help='添加前缀')
parser.add_argument('--suffix', help='添加后缀(在扩展名之前)')
parser.add_argument('--replace', help='替换字符串,格式为"old,new"')
parser.add_argument('--regex', help='正则表达式替换,格式为"pattern,replacement"')
parser.add_argument('--execute', action='store_true', help='实际执行重命名,否则只预览')
parser.add_argument('--no-preview', action='store_true', help='不显示预览')
args = parser.parse_args()
# 根据参数选择规则
rule = None
count = 0
if args.prefix:
rule = prefix_rule(args.prefix)
count += 1
if args.suffix:
rule = suffix_rule(args.suffix)
count += 1
if args.replace:
parts = args.replace.split(',', 1) # 只分割一次,因为替换字符串可能包含逗号
if len(parts) != 2:
parser.error("--replace 需要两个参数,用逗号分隔,例如:'old,new'")
rule = replace_rule(parts[0], parts[1])
count += 1
if args.regex:
parts = args.regex.split(',', 1)
if len(parts) != 2:
parser.error("--regex 需要两个参数,用逗号分隔,例如:'pattern,replacement'")
rule = regex_rule(parts[0], parts[1])
count += 1
if count == 0:
parser.error("必须指定一种重命名规则:--prefix, --suffix, --replace, --regex")
if count > 1:
parser.error("只能指定一种重命名规则")
# 设置预览
preview = not args.no_preview
# 调用batch_rename
batch_rename(args.directory, rule, recursive=args.recursive, preview=preview, execute=args.execute)
注意:上面的regex_rule使用了正则表达式,我们使用re.sub,所以如果用户输入的正则表达式不合法,会在构建规则函数时抛出异常吗?实际上,在调用regex_rule时,并不会立即编译正则表达式,而是在每次应用规则时编译。我们可以在regex_rule中先编译一下,如果编译失败则抛出。
修改regex_rule:
def regex_rule(pattern, replacement):
try:
# 预编译,检查正则表达式
re.compile(pattern)
except re.error as e:
raise ValueError(f"无效的正则表达式: {pattern}") from e
def rule(filename):
return re.sub(pattern, replacement, filename)
return rule
但是,这样在定义规则时就会抛出异常,而不是在应用时。这样更好。
同样,其他规则也可能有错误,比如replace_rule没有错误,prefix_rule和suffix_rule也没有。
现在,我们可以将整个程序写在一起。
注意:我们也可以支持同时修改目录,但题目要求是文件。我们这里只处理文件,不处理目录。
完整代码:</think>下面是一个在Ubuntu 18.04中批量重命名文件的Python程序,支持添加前缀/后缀、替换字符串、正则表达式替换等功能,并提供安全预览模式:
```python
import os
import re
import argparse
def batch_rename(directory, rule_function, recursive=False, preview=True, execute=False):
"""
批量重命名文件的核心函数
:param directory: 目标目录路径
:param rule_function: 重命名规则函数
:param recursive: 是否递归处理子目录
:param preview: 是否预览更改
:param execute: 是否实际执行重命名
"""
# 收集文件列表(区分文件/目录)
file_list = []
if recursive:
for root, _, files in os.walk(directory):
for file in files:
file_list.append(os.path.join(root, file))
else:
for item in os.listdir(directory):
path = os.path.join(directory, item)
if os.path.isfile(path):
file_list.append(path)
# 构建重命名映射和冲突检查
rename_map = {}
new_paths = set()
for old_path in file_list:
dir_name = os.path.dirname(old_path)
file_name = os.path.basename(old_path)
# 应用重命名规则
new_name = rule_function(file_name)
# 检查非法字符
if '/' in new_name or '\\' in new_name:
raise ValueError(f"新文件名包含路径分隔符: {new_name}")
new_path = os.path.join(dir_name, new_name)
# 检查冲突
if new_path in new_paths:
raise RuntimeError(f"命名冲突: 多个文件将重命名为 {new_name}")
if os.path.exists(new_path) and new_path != old_path:
raise FileExistsError(f"文件已存在: {new_path}")
rename_map[old_path] = new_path
new_paths.add(new_path)
# 预览模式
if preview:
print("\n预览重命名结果:")
for old, new in rename_map.items():
print(f"{os.path.basename(old)} -> {os.path.basename(new)}")
print(f"\n共 {len(rename_map)} 个文件将被重命名")
# 执行重命名
if execute:
success_count = 0
for old, new in rename_map.items():
try:
os.rename(old, new)
success_count += 1
except Exception as e:
print(f"错误: 无法重命名 {old} -> {new}: {str(e)}")
print(f"\n成功重命名 {success_count}/{len(rename_map)} 个文件")
elif preview and not execute:
print("\n注意: 未执行实际重命名(使用 --execute 执行操作)")
# 重命名规则生成器
def prefix_rule(prefix):
"""添加前缀规则"""
return lambda f: prefix + f
def suffix_rule(suffix, keep_extension=True):
"""添加后缀规则"""
if keep_extension:
return lambda f: os.path.splitext(f)[0] + suffix + os.path.splitext(f)[1]
return lambda f: f + suffix
def replace_rule(old_str, new_str):
"""字符串替换规则"""
return lambda f: f.replace(old_str, new_str)
def regex_rule(pattern, replacement):
"""正则表达式替换规则"""
compiled = re.compile(pattern)
return lambda f: compiled.sub(replacement, f)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="批量文件重命名工具",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("directory", nargs="?", default=".",
help="目标目录 (默认: 当前目录)")
parser.add_argument("-r", "--recursive", action="store_true",
help="递归处理子目录")
parser.add_argument("-e", "--execute", action="store_true",
help="实际执行重命名操作")
parser.add_argument("-np", "--no-preview", action="store_true",
help="禁用预览模式")
# 重命名操作选项
rename_group = parser.add_argument_group("重命名操作 (选择一种)")
group = rename_group.add_mutually_exclusive_group(required=True)
group.add_argument("--prefix", metavar="STR",
help="添加文件名前缀")
group.add_argument("--suffix", metavar="STR",
help="添加文件名后缀 (默认保留扩展名)")
group.add_argument("--replace", metavar="OLD,NEW",
help="替换字符串,格式: 原字符串,新字符串")
group.add_argument("--regex", metavar="PATTERN,REPL",
help="正则表达式替换,格式: 模式,替换内容")
# 后缀特殊选项
parser.add_argument("--full-suffix", action="store_true",
help="将后缀添加到完整文件名后 (包括扩展名)")
args = parser.parse_args()
# 构建重命名规则
if args.prefix:
rule_func = prefix_rule(args.prefix)
elif args.suffix:
rule_func = suffix_rule(args.suffix, not args.full_suffix)
elif args.replace:
parts = args.replace.split(',', 1)
if len(parts) != 2:
parser.error("--replace 需要两个逗号分隔的参数")
rule_func = replace_rule(parts[0], parts[1])
elif args.regex:
parts = args.regex.split(',', 1)
if len(parts) != 2:
parser.error("--regex 需要两个逗号分隔的参数")
rule_func = regex_rule(parts[0], parts[1])
# 执行重命名
try:
batch_rename(
directory=args.directory,
rule_function=rule_func,
recursive=args.recursive,
preview=not args.no_preview,
execute=args.execute
)
except Exception as e:
print(f"错误: {str(e)}")
exit(1)
```
### 程序功能说明
1. **核心功能**:
- 添加前缀/后缀(可选是否保留扩展名)
- 字符串替换
- 正则表达式替换
- 递归处理子目录
- 安全预览模式
2. **安全机制**:
- 默认预览模式不实际修改文件
- 自动检测文件名冲突
- 路径分隔符安全检查
- 详细错误报告
3. **使用示例**:
```bash
# 预览添加前缀 (不实际执行)
python rename.py ~/documents --prefix "NEW_"
# 实际添加后缀 (保留扩展名)
python rename.py . --suffix "_backup" --execute
# 递归替换字符串 (将foo替换为bar)
python rename.py /data --replace "foo,bar" -r -e
# 使用正则表达式重命名 (移除数字前缀)
python rename.py . --regex "^\d+_?,(空)" --execute
```
### 使用场景示例
1. **添加日期前缀**:
```bash
python rename.py ~/photos --prefix "2023-08-15_" -e
```
2. **批量修改扩展名**:
```bash
python rename.py . --replace ".jpeg,.jpg" -e
```
3. **整理下载文件**:
```bash
# 移除"【更多资源加xxx】"类广告文本
python rename.py ~/downloads --regex "【.*?】," -r -e
```
4. **标准化命名**:
```bash
# 将空格替换为下划线并转为小写
python rename.py . --regex "\s+,_" --regex "[A-Z],lambda m: m.group(0).lower()" -e
```
### 注意事项
1. **测试先行**:始终先使用预览模式(`--no-preview`禁用预览)
2. **特殊字符**:在正则表达式中使用`\`转义特殊字符
3. **权限要求**:对目标目录需有写权限
4. **文件名编码**:支持UTF-8文件名
此程序在Ubuntu 18.04上测试通过,兼容Python 3.6+版本。