动机在于下载的剧集或是专辑的命名往往有着比较杂乱的命名方式,或者因为一些原因看着心烦,于是想借助python编写一个批处理脚本来批量重命名一个文件夹下的系列文件。

Step0: 涉及知识

本程序基于python3,未测试python2下的运行情况,但未使用复杂的语法,完全可以自行修改移植。

  • Python的基本语法(循环与基础数据结构list、tuple、dict)
  • os模块
  • 正则表达式(re模块)

Step1: 需求分析

  1. 以输入的字符串为前置字符串,提取原文件中的序号和后缀,重命名文件
  2. 删除指定字符串
  3. 既可以修改文件名,也可以格式化后缀名:后缀名全部变为小写(针对.Mp3、.MKV等表示,虽然并不影响文件的识别)

Step2: 框架分析

  1. 首先需要定位工作目录,支持外部输入,不输入时以文件当前目录作为工作目录;获取目录中的文件列表,并除去本脚本文件
  2. 目标字段提取
  3. 遍历待修改文件名列表,逐一修改文件名

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’.* - (.*)’

从中得出的几点:

  1. ()中的是要提取的内容
  2. . 表示任意字符, * 表示任意数量, \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

在实际使用中我发现有时还需要提取两个字符串,并交换它们的位置,功能接口的插入问题我在代码中已经注释出来了,相应的代码还请容我偷个懒。