逐步提升程序质量的演变过程示例,提升演变过程示例, 图片文件批量重命名工具


如何编写高质量的程序呢? 在《Web服务端软件的的服务品质概要》阐述了程序的常见质量属性及实现策略方法,本文将通过一个 Python 实现的图片文件批量重命名工具来演示如何逐步提升程序质量。

图片文件批量重命名工具实现的功能是:将指定目录 /home/user/path/to/photos/(xxx.png,yyy.png) 下的图片批量重命名为 prefix0001.png, prefix0002.png, …

 

雏形

首先,可以编写出一个基本可用的程序 batchrename_basic.py 。这个程序并不完美,但是可以完成最初的任务。注意到 生成编号使用了闭包,这是为了将生成编号的过程抽离出来成为一个可复用的过程,而这个过程无法预知需要生成怎样的列表,因此每次仅返回一个编号;程序如下:

# -*- coding: cp936 -*- 
import os 
import os.path as PathUtil

def createDesignator(num, bits):
    return str(num).zfill(bits)

def number_generator(start_num=0, bits=4):
    start = []
    start.append(start_num)
    def inner():
        start[0] = start[0] + 1
        return createDesignator(start[0], bits)
    return inner

def batchrename(dir_path, prefix="IMG_",generator_func=number_generator()):
    '''
    rename files (such as xxx.[jpg, png, etc]) in the directory specified by dir_path to [prefix][designator].[jpg, png, etc], designator is generated by generator_func
    '''
    names = os.listdir(dir_path)
    for filename in names:
        old_filename = PathUtil.join(dir_path,filename)
        if PathUtil.isfile(old_filename)==True: 
            newname=prefix.upper() + generator_func() + '.' + getFileSuffix(filename)
            os.rename(old_filename,PathUtil.join(dir_path,newname))

def getFileSuffix(filename):
    try:
        sep_ind = filename.index('.')
        return filename[sep_ind+1:]
    except ValueError:
        return None

def testGetFileSuffix():
    assert getFileSuffix("good.jpg") == "jpg"
    assert getFileSuffix("good") is None
    print "testGetFileSuffix Passed."

def testNumberGenerator():
    geneNums = []
    generator = number_generator()
    for i in range(10):
        geneNums.append(generator()) 
    assert geneNums[0] == '0001'
    assert geneNums[1] == '0002'
    assert geneNums[9] == '0010'
    print 'testNumberGenerator Passed.'

if __name__ == '__main__':

    testGetFileSuffix()
    testNumberGenerator()

    dir_path = '/home/lovesqcc/setupdir/scitools/pic/mmnet/beauty'
    batchrename(dir_path, prefix="beauty_")

 

健壮性

健壮性体现了程序应对错误的能力。一个需要网络连接的 APP 在网络正常的情况下运行流畅,如果没有网络呢? 就必须告知用户先连接到网络才行。或者采用输入自动纠错。比如在搜索引擎里搜索 jquery, 不小心写成了 jqeury 。搜索引擎会提示是否需要搜索的是 jquery。在此例中,当路径不存在时,就会报错。

Traceback (most recent call last):
  File "batchrename_robust.py", line 57, in 
    batchrename(dir_path, prefix="beauty_")
  File "batchrename_robust.py", line 21, in batchrename
    names = os.listdir(dir_path)
OSError: [Errno 2] No such file or directory: '/home/lovesqcc/setupdir/scitools/pic/mmnet/beauty'

解决方法很简单: 将  names = os.listdir(dir_path) 抽离出来,写成一个函数并进行异常捕获,然后该行改写成 names = getDirFiles(dir_path):

def getDirFiles(dir_path):
    try:
        return os.listdir(dir_path)
    except OSError, err:
        print 'No Such Directory: %s, exit.' % dir_path
        os._exit(1)

 

可定制性

如果用户想指定路径和前缀,就必须在程序里修改并重新部署,显然是比较“僵硬”的。控制台程序通常要加上命令行参数,而实际应用则使用配置文件。下面通过使用 argparse 模块给该程序添加命令行参数,使之具备可定制性。添加一个 parseArgs 方法, 并修改 main 即可。注意到,使用了元组来清晰表达所希望返回的参数格式,便于主程序使用; 魔数均用字符串常量来表达,保证可维护性。

使用方式: $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ -p fz2  -m NUM 1 5

-p, -m 都是可选的。默认只需要指定目录路径。

import argparse

DEFAULT_PREFIX = 'IMG_'
DEFAULT_START_NUM = 1
DEFAULT_BITS = 4
NUM_METHOD = 'NUM'

def parseArgs():
    description = 'This program is used to batch rename files in the given DIRECTORY to PREFIX_GeneratedDesignator. GeneratedDesignator is a BITS number counting from START_NUM to the number of files (etc. PREFIX0001,PREFIX0002,...) in the given DIRECTORY with leading zero if necessary.'
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('DIRECTORY', help='Given directory name is required')
    parser.add_argument('-p','--prefix',nargs='?', default="IMG_", help='Given renamed prefix')
    parser.add_argument('-m','--method',nargs='*',help='method to generate designator, etc --m [NUM [START_NUM [BITS]]]')
    args = parser.parse_args()
    dir_path = args.DIRECTORY

    if args.prefix:
        prefix = args.prefix
    else:
        prefix = DEFAULT_PREFIX

    if not args.method:
       method = NUM_METHOD
       start_num = DEFAULT_START_NUM
       bits = DEFAULT_BITS
       return (dir_path, prefix, (method, start_num, bits))

    if type(args.method) == list:
        if len(args.method) == 0:
            method = NUM_METHOD
            start_num = DEFAULT_START_NUM
            bits = DEFAULT_BITS
        elif args.method[0] ==NUM_METHOD:
            method = NUM_METHOD
            if len(args.method) == 1:
                start_num = DEFAULT_START_NUM
                bits = DEFAULT_BITS
            elif len(args.method) == 2:
                start_num = int(args.method[1])
                bits = DEFAULT_BITS
            elif len(args.method) == 3:
                start_num = int(args.method[1])
                bits = int(args.method[2])

    return (dir_path, prefix, (method, start_num-1, bits))
if __name__ == '__main__':

    testGetFileSuffix()
    testNumberGenerator()

    (dir_path, prefix, (method, start_num, bits)) = parseArgs()
    if method == NUM_METHOD:
        number_generator = number_generator(start_num, bits)
    batchrename(dir_path, prefix, number_generator)

 

可追踪性

可追踪性体现了程序运行过程的可知性和可监控性。记录程序运行中的关键状态和关键路径,也非常有利于出现错误时进行调试。在此例中,要将文件重命名的具体信息记录下来,简便起见,程序中只是打印一下:

os.rename(old_filename,PathUtil.join(dir_path,newname))
print '%s rename to %s.' % (filename, newname)  # should be info log

安全性

安全性通常表达两层含义: 1. 程序绝对不能破坏用户的数据; 2. 程序必须防止其它程序破坏用户数据或窥探用户隐私。其中第一条是不可触犯的。当我们重复运行  $ python batchrename_robust_customized.py /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ 时,会惊讶地发现,重命名后文件变少了!当运行足够次后,文件可能只剩下一个! 这是怎么回事呢? 运行若干次之后,截取一次结果如下:

IMG_0006.png rename to IMG_0002.png.
IMG_0003.jpg rename to IMG_0003.jpg.
IMG_0002.png rename to IMG_0004.png.
IMG_0005.jpg rename to IMG_0005.jpg.
IMG_0007.png rename to IMG_0006.png.

稍作分析即可知道, Python os.rename 在 UnixSystem 上会默认覆盖已存在的文件,而 os.listdir 输出的结果是无序的! 解决方案也很简单:先将 os.listdir 输出的结果排序后再重命名,即要修改 getDirFiles:

def getDirFiles(dir_path):
    try:
        filenames = os.listdir(dir_path)
        filenames.sort()
        return filenames
    except OSError:
        print 'No Such Directory: %s, exit.' % dir_path
        os._exit(1)

 

可复用性

可复用性的关键是单一职责原则和接口定义正交。单一职责原则指一个函数或方法仅做一件小事,望名知义;接口定义正交是说每个函数、类接口定义的事情没有重叠,可以组合实现非常灵活的功能。如果程序具备较好的可复用性,那么,在扩展程序时也会获得益处,将改动影响局部化。在编写程序时应时时考虑抽离出可复用的过程和方法。可复用性也有助于编写更有效的单元测试。此例中正是遵循可复用性原则来编写程序,使得每次改动仅涉及一小部分。

 

可移植性

写程序是为了更好更广泛地使用。可移植性需要:1. 检测操作平台; 2. 将特定操作系统的符号和特定操作系统的行为替换成平台无关的。在本例中,要将路径分隔符 / 修改为 os.sep. Windows下的使用方式: D:>python batchrename.py -d F:picfuzhuangfz2 -p fz2 -m NUM 1 6

batchrename(dir_path+ os.sep +filename, prefix, generator_func)

可扩展性

可扩展性体现了程序应对需求变化的能力。对于此例,可扩展性体现在四点: 1. 要对目录的子目录递归重命名; 2. 要对多个目录使用不同前缀进行批量重命名;3. 支持不同的编号生成方式;4. 对于非图片文件的批量重命名。 对于第一点,只需要修改 batchrename 方法即可,检测到如果是目录,则递归调用 batchrename ; 对于第二点,则需要修改命令行参数格式,增加 -d 参数,参数个数至少一个;修改 -p 参数,参数可为零到多个。如果给定目录数大于给定前缀,则使用最后一个前缀将前缀数补足;若给定目录数小于前缀数,则将从后数多余的前缀忽略。要修改 parseArgs 和 main;对于第三点,则要将生成编号的方式抽离成可复用的过程,使得每次仅返回一个编号;对于第四点,由于没有对文件类型做判断,因此也是适合于非图片文件的。最终的程序如下所示, 使用方式:

$ python batchrename_robust_customized_extended.py -d /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz2/ /home/lovesqcc/setupdir/scitools/pic/fuzhuang/fz1  -p fz2 fz1 -m NUM 1 5

# -*- coding: cp936 -*- 
import os 
import os.path as PathUtil
import argparse

DEFAULT_PREFIX = 'IMG_'
DEFAULT_START_NUM = 1
DEFAULT_BITS = 4
NUM_METHOD = 'NUM'

def parseArgs():
    description = 'This program is used to batch rename files in the given DIRECTORY to PREFIX_GeneratedDesignator. GeneratedDesignator is a BITS number counting from START_NUM to the number of files (etc. PREFIX0001,PREFIX0002,...) in the given DIRECTORY with leading zero if necessary.'
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('-d','--directories', nargs='+', help='Given directory name is at least one required')
    parser.add_argument('-p','--prefix',nargs='*', help='Given renamed prefix')
    parser.add_argument('-m','--method',nargs='*',help='method to generate designator, etc --m [NUM [START_NUM [BITS]]]')
    args = parser.parse_args()
    dir_path_list = args.directories
    dir_num = len(args.directories)

    if not args.prefix or len(args.prefix) == 0:
        prefix_list = [DEFAULT_PREFIX] * dir_num
        prefix_num = dir_num
    else:
        prefix_list = args.prefix
        prefix_num = len(args.prefix)

    if prefix_num > dir_num:
        prefix_list = prefix_list[0:dir_num]
    else:
        prefix_list.extend([prefix_list[prefix_num-1]]*(dir_num-prefix_num))

    if not args.method:
       method = NUM_METHOD
       start_num = DEFAULT_START_NUM
       bits = DEFAULT_BITS
       return (dir_path_list, prefix_list, (method, start_num, bits))

    if type(args.method) == list:
        if len(args.method) == 0:
            method = NUM_METHOD
            start_num = DEFAULT_START_NUM
            bits = DEFAULT_BITS
        elif args.method[0] ==NUM_METHOD:
            method = NUM_METHOD
            if len(args.method) == 1:
                start_num = DEFAULT_START_NUM
                bits = DEFAULT_BITS
            elif len(args.method) == 2:
                start_num = int(args.method[1])
                bits = DEFAULT_BITS
            elif len(args.method) == 3:
                start_num = int(args.method[1])
                bits = int(args.method[2])

    return (dir_path_list, prefix_list, (method, start_num-1, bits))

def createDesignator(num, bits):
    return str(num).zfill(bits)

def number_generator(start_num=0, bits=4):
    start = []
    start.append(start_num)
    def inner():
        start[0] = start[0] + 1
        return createDesignator(start[0], bits)
    return inner

def getDirFiles(dir_path):
    try:
        filenames = os.listdir(dir_path)
        filenames.sort()
        return filenames
    except OSError:
        print 'No Such Directory: %s, exit.' % dir_path
        os._exit(1)

def batchrename(dir_path, prefix=DEFAULT_PREFIX ,generator_func=number_generator()):
    '''
    rename files (such as xxx.[jpg, png, etc]) in the directory specified by dir_path to [prefix][designator].[jpg, png, etc], designator is generated by generator_func
    '''
    names = getDirFiles(dir_path)
    for filename in names:
        old_filename = PathUtil.join(dir_path,filename)
        if PathUtil.isfile(old_filename)==True: 
            newname=prefix.upper() + generator_func() + '.' + getFileSuffix(filename)
            os.rename(old_filename,PathUtil.join(dir_path,newname))
            print '%s rename to %s.' % (filename, newname)  # should be info log
        else:
            batchrename(dir_path+os.sep+filename, prefix, generator_func)

def getFileSuffix(filename):
    try:
        sep_ind = filename.index('.')
        return filename[sep_ind+1:]
    except ValueError:
        return None

def testGetFileSuffix():
    assert getFileSuffix("good.jpg") == "jpg"
    assert getFileSuffix("good") is None
    print "testGetFileSuffix Passed."

def testNumberGenerator():
    geneNums = []
    generator = number_generator()
    for i in range(10):
        geneNums.append(generator()) 
    assert geneNums[0] == '0001'
    assert geneNums[1] == '0002'
    assert geneNums[9] == '0010'
    print 'testNumberGenerator Passed.'

if __name__ == '__main__':

    testGetFileSuffix()
    testNumberGenerator()

    (dir_path_list, prefix_list, (method, start_num, bits)) = parseArgs()

    dir_num = len(dir_path_list)
    for i in range(dir_num):
        if method == NUM_METHOD:
            number_generator_func = number_generator(start_num, bits)
        batchrename(dir_path_list[i], prefix_list[i], number_generator_func)

 

性能成本

程序员有追求高效的强迫症。想象这是一个 web 服务, 性能成本通常体现在响应速度和吞吐量。响应速度是用户可感知的,影响到用户体验;吞吐量是用户不可感知的,影响到服务成本。此例中可以考虑百万个文件的重命名;影响效率的因素有两个: 1. 文件名排序时间; 2.  rename 系统调用时间。对于前者,使用快速排序,或者使用更精细的方法在 batchrename 函数中解决 os.rename 默认覆盖已存在文件的问题(这样会降低可维护性); 对于后者,如果编程平台或系统调用提供了更高效的批量重命名接口,则可批量调用该接口来完成任务。

结语

提高程序质量并非一蹴而就,而是可以通过渐进的方式来实现。当实现了一个基本可用的程序时,还处于一个起点,有必要问问自己:

1.  健壮性: 程序需要怎样的运行环境和输入参数? 如果运行环境不满足或输入参数不合法,程序该如何应对?

2.  可定制性: 程序有哪些参数或特性是可定制的? 切忌在代码里写死;

3.  可追踪性: 程序有哪些关键运行状态和关键运行路径? 使用 info 日志记录下来;

4.  安全性: 程序在何种情况下可能破坏用户的数据? 程序如何禁止非法程序破坏或窥探用户数据?

5.  可扩展性: 程序可能有哪些变化的潜在合理的需求?

6.  可复用性: 函数方法是否臃肿,可以从中抽离出可复用的子过程?

7.  可测试性: 关键函数和方法是否有充分的单元测试?

8.  性能成本: 响应速度是否在用户接受范围内?是否可以在不降低可维护性的前提下优化局部,提高整体吞吐量? 对于大数据量,程序是否可以应对? 程序的吞吐量极限是多少?

相关内容

    暂无相关文章

评论关闭