动机在于下载的剧集或是专辑的命名往往有着比较杂乱的命名方式,或者因为一些原因看着心烦,于是想借助python编写一个批处理脚本来批量重命名一个文件夹下的系列文件。
Step0: 涉及知识
本程序基于python3,未测试python2下的运行情况,但未使用复杂的语法,完全可以自行修改移植。
- Python的基本语法(循环与基础数据结构list、tuple、dict)
- os模块
- 正则表达式(re模块)
Step1: 需求分析
- 以输入的字符串为前置字符串,提取原文件中的序号和后缀,重命名文件
- 删除指定字符串
- 既可以修改文件名,也可以格式化后缀名:后缀名全部变为小写(针对.Mp3、.MKV等表示,虽然并不影响文件的识别)
Step2: 框架分析
- 首先需要定位工作目录,支持外部输入,不输入时以文件当前目录作为工作目录;获取目录中的文件列表,并除去本脚本文件
- 目标字段提取
- 遍历待修改文件名列表,逐一修改文件名
Step3: 模块实现
1. 主要的遍历过程
def traversal(work_path_='', info_=info_str_):
"""
由于会使用默认模式串和期望前缀名,所以必须要提前设置
:param work_path_: [str] 脚本工作目录
:param info_: [str] 正则模板
:return: None
"""
# step1 根据work_path_获取文件列表,返回文件名列表allfiles
# step2 根据正则模板筛选文件列表,将提取的字段存入列表info_str
# step2.1 希望能检查一下筛选结果,最好是将原名称和将来修改后的名称全部表示出来
# step2.2 如果不满意,及时终止
# step3 执行重命名,打印结果
2. 获取待修改文件名列表
本来没什么问题,直到我拿windows下写的文件到Linux下用的时候出现了问题,所以加了if … elif … 的操作系统判断。
def path_input(path_str_=''):
"""
输入要批量重命名的工作目录,可以直接回车定位在当前目录
:param path_str_: [str]
:return: [str] 工作空间路径
"""
import os
import platform
if path_str_=='': path_str_ = input("请输入工作目录【直接回车则为当前目录】:")
if (platform.system()=='Windows') :
if len(path_str_) > 0:
if path_str_[-1] != '\\': path_str_ += os.sep
print("在 " + path_str_ + " 进行处理")
else:
print("在 当前目录 进行处理")
path_str_ = '.\\'
# elif (platform.system()=='Linux') :
# if len(path_str_) > 0:
# if path_str_[-1] != r'/': path_str_ += os.sep
# print("在 " + path_str_ + " 进行处理")
# else:
# print("在 当前目录 进行处理")
# path_str_ = r'./'
return path_str_
3. 正则解析
这里要求要了解基本的正则表达,我的日常处理对象是一些连续剧或者专辑歌曲,实际操作下来发现要处理的类型无外乎字符和数字,比如:
- 从 “[www.dreamv5com]H.J.EP14.540p-SHD” 中提取 14
pattern = r’.*EP(\d\d).*$’ - 从 “[www.xingkcc]E01.540p@XingkTV” 中提取 E01
pattern = r’.*](\D\d\d)…*$’ - 从 “06 - 平行宇宙.flac” 中保留 歌名
pattern = r’.* - (.*)’
从中得出的几点:
- ()中的是要提取的内容
- . 表示任意字符, * 表示任意数量, \d表示0-9中的一个字符, \D表示0-9之外的一个字符,\w表示一个文字字符,同样\W表示文字字符之外的任意一个字符;.*的组合表示任意数量的任意字符,一般用来填充字段;$表示字符串末尾
- 想学更多关于正则表达式可以参考这个网站。
def pattern_filter(nameList_, pattern_=''):
"""
遍历nameList_中所有文件名,筛选出满足info_str_的文件名
调用changeSuffix,将后缀名改为小写。 通过修改changeSuffix可以修改文件后缀名
:param nameList_: [list] 待匹配的文件名
:param pattern_: [str] 模板
:return: [list of Ternary Tuple] 第一个是原文件名,第二个是后缀名,最后一个是匹配的字符串列表
"""
import re
new_names = []
for each in nameList_:
# 首先要排除本脚本文件,这步导致不能对.py文件进行处理
if each[-3:] == '.py':continue
num = each.rfind('.')
if num < 1: continue # -1 = folder, 0 = Linux下的隐藏文件
else:
suffix = each[num:] # 后缀名
prefix = each[:num] # 原文件名, 从中提取匹配字符串
# 搜索匹配
# print(prefix) # 显示原文件名
ss = re.findall(pattern_, prefix) # 注意匹配结果是一个list
if ss: # 提取成功时,list非空
if len(ss[0])==1 : # ss被存为字符串
print("从 [" +prefix+ "]\t中提取出\t[" + str(ss) + "]")
elif len(ss[0])>1: # ss被存为tuple
print("从 [" +prefix+ "]\t中提取出\t", ss )
new_names.append( (prefix, suffix, id[0]) )
return new_names
4. 完成循环遍历
def traversal(work_path_='', info_=info_str_):
"""
由于会使用默认模式串和期望前缀名,所以必须要提前设置
:param work_path_: [str] path of workspace
:param info_: [str] 正则模板
:return: None
"""
import os, sys
global prefix_, DELIMS
# step1 根据work_path_获取文件列表,返回文件名列表allfiles
if work_path_ == '': work_path_ = path_input()
allfiles = os.listdir(work_path_)
# step2 根据正则模板筛选文件列表,将提取的字段存入列表info_str
target_files = pattern_filter(allfiles, info_)
if target_files != []:
# 旧文件名
OldFileNameList = [each[0]+each[1] for each in target_files]
print(target_files[0])
# 提取到了文件中的特殊信息,现在按照自己想要的方式重新组织名称
# TODO 这里可以进一步拓展功能
if prefix_ == '':
prefix_ = input(r"输入期望的前缀名(如有必要,请包含E):")
NewFileNameList = [prefix_+each[2]+each[1] for each in target_files]
# step2.1 希望能检查一下筛选结果,最好是将原名称和将来修改后的名称全部表示出来
print(r"即将执行如下重命名:")
for o, n in zip(OldFileNameList, NewFileNameList):
print(o+
' \t--> |' + # 最后的文件名形式
n)
# step2.2 如果不满意,及时终止
flag = input(r"如需取消执行,请按0,否则请按除0外的任意键:")
if flag == '0' or flag == 'o' or flag == 'O': sys.exit()
else:
input("没有找到匹配的文件,回车结束程序")
sys.exit()
# step3 执行重命名,打印结果
print("="*40)
for o, n in zip(OldFileNameList, NewFileNameList):
os.rename(work_path_+o, work_path_+n)
print('已经重命名为 '+n)
5. Finally
最后自然是在mian中调用核心过程了。
if __name__ == '__main__':
# 务必确保输入的路径以"\"或"/"结尾, window下的'\'需要'\\'
traversal()
实际在用的时候其实在文件开头还有点小细节没有赘述,比如traversal中的 info_str_ 其实就是针对特定对象自己写的正则模板。
又比如为了在脚本中使用中文需要加上:
# coding:utf-8
在实际使用中我发现有时还需要提取两个字符串,并交换它们的位置,功能接口的插入问题我在代码中已经注释出来了,相应的代码还请容我偷个懒。