一個圖片文件批量重命名工具的質量改善過程

 

  如何編寫高質量的程序呢? 在《Web服務端軟件的的服務品質概要》闡述了程序的常見質量屬性及實現策略方法,本文將經過一個 Python 實現的圖片文件批量重命名工具來演示如何逐步提高程序質量。html

  圖片文件批量重命名工具實現的功能是:將指定目錄 /home/user/path/to/photos/(xxx.png,yyy.png) 下的圖片批量重命名爲 prefix0001.png, prefix0002.png, ...python

 

      雛形程序員

  首先,能夠編寫出一個基本可用的程序 batchrename_basic.py 。這個程序並不完美,可是能夠完成最初的任務。注意到 生成編號使用了閉包,這是爲了將生成編號的過程抽離出來成爲一個可複用的過程,而這個過程沒法預知須要生成怎樣的列表,所以每次僅返回一個編號;程序以下:  web

# -*- 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在網絡信號不好的地方會沒法使用、須要配置文件的程序找不到指定的配置、通訊鏈接中斷、查詢不到指定的數據、接收的參數不合法或非法等。此時,程序的行爲是可以預估到這些情景並優雅處理和返回,仍是嘎地中斷,就體現了程序的健壯和編程者的素養。通常狀況下,對於可以預估的極可能產生的情形,好比參數不合法、指定數據查詢不到,能夠經過條件來判斷、識別和處理;而對於沒法預料的情形,好比網絡中斷,就捕獲異常處理。在此例中,須要指定一個目錄路徑。當目錄路徑不存在時,就會拋出異常:安全

Traceback (most recent call last):
  File "batchrename_basic.py", line 57, in <module>
    batchrename(dir_path, prefix="beauty_")
  File "batchrename_basic.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)

      實際上,這種解法是濫用異常的例子。 異常應該是沒法預料的情形纔去捕獲,確保萬無一失。而指定目錄路徑不存在,徹底是一個能夠預料的情景。正確的作法是: 在調用 batchrename(dir_path, prefix="beauty_") 以前先判斷路徑 dir_path 是否存在且合法。 若是存在且合法,就進行後續的動做;不然退出程序。 不過話說回來,若是判斷路徑是否存在且合法的系統庫調用會拋出異常,那麼這裏仍是須要捕獲異常。另外,爲了代碼更清晰些,有些開發者傾向於不管什麼錯誤所有拋出異常,而後在高層的某個地方作統一處理。怎麼處理錯誤是個仁者見仁智者見智的問題,可是捕獲並處理錯誤是一個達成共識的作法。閉包

 

     可定製性app

     當程序上線後,遇到的迴應多是:是否能夠根據客戶的實際須要作特定的修改和處理,便可定製。

  若是用戶想指定路徑和前綴,就必須在程序裏修改並從新部署,顯然是比較「僵硬」的。控制檯程序一般要加上命令行參數,而實際應用則使用配置文件。下面經過使用 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)

 

  可追蹤性

    可追蹤性體現了程序運行過程的可知性和可監控性。程序老是會潛藏或多或少的BUG。當用戶數據出現問題須要排查時,有效的日誌會是很是好的幫手。有效的日誌一般指記錄程序運行中的關鍵狀態和關鍵路徑。在此例中,要將文件重命名的具體信息記錄下來,簡便起見,程序中只是打印一下: 

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

      

     安全性

     安全性是一個性質很是敏感的質量屬性,很容易形成嚴重的故障, 不可不察。

   安全性一般表達三層含義: 1. 程序絕對不能破壞用戶的數據; 2. 某個用戶的數據不能未經受權地被其餘用戶獲取到; 3. 程序必須防止其它程序破壞用戶數據或窺探用戶隱私。其中第一條是不可觸犯的。當咱們重複運行  $ 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) 獲取文件列表: 根據給定目錄獲取目錄下的全部文件名稱列表;

     (3) 獲取文件後綴: 根據文件名稱獲取文件後綴;

     (4) 生成編號: 根據指定編號生成方式,生成符合條件的重命名編號;

     (5) 批量重命名: 將舊的文件名重命名爲新的重命名名稱。

    此例正是遵循可複用性原則來編寫程序,使得每次改動僅涉及一小部分。

 

     可移植性

     寫程序是爲了更好更普遍地使用。可移植性須要:1. 檢測操做平臺; 2. 將特定操做系統的符號和特定操做系統的行爲替換成平臺無關的。在本例中,要將路徑分隔符 / 修改成 os.sep. Windows下的使用方式: D:\>python batchrename.py -d F:\pic\fuzhuang\fz2 -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)

 

      易用性 

      易用性的討論可參見文章 《程序與軟件的易用性》。在本例的命令行程序中,適用的法則是: 1. 若用戶輸入 -h, --help, 展現該程序的具體用法和選項;2.  若用戶直接輸入程序名稱,那麼提示用戶必須輸入目錄名稱,並提示該程序的具體用法和選項。

 

    性能成本

  程序員有追求高效的強迫症。想象這是一個 web 服務, 性能成本一般體如今響應速度和吞吐量。響應速度是用戶可感知的,影響到用戶體驗;吞吐量是用戶不可感知的,影響到服務成本。此例中能夠考慮百萬個文件的重命名;影響效率的因素有兩個: 1. 文件名排序時間; 2.  rename 系統調用時間。對於前者,使用快速排序,或者使用更精細的方法在 batchrename 函數中解決 os.rename 默認覆蓋已存在文件的問題(這樣會下降可維護性); 對於後者,若是編程平臺或系統調用提供了更高效的批量重命名接口,則可批量調用該接口來完成任務。

 

  結語

  提升程序質量並不是一蹴而就,而是能夠經過漸進的方式來實現。當實現了一個基本可用的程序時,還處於一個起點,有必要問問本身:

  1.  健壯性: 程序須要怎樣的運行環境和輸入參數? 若是運行環境不知足或輸入參數不合法,程序該如何應對?

  2.  可定製性: 程序有哪些參數或特性是可定製的? 切忌在代碼裏寫死;

  3.  可追蹤性: 程序有哪些關鍵運行狀態和關鍵運行路徑? 使用 info 和 error 日誌記錄下來;

  4.  安全性: 程序在何種狀況下可能破壞用戶的數據? 程序如何禁止非法程序破壞或窺探用戶數據?

      5.  可複用性: 模塊劃分是否正交清晰? 函數方法的實現是否臃腫,能夠從中抽離出可複用的子過程?

  6.  可擴展性: 程序可能有哪些變化的潛在合理的需求? 

  7.  可測試性: 關鍵函數和方法是否有充分的單元測試?

      8.  易用性:  程序是否容易使用,能讓用戶迅速理解和正確使用?

  9.  性能成本: 響應速度是否在用戶接受範圍內?是否能夠在不下降可維護性的前提下優化局部,提升總體吞吐量? 對於大數據量,程序是否能夠應對? 程序的吞吐量極限是多少?

      這九大質量屬性是程序上線後能成爲有價值的工具和產品服務所應當努力追求的。固然,很難達到全部質量屬性的完善程度。在軟件發展的各個階段,始終面臨着各個質量屬性的權衡。

      最初上線: 覆蓋基本場景的健壯性 + 可接受的性能成本 + 基本的安全性 + 基本的可追蹤性 + 符合大衆習慣的易用性 + 基本的可複用性 + 基本的可擴展性 + 基本的測試

      成長: 從產品上優化易用性 + 從業務發展和功能設計上提高可擴展性 + 覆蓋更多場景的健壯性 + 適當的測試提高 + 安全性提高 

      發展: 更優的可擴展性 +  更優的性能成本 + 兼容原有功能設計與體驗 + 提高測試效率和質量 + 安全性提高

      成熟: 高度的安全性 + 更優的性能 + 完善的可運維可監控 + 適度的可擴展性 + 完善的測試鏈

 

  轉載請註明出處。謝謝 :)

相關文章
相關標籤/搜索