加入收藏 | 设为首页 | 会员中心 | 我要投稿 安卓应用网 (https://www.0791zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Python > 正文

Python实现代码统计工具(终极篇)

发布时间:2020-05-24 03:07:03 所属栏目:Python 来源:互联网
导读:本文对于先前系列文章中实现的C/Python代码统计工具(CPLineCounter),通过C扩展接口重写核心算法加以优化,并与网上常见的统计工具做对比。实测表明,CPLineCounter在统计精度和性能方面均优于其他同类统计工具。以千

本文对于先前系列文章中实现的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)]' 

注意,Python不同版本间语法存在差异,故文中某些代码实例需要稍作修改,以便在低版本Python环境中运行。
一. 代码实现与优化
 为避免碎片化,本节将给出完整的实现代码。注意,本节某些变量或函数定义与先前系列文章中的实现存在细微差异,请注意甄别。 
1.1 代码实现
首先,定义两个存储统计结果的列表: 

import os,sys
rawCountInfo = [0,0]
detailCountInfo = [] 

其中,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,len(line)
 if not lineLen:
  return lineType

 line = line + 'nn' #添加两个字符防止iChar+2时越界
 iChar,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代码和注释 

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 

为提升运行速度,作者将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,isBlockComment):
 try:
  bCmmtArr = ffiBuilder.new('unsigned char[2]',isBlockComment)
  if fileType is 'ch': #is(同一性运算符)判断对象标识(id)是否相同,较==更快
   lineType = CFuncObj.CalcLinesCh(line,isBlockComment)

 return lineType 

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
   else:
    assert False,'Unexpected lineType: %d(0~3)!' %lineType

 if isRawReport:
  global rawCountInfo
  rawCountInfo[:-1] = [x+y for x,y in zip(rawCountInfo[:-1],lineCountInfo[:-1])]
  rawCountInfo[-1] += 1
 elif isShortName:
  lineCountInfo[4] = SafeDiv(lineCountInfo[2],lineCountInfo[2]+lineCountInfo[1])
  detailCountInfo.append([os.path.basename(filePath),lineCountInfo])
 else:
  lineCountInfo[4] = SafeDiv(lineCountInfo[2],lineCountInfo[2]+lineCountInfo[1])
  detailCountInfo.append([filePath,lineCountInfo]) 

注意"%d files processed..."进度提示。因无法判知输出是否通过命令行重定向至文件(sys.stdout不变,sys.argv不含">out"),该进度提示将换行写入输出文件内。假定代码文件数目为N,输出文件内将含N行进度信息。目前只能利用重定向缺省只影响标准输出的特点,将进度信息由标准错误输出至控制台;同时增加-o选项,以显式地区分标准输出和文件写入,降低使用者重定向的可能性。 

此外,调用CalcLines()函数时通过strip()方法剔除文件行首尾的空白字符。因此,CalcLinesCh()和CalcLinesPy()内无需行结束符判断分支。

SORT_ORDER = (lambda x:x[0],False)
def SetSortArg(sortArg=None):
 global SORT_ORDER
 if not sortArg:
  return
 if any(s in sortArg for s in ('file','0')): #条件宽松些
 #if sortArg in ('rfile','file','r0','0'):
  keyFunc = lambda x:x[1][0]
 elif any(s in sortArg for s in ('code','1')):
  keyFunc = lambda x:x[1][1]
 elif any(s in sortArg for s in ('cmmt','2')):
  keyFunc = lambda x:x[1][2]
 elif any(s in sortArg for s in ('blan','3')):
  keyFunc = lambda x:x[1][3]
 elif any(s in sortArg for s in ('ctpr','4')):
  keyFunc = lambda x:x[1][4]
 elif any(s in sortArg for s in ('name','5')):
  keyFunc = lambda x:x[0]
 else: #因argparse内已限制排序参数范围,此处也可用assert
  print >>sys.stderr,'Unsupported sort order(%s)!' %sortArg
  return

 isReverse = sortArg[0]=='r' #False:升序(ascending); True:降序(decending)
 SORT_ORDER = (keyFunc,isReverse)

def ReportCounterInfo(isRawReport=True,stream=sys.stdout):
  #代码注释率 = 注释行 / (注释行+有效代码行)
 print >>stream,'FileLines CodeLines CommentLines BlankLines CommentPercent %s'
   %(not isRawReport and 'FileName' or '')

 if isRawReport:
  print >>stream,'%-11d%-11d%-14d%-12d%-16.2f<Total:%d Code Files>' %(rawCountInfo[0],
    rawCountInfo[1],rawCountInfo[2],rawCountInfo[3],
    SafeDiv(rawCountInfo[2],rawCountInfo[2]+rawCountInfo[1]),rawCountInfo[4])
  return

 total = [0,0]
 #对detailCountInfo排序。缺省按第一列元素(文件名)升序排序,以提高输出可读性。
 detailCountInfo.sort(key=SORT_ORDER[0],reverse=SORT_ORDER[1])
 for item in detailCountInfo:
  print >>stream,'%-11d%-11d%-14d%-12d%-16.2f%s' %(item[1][0],item[1][1],item[1][2],
    item[1][3],item[1][4],item[0])
  total[0] += item[1][0]; total[1] += item[1][1]
  total[2] += item[1][2]; total[3] += item[1][3]
 print >>stream,'-' * 90 #输出90个负号(minus)或连字号(hyphen)
 print >>stream,'%-11d%-11d%-14d%-12d%-16.2f<Total:%d Code Files>' 
   %(total[0],total[1],total[2],total[3],
   SafeDiv(total[2],total[2]+total[1]),len(detailCountInfo)) 

(编辑:安卓应用网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读