1. 程式人生 > >Python每日一題:第6題:如何設計一個程式碼統計工具

Python每日一題:第6題:如何設計一個程式碼統計工具

這是Python之禪和他朋友們在知識星球的第6題:如何設計一個程式碼統計工具

問題

設計一個程式,用於統計一個專案中的程式碼行數,包括檔案個數,程式碼行數,註釋行數,空行行數。儘量設計靈活一點可以通過輸入不同引數來統計不同語言的專案,例如:

# type用於指定檔案型別
python counter.py --type python

輸出:

files:10
code_lines:200
comments:100
blanks:20

分析

這是一個看起來很簡單,但做起來有點複雜的設計題,我們可以把問題化小,只要能正確統計一個檔案的程式碼行數,那麼統計一個目錄也不成問題,其中最複雜的就是關於多行註釋,以 Python 為例,註釋程式碼行有如下幾種情況:

1、井號開頭的單行註釋

# 單行註釋

2、多行註釋符在同一行的情況

"""這是多行註釋"""
'''這也是多行註釋'''

3、多行註釋符

"""
這3行都是註釋符
"""

我們的思路採取逐行解析的方式,多行註釋需要一個額外的識別符號in_multi_comment 來標識當前行是不是處於多行註釋符當中,預設為 False,多行註釋開始時,置為 True,遇到下一個多行註釋符時置為 False。從多行註釋開始符號直到下一個結束符號之間的程式碼都應該屬於註釋行。

知識點

如何正確讀取檔案,讀出的檔案當字串處理時,字串的常用方法

簡化版

我們逐步進行迭代,先實現一個簡化版程式,只統計Python程式碼的單檔案,而且不考慮多行註釋的情況,這是任何入門 Python 的人都能實現的功能。關鍵地方是把每一行讀出來之後,先用 strip() 方法把字串兩邊的空格、回車去掉

# -*- coding: utf-8 -*-
"""
只能統計單行註釋的py檔案
"""
def parse(path):
    comments = 0
    blanks = 0
    codes = 0
    with open(path, encoding='utf-8') as f:
        for line in f.readlines():
            line = line.strip()
            if line == "":
                blanks += 1
            elif line.startswith
("#"): comments += 1 else: codes += 1 return {"comments": comments, "blanks": blanks, "codes": codes} if __name__ == '__main__': print(parse("xxx.py"))

多行註釋版

如果只能統計單行註釋的程式碼,意義並不大,要解決多行註釋的統計才能算是一個真正的程式碼統計器

# -*- coding: utf-8 -*-
"""
可以統計包含有多行註釋的py檔案
"""
def parse(path):
    in_multi_comment = False  # 多行註釋符識別符號號
    comments = 0
    blanks = 0
    codes = 0
    with open(path, encoding="utf-8") as f:
        for line in f.readlines():
            line = line.strip()

            # 多行註釋中的空行當做註釋處理
            if line == "" and not in_multi_comment:
                blanks += 1
            # 註釋有4種
            # 1. # 井號開頭的單行註釋
            # 2. 多行註釋符在同一行的情況
            # 3. 多行註釋符之間的行
            elif line.startswith("#") or \
                            (line.startswith('"""') and line.endswith('"""') and len(line)) > 3 or \
                    (line.startswith("'''") and line.endswith("'''") and len(line) > 3) or \
                    (in_multi_comment and not (line.startswith('"""') or line.startswith("'''"))):
                comments += 1
            # 4. 多行註釋符的開始行和結束行
            elif line.startswith('"""') or line.startswith("'''"):
                in_multi_comment = not in_multi_comment
                comments += 1
            else:
                codes += 1
    return {"comments": comments, "blanks": blanks, "codes": codes}
if __name__ == '__main__':
    print(parse("xxx.py"))

上面的第4種情況,遇到多行註釋符號時,in_multi_comment 識別符號進行取反操作是關鍵操作,而不是單純地置為 False 或 True,第一次遇到 """ 時為True,第二次遇到 """ 就是多行註釋的結束符,取反為False,以此類推,第三次又是開始,取反又是True。

那麼判斷其它語言是不是要重新寫一個解析函式呢?如果你仔細觀察的話,多行註釋的4種情況可以抽象出4個判斷條件,因為大部分語言都有單行註釋,多行註釋,只是他們的符號不一樣而已。

CONF = {"py": {"start_comment": ['"""', "'''"], "end_comment": ['"""', "'''"], "single": "#"},
        "java": {"start_comment": ["/*"], "end_comment": ["*/"], "single": "//"}}

start_comment = CONF.get(exstansion).get("start_comment")
end_comment = CONF.get(exstansion).get("end_comment")
cond2 = False
cond3 = False
cond4 = False
for index, item in enumerate(start_comment):
    cond2 = line.startswith(item) and line.endswith(end_comment[index]) and len(line) > len(item)
    if cond2:
        break
for item in end_comment:
    if line.startswith(item):
        cond3 = True
        break

for item in start_comment+end_comment:
    if line.startswith(item):
        cond4 = True
        break

if line == "" and not in_multi_comment:
    blanks += 1
# 註釋有4種
# 1. # 井號開頭的單行註釋
# 2. 多行註釋符在同一行的情況
# 3. 多行註釋符之間的行

elif line.startswith(CONF.get(exstansion).get("single")) or cond2 or \
        (in_multi_comment and not cond3):
    comments += 1
# 4. 多行註釋符分佈在多行時,開始行和結束行
elif cond4:
    in_multi_comment = not in_multi_comment
    comments += 1
else:
    codes += 1

只需要一個配置常量把所有語言的單行、多行註釋的符號標記出來,對應出 cond1到cond4幾種情況就ok。剩下的任務就是解析多個檔案,可以用 os.walk 方法。

def counter(path):
    """
    可以統計目錄或者某個檔案
    :param path:
    :return:
    """
    if os.path.isdir(path):
        comments, blanks, codes = 0, 0, 0
        list_dirs = os.walk(path)
        for root, dirs, files in list_dirs:
            for f in files:
                file_path = os.path.join(root, f)
                stats = parse(file_path)
                comments += stats.get("comments")
                blanks += stats.get("blanks")
                codes += stats.get("codes")
        return {"comments": comments, "blanks": blanks, "codes": codes}
    else:
        return parse(path)

當然,想要把這個程式做完善,還有很多工作要多,包括命令列解析,根據指定引數只解析某一種語言。


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪