本文对于先前系列文章中实现的C/Python代码统计工具(CPLineCounter),通过C扩展接口重写核心算法加以优化,并与网上常见的统计工具做对比。实测表明,CPLineCounter在统计精度和性能方面均优于其他同类统计工具。以千万行代码为例评测性能,CPLineCounter在Cpython和Pypy环境下运行时,比国外统计工具cloc1.64分别快14.5倍和29倍,比国内SourceCounter3.4分别快1.8倍和3.6倍。
运行测试环境
本文基于Windows系统平台,运行和测试所涉及的代码实例。平台信息如下:
>>> import sys, platform >>> print '%s %s, Python %s' %(platform.system(), platform.release(), platform.python_version()) Windows XP, Python 2.7.11 >>> sys.version '2.7.11 (v2.7.11:6d1b6a68f775, Dec 5 2015, 20:32:19) [MSC v.1500 32 bit (Intel)]'</div>
注意,Python不同版本间语法存在差异,故文中某些代码实例需要稍作修改,以便在低版本Python环境中运行。
一. 代码实现与优化
为避免碎片化,本节将给出完整的实现代码。注意,本节某些变量或函数定义与先前系列文章中的实现存在细微差异,请注意甄别。
1.1 代码实现
首先,定义两个存储统计结果的列表:
import os, sys rawCountInfo = [0, 0, 0, 0, 0] detailCountInfo = []</div>
其中,rawCountInfo存储粗略的文件总行数信息,列表元素依次为文件行、代码行、注释行和空白行的总数,以及文件数目。detailCountInfo存储详细的统计信息,包括单个文件的行数信息和文件名,以及所有文件的行数总和。
以下将给出具体的实现代码。为避免大段粘贴代码,以函数为片段简要描述。
def CalcLinesCh(line, isBlockComment):
lineType, lineLen = 0, len(line)
if not lineLen:
return lineType
line = line + '\n' #添加一个字符防止iChar+1时越界
iChar, isLineComment = 0, False
while iChar < lineLen:
if line[iChar] == ' ' or line[iChar] == '\t': #空白字符
iChar += 1; continue
elif line[iChar] == '/' and line[iChar+1] == '/': #行注释
isLineComment = True
lineType |= 2; iChar += 1 #跳过'/'
elif line[iChar] == '/' and line[iChar+1] == '*': #块注释开始符
isBlockComment[0] = True
lineType |= 2; iChar += 1
elif line[iChar] == '*' and line[iChar+1] == '/': #块注释结束符
isBlockComment[0] = False
lineType |= 2; iChar += 1
else:
if isLineComment or isBlockComment[0]:
lineType |= 2
else:
lineType |= 1
iChar += 1
return lineType #Bitmap:0空行,1代码,2注释,3代码和注释
def CalcLinesPy(line, isBlockComment):
#isBlockComment[single quotes, double quotes]
lineType, lineLen = 0, len(line)
if not lineLen:
return lineType
line = line + '\n\n' #添加两个字符防止iChar+2时越界
iChar, isLineComment = 0, False
while iChar < lineLen:
if line[iChar] == ' ' or line[iChar] == '\t': #空白字符
iChar += 1; continue
elif line[iChar] == '#': #行注释
isLineComment = True
lineType |= 2
elif line[iChar:iChar+3] == "'''": #单引号块注释
if isBlockComment[0] or isBlockComment[1]:
isBlockComment[0] = False
else:
isBlockComment[0] = True
lineType |= 2; iChar += 2
elif line[iChar:iChar+3] == '"""': #双引号块注释
if isBlockComment[0] or isBlockComment[1]:
isBlockComment[1] = False
else:
isBlockComment[1] = True
lineType |= 2; iChar += 2
else:
if isLineComment or isBlockComment[0] or isBlockComment[1]:
lineType |= 2
else:
lineType |= 1
iChar += 1
return lineType #Bitmap:0空行,1代码,2注释,3代码和注释
</div>
CalcLinesCh()和CalcLinesPy()函数分别基于C和Python语法判断文件行属性,按代码、注释或空行分别统计。
from ctypes import c_uint, c_ubyte, CDLL
CFuncObj = None
def LoadCExtLib():
try:
global CFuncObj
CFuncObj = CDLL('CalcLines.dll')
except Exception: #不捕获系统退出(SystemExit)和键盘中断(KeyboardInterrupt)异常
pass
def CalcLines(fileType, line, isBlockComment):
try:
#不可将CDLL('CalcLines.dll')放于本函数内,否则可能严重拖慢执行速度
bCmmtArr = (c_ubyte * len(isBlockComment))(*isBlockComment)
CFuncObj.CalcLinesCh.restype = c_uint
if fileType is 'ch': #is(同一性运算符)判断对象标识(id)是否相同,较==更快
lineType = CFuncObj.CalcLinesCh(line, bCmmtArr)
else:
lineType = CFuncObj.CalcLinesPy(line, bCmmtArr)
isBlockComment[0] = True if bCmmtArr[0] else False
isBlockComment[1] = True if bCmmtArr[1] else False
#不能采用以下写法,否则本函数返回后isBlockComment列表内容仍为原值
#isBlockComment = [True if i else False for i in bCmmtArr]
except Exception, e:
#print e
if fileType is 'ch':
lineType = CalcLinesCh(line, isBlockComment)
else:
lineType = CalcLinesPy(line, isBlockComment)
return lineType
</div>
为提升运行速度,作者将CalcLinesCh()和CalcLinesPy()函数用C语言重写,并编译生成动态链接库。这两个函数的C语言版本实现和使用详见1.2小节。LoadCExtLib()和CalcLines()函数旨在加载该动态链接库并执行相应的C版本统计函数,若加载失败则执行较慢的Python版本统计函数。
上述代码运行于CPython环境,且C动态库通过Python2.5及后续版本内置的ctypes模块加载和执行。该模块作为Python的外部函数库,提供与C语言兼容的数据类型,并允许调用DLL或共享库中的函数。因此,ctypes常被用来在纯Python代码中封装(wrap)外部动态库。
若代码运行于Pypy环境,则需使用cffi接口调用C程序:
from cffi import FFI
CFuncObj, ffiBuilder = None, FFI()
def LoadCExtLib():
try:
global CFuncObj
ffiBuilder.cdef('''
unsigned int CalcLinesCh(char *line, unsigned char isBlockComment[2]);
unsigned int CalcLinesPy(char *line, unsigned char isBlockComment[2]);
''')
CFuncObj = ffiBuilder.dlopen('CalcLines.dll')
except Exception: #不捕获系统退出(SystemExit)和键盘中断(KeyboardInterrupt)异常
pass
def CalcLines(fileType, line, isBlockComment):
try:
bCmmtArr = ffiBuilder.new('unsigned char[2]', isBlockComment)
if fileType is 'ch': #is(同一性运算符)判断对象标识(id)是否相同,较==更快
lineType = CFuncObj.CalcLinesCh(line, bCmmtArr)
else:
lineType = CFuncObj.CalcLinesPy(line, bCmmtArr)
isBlockComment[0] = True if bCmmtArr[0] else False
isBlockComment[1] = True if bCmmtArr[1] else False
#不能采用以下写法,否则本函数返回后isBlockComment列表内容仍为原值
#isBlockComment = [True if i else False for i in bCmmtArr]
except Exception, e:
#print e
if fileType is 'ch':
lineType = CalcLinesCh(line, isBlockComment)
else:
lineType = CalcLinesPy(line, isBlockComment)
return lineType
</div>
cffi用法类似ctypes,但允许直接加载C文件来调用里面的函数(在解释过程中自动编译)。此处为求统一,仍使用加载动态库的方式。
def SafeDiv(dividend, divisor):
if divisor: return float(dividend)/divisor
elif dividend: return -1
else: return 0
gProcFileNum = 0
def CountFileLines(filePath, isRawReport=True, isShortName=False):
fileExt = os.path.splitext(filePath)
if fileExt[1] == '.c' or fileExt[1] == '.h':
fileType = 'ch'
elif fileExt[1] == '.py': #==(比较运算符)判断对象值(value)是否相同
fileType = 'py'
else:
return
global gProcFileNum; gProcFileNum += 1
sys.stderr.write('%d files processed...\r'%gProcFileNum)
isBlockComment = [False]*2 #或定义为全局变量,以保存上次值
lineCountInfo = [0]*5 #[代码总行数, 代码行数, 注释行数, 空白行数, 注释率]
with open(filePath, 'r') as file:
for line in file:
lineType = CalcLines(fileType, line.strip(), isBlockComment)
lineCountInfo[0] += 1
if lineType == 0: lineCountInfo[3] += 1
elif lineType == 1: lineCountInfo[1] += 1
elif lineType == 2: lineCountInfo[2] += 1
elif lineType == 3: lineCountInfo[1] += 1; lineCountInfo[2] += 1

