Python實現C代碼統計工具(一)
標簽: Python 代碼統計
聲明
本文將基於Python2.7腳本語言,實現一個簡易的C代碼統計工具。
本文同時也發布於作業部落,視覺效果略有不同。
一. 問題提出
代碼規模較大時,不易對其做出准確的評估。通過代碼統計工具,可自動分析和統計軟件項目中的文件行數、有效代碼行數、注釋行數及空白行數,提供准確而直觀的代碼量報告。基於這種定量報告,可以有針對性地提升代碼質量。例如,分拆組合以消除巨型文件,對注釋率過低的文件增加注釋信息,等等。
為簡單起見,本文僅僅統計C語言代碼,即后綴名為c或h的文件。並且,約定以下幾條統計原則:
- 當代碼和注釋位於同一行時,代碼行數和注釋行數均會加1。考慮到行注釋的普遍性,因此代碼行數、注釋行數和空白行數的總和通常大於文件總行數。
- 塊注釋中間和空白行計入空白行數。
- "#if 0...endif"塊視為代碼行。
二. 代碼實現
首先,定義兩個存儲統計結果的列表:
rawCountInfo = [0, 0, 0, 0, 0]
detailCountInfo = []
其中,rawCountInfo存儲粗略的文件總行數信息,列表元素依次為文件行、代碼行、注釋行和空白行的總數,以及文件數目。detailCountInfo存儲詳細的統計信息,包括單個文件的行數信息和文件名,以及所有文件的行數總和。這是一個多維列表,存儲內容示例如下:
[['line.c', [33, 19, 15, 4]], ['test.c', [44, 34, 3, 7]]]
以下將給出具體的實現代碼。為避免大段粘貼代碼,以函數為片段簡要描述。
def CalcLines(lineList):
lineNo, totalLines = 0, len(lineList)
codeLines, commentLines, emptyLines = 0, 0, 0
while lineNo < len(lineList):
if lineList[lineNo].isspace(): #空行
emptyLines += 1; lineNo += 1; continue
regMatch = re.match('^([^/]*)/(/|\*)+(.*)$', lineList[lineNo].strip())
if regMatch != None: #注釋行
commentLines += 1
#代碼&注釋混合行
if regMatch.group(1) != '':
codeLines += 1
elif regMatch.group(2) == '*' \
and re.match('^.*\*/.+$', regMatch.group(3)) != None:
codeLines += 1
#行注釋或單行塊注釋
if '/*' not in lineList[lineNo] or '*/' in lineList[lineNo]:
lineNo += 1; continue
#跨行塊注釋
lineNo += 1
while '*/' not in lineList[lineNo]:
if lineList[lineNo].isspace():
emptyLines += 1
else:
commentLines += 1
lineNo = lineNo + 1; continue
commentLines += 1 #'*/'所在行
else: #代碼行
codeLines += 1
lineNo += 1; continue
return [totalLines, codeLines, commentLines, emptyLines]
CalcLines()函數基於C語法判斷文件行屬性,按代碼、注釋或空行分別統計。參數lineList由readlines()讀取文件得到,讀到的每行末尾均含換行符。strip()可剔除字符串首尾的空白字符(包括換行符)。當通過print輸出文件行內容時,可采用如下兩種寫法剔除多余的換行符:
print '%s' %(line), #注意行末逗號
print '%s' %(line.strip())
行尾包含換行符的問題也存在於readline()和read()調用,包括for line in file的語法。對於read()調用,可在讀取文件后split('\n')得到不帶換行符的行列表。注意,調用readlines()和read()時,會讀入整個文件,文件位置指示器將指向文件尾端。此后再調用時,必須先通過file.seek(0)方法返回文件開頭,否則讀取的內容為空。
def CountFileLines(filePath, isRaw=True):
fileExt = os.path.splitext(filePath)
if fileExt[1] != '.c' and fileExt[1] != '.h': #識別C文件
return
try:
fileObj = open(filePath, 'r')
except IOError:
print 'Cannot open file (%s) for reading!', filePath
else:
lineList = fileObj.readlines()
fileObj.close()
if isRaw:
global rawCountInfo
rawCountInfo[:-1] = [x+y for x,y in zip(rawCountInfo[:-1], CalcLines(lineList))]
rawCountInfo[-1] += 1
else:
detailCountInfo.append([filePath, CalcLines(lineList)])
CountFileLines()統計單個文件的行數信息,其參數isRaw指示統計報告是粗略還是詳細的。對於詳細報告,需要向detailCountInfo不斷附加單個文件的統計結果;而對於詳細報告,只需要保證rawCountInfo的元素值正確累加即可。
def ReportCounterInfo(isRaw=True):
#Python2.5版本引入條件表達式(if-else)實現三目運算符,低版本可采用and-or的短路特性
#print 'FileLines CodeLines CommentLines EmptyLines %s' %('' if isRaw else 'FileName')
print 'FileLines CodeLines CommentLines EmptyLines %s' %(not isRaw and 'FileName' or '')
if isRaw:
print '%-11d%-11d%-14d%-12d<Total:%d Files>' %(rawCountInfo[0], rawCountInfo[1],\
rawCountInfo[2], rawCountInfo[3], rawCountInfo[4])
return
total = [0, 0, 0, 0]
#對detailCountInfo按第一列元素(文件名)排序,以提高輸出可讀性
#import operator; detailCountInfo.sort(key=operator.itemgetter(0))
detailCountInfo.sort(key=lambda x:x[0]) #簡潔靈活,但不如operator高效
for item in detailCountInfo:
print '%-11d%-11d%-14d%-12d%s' %(item[1][0], item[1][1], item[1][2], item[1][3], item[0])
total[0] += item[1][0]; total[1] += item[1][1]
total[2] += item[1][2]; total[3] += item[1][3]
print '%-11d%-11d%-14d%-12d<Total:%d Files>' %(total[0], total[1], total[2], total[3], len(detailCountInfo))
ReportCounterInfo()輸出統計報告。注意,詳細報告輸出前,先按文件名排序。
def CountDirLines(dirPath, isRawReport=True):
if not os.path.exists(dirPath):
print dirPath + ' is non-existent!'
return
if not os.path.isdir(dirPath):
print dirPath + ' is not a directory!'
return
for root, dirs, files in os.walk(dirPath):
for file in files:
CountFileLines(os.path.join(root, file), isRawReport)
ReportCounterInfo(isRawReport)
CountDirLines()統計當前目錄及其子目錄下所有文件的行數信息,並輸出統計報告。注意,os.walk()不一定按字母順序遍歷文件。在作者的Windows XP主機上,os.walk()按文件名順序遍歷;而在Linux Redhat主機上,os.walk()以"亂序"遍歷。
最后,添加簡單的命令行處理:
if __name__ == '__main__':
DIR_PATH = r'E:\PyTest\lctest'
if len(sys.argv) == 1: #腳本名
CountDirLines(DIR_PATH)
sys.exit()
if len(sys.argv) >= 2:
if int(sys.argv[1]):
CountDirLines(DIR_PATH, False)
else:
CountDirLines(DIR_PATH)
sys.exit()
三. 效果驗證
為驗證上節的代碼實現,建立lctest調試目錄。該目錄下包含line.c及和《為C函數自動添加跟蹤語句》一文中的test.c文件。其中,line.c內容如下:
#include <stdio.h>
/* {{{ comment */
/***********
Multiline
Comment
***********/
int test(int a/*comment*/, int b)
{
int a2; int b2; //comment
a2 = 1;
b2 = 2;
}
/* {{{ test3 */
int test3(int a,
int b) /*test2 has been deleted,
so this is test3. */
{int a3 = 1; int b3 = 2;
if(a3)
{/*comment*/
a3 = 0;
}
//comment
b3 = 0;
}
/* }}} */
//comment //comment
/*FALSE*/ #if M_DEFINED
#error Defination!
#endif
以不同的命令行參數運行CLineCounter.py,輸出如下:
E:\PyTest>CLineCounter.py
FileLines CodeLines CommentLines EmptyLines
77 53 18 11 <Total:2 Files>
E:\PyTest>CLineCounter.py 0
FileLines CodeLines CommentLines EmptyLines
77 53 18 11 <Total:2 Files>
E:\PyTest>CLineCounter.py 1
FileLines CodeLines CommentLines EmptyLines FileName
33 19 15 4 E:\PyTest\lctest\line.c
44 34 3 7 E:\PyTest\lctest\test.c
77 53 18 11 <Total:2 Files>
經人工校驗,統計信息正確。
接着,在實際工程中運行python CLineCounter.py 1,截取部分運行輸出如下:
[wangxiaoyuan_@localhost ~]$ python CLineCounter.py 1
FileLines CodeLines CommentLines EmptyLines FileName
99 21 58 24 /sdb1/wangxiaoyuan/include/Dsl_Alloc.h
120 79 28 24 /sdb1/wangxiaoyuan/include/Dsl_Backtrace.h
... ... ... ... ... ... ... ...
139 89 24 26 /sdb1/wangxiaoyuan/source/Dsl_Tbl_Map.c
617 481 64 78 /sdb1/wangxiaoyuan/source/Dsl_Test_Suite.c
797 569 169 82 /sdb1/wangxiaoyuan/source/xDSL_Common.c
15450 10437 3250 2538 <Total:40 Files>
四. 后記
本文所實現的C代碼統計工具較為簡陋,后續將重構代碼並添加控制選項。
