欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python實(shí)現(xiàn)代碼統(tǒng)計(jì)工具(終極篇)

 更新時(shí)間:2016年07月04日 10:53:15   作者:clover_toeic  
這篇文章主要介紹了Python實(shí)現(xiàn)代碼統(tǒng)計(jì)工具的相關(guān)資料,供大家參考,感興趣的小伙伴們可以參考一下

本文對(duì)于先前系列文章中實(shí)現(xiàn)的C/Python代碼統(tǒng)計(jì)工具(CPLineCounter),通過(guò)C擴(kuò)展接口重寫核心算法加以優(yōu)化,并與網(wǎng)上常見的統(tǒng)計(jì)工具做對(duì)比。實(shí)測(cè)表明,CPLineCounter在統(tǒng)計(jì)精度和性能方面均優(yōu)于其他同類統(tǒng)計(jì)工具。以千萬(wàn)行代碼為例評(píng)測(cè)性能,CPLineCounter在Cpython和Pypy環(huán)境下運(yùn)行時(shí),比國(guó)外統(tǒng)計(jì)工具cloc1.64分別快14.5倍和29倍,比國(guó)內(nèi)SourceCounter3.4分別快1.8倍和3.6倍。

運(yùn)行測(cè)試環(huán)境
本文基于Windows系統(tǒng)平臺(tái),運(yùn)行和測(cè)試所涉及的代碼實(shí)例。平臺(tái)信息如下: 

>>> 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不同版本間語(yǔ)法存在差異,故文中某些代碼實(shí)例需要稍作修改,以便在低版本Python環(huán)境中運(yùn)行。
一. 代碼實(shí)現(xiàn)與優(yōu)化
 為避免碎片化,本節(jié)將給出完整的實(shí)現(xiàn)代碼。注意,本節(jié)某些變量或函數(shù)定義與先前系列文章中的實(shí)現(xiàn)存在細(xì)微差異,請(qǐng)注意甄別。 
1.1 代碼實(shí)現(xiàn)
首先,定義兩個(gè)存儲(chǔ)統(tǒng)計(jì)結(jié)果的列表: 

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

其中,rawCountInfo存儲(chǔ)粗略的文件總行數(shù)信息,列表元素依次為文件行、代碼行、注釋行和空白行的總數(shù),以及文件數(shù)目。detailCountInfo存儲(chǔ)詳細(xì)的統(tǒng)計(jì)信息,包括單個(gè)文件的行數(shù)信息和文件名,以及所有文件的行數(shù)總和。 

以下將給出具體的實(shí)現(xiàn)代碼。為避免大段粘貼代碼,以函數(shù)為片段簡(jiǎn)要描述。

 def CalcLinesCh(line, isBlockComment):
 lineType, lineLen = 0, len(line)
 if not lineLen:
  return lineType

 line = line + '\n' #添加一個(gè)字符防止iChar+1時(shí)越界
 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 #跳過(guò)'/'
  elif line[iChar] == '/' and line[iChar+1] == '*': #塊注釋開始符
   isBlockComment[0] = True
   lineType |= 2; iChar += 1
  elif line[iChar] == '*' and line[iChar+1] == '/': #塊注釋結(jié)束符
   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' #添加兩個(gè)字符防止iChar+2時(shí)越界
 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] == "'''": #單引號(hào)塊注釋
   if isBlockComment[0] or isBlockComment[1]:
    isBlockComment[0] = False
   else:
    isBlockComment[0] = True
   lineType |= 2; iChar += 2
  elif line[iChar:iChar+3] == '"""': #雙引號(hào)塊注釋
   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()函數(shù)分別基于C和Python語(yǔ)法判斷文件行屬性,按代碼、注釋或空行分別統(tǒng)計(jì)。

 from ctypes import c_uint, c_ubyte, CDLL
CFuncObj = None
def LoadCExtLib():
 try:
  global CFuncObj
  CFuncObj = CDLL('CalcLines.dll')
 except Exception: #不捕獲系統(tǒng)退出(SystemExit)和鍵盤中斷(KeyboardInterrupt)異常
  pass

def CalcLines(fileType, line, isBlockComment):
 try:
  #不可將CDLL('CalcLines.dll')放于本函數(shù)內(nèi),否則可能嚴(yán)重拖慢執(zhí)行速度
  bCmmtArr = (c_ubyte * len(isBlockComment))(*isBlockComment)
  CFuncObj.CalcLinesCh.restype = c_uint
  if fileType is 'ch': #is(同一性運(yùn)算符)判斷對(duì)象標(biāo)識(shí)(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
  #不能采用以下寫法,否則本函數(shù)返回后isBlockComment列表內(nèi)容仍為原值
  #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 

為提升運(yùn)行速度,作者將CalcLinesCh()和CalcLinesPy()函數(shù)用C語(yǔ)言重寫,并編譯生成動(dòng)態(tài)鏈接庫(kù)。這兩個(gè)函數(shù)的C語(yǔ)言版本實(shí)現(xiàn)和使用詳見1.2小節(jié)。LoadCExtLib()和CalcLines()函數(shù)旨在加載該動(dòng)態(tài)鏈接庫(kù)并執(zhí)行相應(yīng)的C版本統(tǒng)計(jì)函數(shù),若加載失敗則執(zhí)行較慢的Python版本統(tǒng)計(jì)函數(shù)。 

上述代碼運(yùn)行于CPython環(huán)境,且C動(dòng)態(tài)庫(kù)通過(guò)Python2.5及后續(xù)版本內(nèi)置的ctypes模塊加載和執(zhí)行。該模塊作為Python的外部函數(shù)庫(kù),提供與C語(yǔ)言兼容的數(shù)據(jù)類型,并允許調(diào)用DLL或共享庫(kù)中的函數(shù)。因此,ctypes常被用來(lái)在純Python代碼中封裝(wrap)外部動(dòng)態(tài)庫(kù)。 

若代碼運(yùn)行于Pypy環(huán)境,則需使用cffi接口調(diào)用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: #不捕獲系統(tǒng)退出(SystemExit)和鍵盤中斷(KeyboardInterrupt)異常
  pass

def CalcLines(fileType, line, isBlockComment):
 try:
  bCmmtArr = ffiBuilder.new('unsigned char[2]', isBlockComment)
  if fileType is 'ch': #is(同一性運(yùn)算符)判斷對(duì)象標(biāo)識(shí)(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
  #不能采用以下寫法,否則本函數(shù)返回后isBlockComment列表內(nèi)容仍為原值
  #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 

cffi用法類似ctypes,但允許直接加載C文件來(lái)調(diào)用里面的函數(shù)(在解釋過(guò)程中自動(dòng)編譯)。此處為求統(tǒng)一,仍使用加載動(dòng)態(tài)庫(kù)的方式。

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': #==(比較運(yùn)算符)判斷對(duì)象值(value)是否相同
  fileType = 'py'
 else:
  return

 global gProcFileNum; gProcFileNum += 1
 sys.stderr.write('%d files processed...\r'%gProcFileNum)

 isBlockComment = [False]*2 #或定義為全局變量,以保存上次值
 lineCountInfo = [0]*5  #[代碼總行數(shù), 代碼行數(shù), 注釋行數(shù), 空白行數(shù), 注釋率]
 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..."進(jìn)度提示。因無(wú)法判知輸出是否通過(guò)命令行重定向至文件(sys.stdout不變,sys.argv不含">out"),該進(jìn)度提示將換行寫入輸出文件內(nèi)。假定代碼文件數(shù)目為N,輸出文件內(nèi)將含N行進(jìn)度信息。目前只能利用重定向缺省只影響標(biāo)準(zhǔn)輸出的特點(diǎn),將進(jìn)度信息由標(biāo)準(zhǔn)錯(cuò)誤輸出至控制臺(tái);同時(shí)增加-o選項(xiàng),以顯式地區(qū)分標(biāo)準(zhǔn)輸出和文件寫入,降低使用者重定向的可能性。 

此外,調(diào)用CalcLines()函數(shù)時(shí)通過(guò)strip()方法剔除文件行首尾的空白字符。因此,CalcLinesCh()和CalcLinesPy()內(nèi)無(wú)需行結(jié)束符判斷分支。

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內(nèi)已限制排序參數(shù)范圍,此處也可用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, 0, 0]
 #對(duì)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個(gè)負(fù)號(hào)(minus)或連字號(hào)(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)) 

ReportCounterInfo()輸出統(tǒng)計(jì)報(bào)告。注意,詳細(xì)報(bào)告輸出前,會(huì)根據(jù)指定的排序規(guī)則對(duì)輸出內(nèi)容排序。此外,空白行術(shù)語(yǔ)由EmptyLines改為BlankLines。前者表示該行除行結(jié)束符外不含任何其他字符,后者表示該行只包含空白字符(空格、制表符和行結(jié)束符等)。 

為支持同時(shí)統(tǒng)計(jì)多個(gè)目錄和(或)文件,使用ParseTargetList()解析目錄-文件混合列表,將其元素分別存入目錄和文件列表: 

def ParseTargetList(targetList):
 fileList, dirList = [], []
 if targetList == []:
  targetList.append(os.getcwd())
 for item in targetList:
  if os.path.isfile(item):
   fileList.append(os.path.abspath(item))
  elif os.path.isdir(item):
   dirList.append(os.path.abspath(item))
  else:
   print >>sys.stderr, "'%s' is neither a file nor a directory!" %item
 return [fileList, dirList] 

LineCounter()函數(shù)基于目錄和文件列表進(jìn)行統(tǒng)計(jì): 

def CountDir(dirList, isKeep=False, isRawReport=True, isShortName=False):
 for dir in dirList:
  if isKeep:
   for file in os.listdir(dir):
    CountFileLines(os.path.join(dir, file), isRawReport, isShortName)
  else:
   for root, dirs, files in os.walk(dir):
    for file in files:
     CountFileLines(os.path.join(root, file), isRawReport, isShortName)

def CountFile(fileList, isRawReport=True, isShortName=False):
 for file in fileList:
  CountFileLines(file, isRawReport, isShortName)

def LineCounter(isKeep=False, isRawReport=True, isShortName=False, targetList=[]):
 fileList, dirList = ParseTargetList(targetList)
 if fileList != []:
  CountFile(fileList, isRawReport, isShortName)
 if dirList != []:
  CountDir(dirList, isKeep, isRawReport, isShortName) 

然后,添加命令行解析處理:

import argparse
def ParseCmdArgs(argv=sys.argv):
 parser = argparse.ArgumentParser(usage='%(prog)s [options] target',
      description='Count lines in code files.')
 parser.add_argument('target', nargs='*',
   help='space-separated list of directories AND/OR files')
 parser.add_argument('-k', '--keep', action='store_true',
   help='do not walk down subdirectories')
 parser.add_argument('-d', '--detail', action='store_true',
   help='report counting result in detail')
 parser.add_argument('-b', '--basename', action='store_true',
   help='do not show file\'s full path')
## sortWords = ['0', '1', '2', '3', '4', '5', 'file', 'code', 'cmmt', 'blan', 'ctpr', 'name']
## parser.add_argument('-s', '--sort',
##  choices=[x+y for x in ['','r'] for y in sortWords],
##  help='sort order: {0,1,2,3,4,5} or {file,code,cmmt,blan,ctpr,name},' \
##    "prefix 'r' means sorting in reverse order")
 parser.add_argument('-s', '--sort',
   help='sort order: {0,1,2,3,4,5} or {file,code,cmmt,blan,ctpr,name}, ' \
    "prefix 'r' means sorting in reverse order")
 parser.add_argument('-o', '--out',
   help='save counting result in OUT')
 parser.add_argument('-c', '--cache', action='store_true',
   help='use cache to count faster(unreliable when files are modified)')
 parser.add_argument('-v', '--version', action='version',
   version='%(prog)s 3.0 by xywang')

 args = parser.parse_args()
 return (args.keep, args.detail, args.basename, args.sort, args.out, args.cache, args.target) 

注意ParseCmdArgs()函數(shù)中增加的-s選項(xiàng)。該選項(xiàng)指定輸出排序方式,并由r前綴指定升序還是降序。例如,-s 0或-s file表示輸出按文件行數(shù)升序排列,-s r0或-s rfile表示輸出按文件行數(shù)降序排列。
-c緩存選項(xiàng)最適用于改變輸出排序規(guī)則時(shí)。為支持該選項(xiàng),使用Json模塊持久化統(tǒng)計(jì)報(bào)告:

CACHE_FILE = 'Counter.dump'
CACHE_DUMPER, CACHE_GEN = None, None

from json import dump, JSONDecoder
def CounterDump(data):
 global CACHE_DUMPER
 if CACHE_DUMPER == None:
  CACHE_DUMPER = open(CACHE_FILE, 'w')
 dump(data, CACHE_DUMPER)

def ParseJson(jsonData):
 endPos = 0
 while True:
  jsonData = jsonData[endPos:].lstrip()
  try:
   pyObj, endPos = JSONDecoder().raw_decode(jsonData)
   yield pyObj
  except ValueError:
   break

def CounterLoad():
 global CACHE_GEN
 if CACHE_GEN == None:
  CACHE_GEN = ParseJson(open(CACHE_FILE, 'r').read())

 try:
  return next(CACHE_GEN)
 except StopIteration, e:
  return []

def shouldUseCache(keep, detail, basename, cache, target):
 if not cache: #未指定啟用緩存
  return False

 try:
  (_keep, _detail, _basename, _target) = CounterLoad()
 except (IOError, EOFError, ValueError): #緩存文件不存在或內(nèi)容為空或不合法
  return False

 if keep == _keep and detail == _detail and basename == _basename \
  and sorted(target) == sorted(_target):
  return True
 else:
  return False 

注意,json持久化會(huì)涉及字符編碼問(wèn)題。例如,當(dāng)源文件名包含gbk編碼的中文字符時(shí),文件名寫入detailCountInfo前應(yīng)通過(guò)unicode(os.path.basename(filePath), 'gbk')轉(zhuǎn)換為Unicode,否則dump時(shí)會(huì)報(bào)錯(cuò)。幸好,只有測(cè)試用的源碼文件才可能包含中文字符。因此,通常不用考慮編碼問(wèn)題。 

此時(shí),可調(diào)用以上函數(shù)統(tǒng)計(jì)代碼并輸出報(bào)告: 

def main():
 global gIsStdout, rawCountInfo, detailCountInfo
 (keep, detail, basename, sort, out, cache, target) = ParseCmdArgs()
 stream = sys.stdout if not out else open(out, 'w')
 SetSortArg(sort); LoadCExtLib()
 cacheUsed = shouldUseCache(keep, detail, basename, cache, target)
 if cacheUsed:
  try:
   (rawCountInfo, detailCountInfo) = CounterLoad()
  except (EOFError, ValueError), e: #不太可能出現(xiàn)
   print >>sys.stderr, 'Unexpected Cache Corruption(%s), Try Counting Directly.'%e
   LineCounter(keep, not detail, basename, target)
 else:
  LineCounter(keep, not detail, basename, target)

 ReportCounterInfo(not detail, stream)
 CounterDump((keep, detail, basename, target))
 CounterDump((rawCountInfo, detailCountInfo)) 

為測(cè)量行數(shù)統(tǒng)計(jì)工具的運(yùn)行效率,還可添加如下計(jì)時(shí)代碼:

if __name__ == '__main__':
 from time import clock
 startTime = clock()
 main()
 endTime = clock()
 print >>sys.stderr, 'Time Elasped: %.2f sec.' %(endTime-startTime) 

為避免cProfile開銷,此處使用time.clock()測(cè)量耗時(shí)。
 1.2 代碼優(yōu)化
 CalcLinesCh()和CalcLinesPy()除len()函數(shù)外并未使用其他Python庫(kù)函數(shù),因此很容易改寫為C實(shí)現(xiàn)。其C語(yǔ)言版本實(shí)現(xiàn)最初如下: 

#include <stdio.h>
#include <string.h>
#define TRUE 1
#define FALSE 0

unsigned int CalcLinesCh(char *line, unsigned char isBlockComment[2]) {
 unsigned int lineType = 0;
 unsigned int lineLen = strlen(line);
 if(!lineLen)
  return lineType;

 char *expandLine = calloc(lineLen + 1/*\n*/, 1);
 if(NULL == expandLine)
  return lineType;
 memmove(expandLine, line, lineLen);
 expandLine[lineLen] = '\n'; //添加一個(gè)字符防止iChar+1時(shí)越界

 unsigned int iChar = 0;
 unsigned char isLineComment = FALSE;
 while(iChar < lineLen) {
  if(expandLine[iChar] == ' ' || expandLine[iChar] == '\t') { //空白字符
   iChar += 1; continue;
  }
  else if(expandLine[iChar] == '/' && expandLine[iChar+1] == '/') { //行注釋
   isLineComment = TRUE;
   lineType |= 2; iChar += 1; //跳過(guò)'/'
  }
  else if(expandLine[iChar] == '/' && expandLine[iChar+1] == '*') { //塊注釋開始符
   isBlockComment[0] = TRUE;
   lineType |= 2; iChar += 1;
  }
  else if(expandLine[iChar] == '*' && expandLine[iChar+1] == '/') { //塊注釋結(jié)束符
   isBlockComment[0] = FALSE;
   lineType |= 2; iChar += 1;
  }
  else {
   if(isLineComment || isBlockComment[0])
    lineType |= 2;
   else
    lineType |= 1;
  }
  iChar += 1;
 }

 free(expandLine);
 return lineType; //Bitmap:0空行,1代碼,2注釋,3代碼和注釋
}

unsigned int CalcLinesPy(char *line, unsigned char isBlockComment[2]) {
 //isBlockComment[single quotes, double quotes]
 unsigned int lineType = 0;
 unsigned int lineLen = strlen(line);
 if(!lineLen)
  return lineType;

 char *expandLine = calloc(lineLen + 2/*\n\n*/, 1);
 if(NULL == expandLine)
  return lineType;
 memmove(expandLine, line, lineLen);
 //添加兩個(gè)字符防止iChar+2時(shí)越界
 expandLine[lineLen] = '\n'; expandLine[lineLen+1] = '\n';

 unsigned int iChar = 0;
 unsigned char isLineComment = FALSE;
 while(iChar < lineLen) {
  if(expandLine[iChar] == ' ' || expandLine[iChar] == '\t') { //空白字符
   iChar += 1; continue;
  }
  else if(expandLine[iChar] == '#') { //行注釋
   isLineComment = TRUE;
   lineType |= 2;
  }
  else if(expandLine[iChar] == '\'' && expandLine[iChar+1] == '\''
    && expandLine[iChar+2] == '\'') { //單引號(hào)塊注釋
   if(isBlockComment[0] || isBlockComment[1])
    isBlockComment[0] = FALSE;
   else
    isBlockComment[0] = TRUE;
   lineType |= 2; iChar += 2;
  }
  else if(expandLine[iChar] == '"' && expandLine[iChar+1] == '"'
    && expandLine[iChar+2] == '"') { //雙引號(hào)塊注釋
   if(isBlockComment[0] || isBlockComment[1])
    isBlockComment[1] = FALSE;
   else
    isBlockComment[1] = TRUE;
   lineType |= 2; iChar += 2;
  }
  else {
   if(isLineComment || isBlockComment[0] || isBlockComment[1])
    lineType |= 2;
   else
    lineType |= 1;
  }
  iChar += 1;
 }

 free(expandLine);
 return lineType; //Bitmap:0空行,1代碼,2注釋,3代碼和注釋
} 

這種實(shí)現(xiàn)最接近原來(lái)的Python版本,但還能進(jìn)一步優(yōu)化,如下:

 #define TRUE 1
#define FALSE 0
unsigned int CalcLinesCh(char *line, unsigned char isBlockComment[2]) {
 unsigned int lineType = 0;

 unsigned int iChar = 0;
 unsigned char isLineComment = FALSE;
 while(line[iChar] != '\0') {
  if(line[iChar] == ' ' || line[iChar] == '\t') { //空白字符
   iChar += 1; continue;
  }
  else if(line[iChar] == '/' && line[iChar+1] == '/') { //行注釋
   isLineComment = TRUE;
   lineType |= 2; iChar += 1; //跳過(guò)'/'
  }
  else if(line[iChar] == '/' && line[iChar+1] == '*') { //塊注釋開始符
   isBlockComment[0] = TRUE;
   lineType |= 2; iChar += 1;
  }
  else if(line[iChar] == '*' && line[iChar+1] == '/') { //塊注釋結(jié)束符
   isBlockComment[0] = FALSE;
   lineType |= 2; iChar += 1;
  }
  else {
   if(isLineComment || isBlockComment[0])
    lineType |= 2;
   else
    lineType |= 1;
  }
  iChar += 1;
 }

 return lineType; //Bitmap:0空行,1代碼,2注釋,3代碼和注釋
}

unsigned int CalcLinesPy(char *line, unsigned char isBlockComment[2]) {
 //isBlockComment[single quotes, double quotes]
 unsigned int lineType = 0;

 unsigned int iChar = 0;
 unsigned char isLineComment = FALSE;
 while(line[iChar] != '\0') {
  if(line[iChar] == ' ' || line[iChar] == '\t') { //空白字符
   iChar += 1; continue;
  }
  else if(line[iChar] == '#') { //行注釋
   isLineComment = TRUE;
   lineType |= 2;
  }
  else if(line[iChar] == '\'' && line[iChar+1] == '\''
    && line[iChar+2] == '\'') { //單引號(hào)塊注釋
   if(isBlockComment[0] || isBlockComment[1])
    isBlockComment[0] = FALSE;
   else
    isBlockComment[0] = TRUE;
   lineType |= 2; iChar += 2;
  }
  else if(line[iChar] == '"' && line[iChar+1] == '"'
    && line[iChar+2] == '"') { //雙引號(hào)塊注釋
   if(isBlockComment[0] || isBlockComment[1])
    isBlockComment[1] = FALSE;
   else
    isBlockComment[1] = TRUE;
   lineType |= 2; iChar += 2;
  }
  else {
   if(isLineComment || isBlockComment[0] || isBlockComment[1])
    lineType |= 2;
   else
    lineType |= 1;
  }
  iChar += 1;
 }

 return lineType; //Bitmap:0空行,1代碼,2注釋,3代碼和注釋
} 

優(yōu)化后的版本利用&&運(yùn)算符短路特性,因此不必考慮越界問(wèn)題,從而避免動(dòng)態(tài)內(nèi)存的分配和釋放。 

作者的Windows系統(tǒng)最初未安裝Microsoft VC++工具,因此使用已安裝的MinGW開發(fā)環(huán)境編譯dll文件。將上述C代碼保存為CalcLines.c,編譯命令如下:
 gcc -shared -o CalcLines.dll CalcLines.c
注意,MinGW中編譯dll和編譯so的命令相同。-shared選項(xiàng)指明創(chuàng)建共享庫(kù),在Windows中為dll文件,在Unix系統(tǒng)中為so文件。 

其間,作者還嘗試其他C擴(kuò)展工具,如PyInline。在http://pyinline.sourceforge.net/下載壓縮包,解壓后拷貝目錄PyInline-0.03至Lib\site-packages下。在命令提示符窗口中進(jìn)入該目錄,執(zhí)行python setup.py install安裝PyInline
 執(zhí)行示例時(shí)提示BuildError: error: Unable to find vcvarsall.bat。查閱網(wǎng)絡(luò)資料,作者下載Microsoft Visual C++ Compiler for Python 2.7并安裝。然而,實(shí)踐后發(fā)現(xiàn)PyInline非常難用,于是作罷。 

由于對(duì)MinGW編譯效果存疑,作者最終決定安裝VS2008 Express Edition。之所以選擇2008版本,是考慮到CPython2.7的Windows版本基于VS2008的運(yùn)行時(shí)(runtime)庫(kù)。安裝后,在C:\Program Files\Microsoft Visual Studio 9.0\VC\bin目錄可找到cl.exe(編譯器)和link.exe(鏈接器)。按照網(wǎng)絡(luò)教程設(shè)置環(huán)境變量后,即可在Visual Studio 2008 Command Prompt命令提示符中編譯和鏈接程序。輸入cl /help或cl -help可查看編譯器選項(xiàng)說(shuō)明。 

將CalcLines.c編譯為動(dòng)態(tài)鏈接庫(kù)前,還需要對(duì)函數(shù)頭添加_declspec(dllexport),以指明這是從dll導(dǎo)出的函數(shù):
 _declspec(dllexport) unsigned int CalcLinesCh(char *line, unsigned char isBlockComment[2]) {...
_declspec(dllexport) unsigned int CalcLinesPy(char *line, unsigned char isBlockComment[2]) {...
否則Python程序加載動(dòng)態(tài)庫(kù)后,會(huì)提示找不到相應(yīng)的C函數(shù)。 

添加函數(shù)導(dǎo)出標(biāo)記后,執(zhí)行如下命令編譯源代碼:
 cl /Ox /Ot /Wall /LD /FeCalcLines.dll CalcLines.c
其中,/Ox選項(xiàng)表示使用最大優(yōu)化,/Ot選項(xiàng)表示代碼速度優(yōu)先。/LD表示創(chuàng)建動(dòng)態(tài)鏈接庫(kù),/Fe指明動(dòng)態(tài)庫(kù)名稱。 

動(dòng)態(tài)庫(kù)文件可用UPX壓縮。由MinGW編譯的dll文件,UPX壓縮前后分別為13KB和11KB;而VS2008編譯過(guò)的dll文件,UPX壓縮前后分別為41KB和20KB。經(jīng)測(cè)兩者速度相當(dāng)??紤]到動(dòng)態(tài)庫(kù)體積,后文僅使用MinGW編譯的dll文件。 

使用C擴(kuò)展的動(dòng)態(tài)鏈接庫(kù),代碼統(tǒng)計(jì)工具在CPython2.7環(huán)境下可獲得極大的速度提升。相對(duì)而言,Pypy因?yàn)楸旧砑铀傩Ч@著,動(dòng)態(tài)庫(kù)的性能提升反而不太明顯。此外,當(dāng)待統(tǒng)計(jì)文件數(shù)目較少時(shí),也可不使用dll文件(此時(shí)將啟用Python版本的算法);當(dāng)文件數(shù)目較多時(shí),dll文件會(huì)顯著提高統(tǒng)計(jì)速度。詳細(xì)的評(píng)測(cè)數(shù)據(jù)參見第二節(jié)。 

作者使用的Pypy版本為5.1,可從官網(wǎng)下載Win32安裝包。該安裝包默認(rèn)包含cffi1.6,后者的使用可參考《Python學(xué)習(xí)入門手冊(cè)以及CFFI》或CFFI官方文檔。安裝Pypy5.1后,在命令提示符窗口輸入pypy可查看pypy和cffi版本信息: 

E:\PyTest>pypy
Python 2.7.10 (b0a649e90b66, Apr 28 2016, 13:11:00)
[PyPy 5.1.1 with MSC v.1500 32 bit] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>> import cffi
>>>> cffi.__version__
'1.6.0' 

若要CPLineCounter在未安裝Python環(huán)境的主機(jī)上運(yùn)行,應(yīng)先將CPython版本的代碼轉(zhuǎn)換為exe并壓縮后,連同壓縮后的dll文件一并發(fā)布。使用者可將其放入同一個(gè)目錄,再將該目錄加入PATH環(huán)境變量,即可在Windows命令提示符窗口中運(yùn)行CPLineCounter。例如:

 D:\pytest>CPLineCounter -d lctest -s code
FileLines CodeLines CommentLines BlankLines CommentPercent FileName
6   3   4    0   0.57   D:\pytest\lctest\hard.c
27   7   15   5   0.68   D:\pytest\lctest\file27_code7_cmmt15_blank5.py
33   19   15   4   0.44   D:\pytest\lctest\line.c
44   34   3    7   0.08   D:\pytest\lctest\test.c
44   34   3    7   0.08   D:\pytest\lctest\subdir\test.c
243  162  26   60   0.14   D:\pytest\lctest\subdir\CLineCounter.py
------------------------------------------------------------------------------------------
397  259  66   83   0.20   <Total:6 Code Files>
Time Elasped: 0.04 sec. 

二. 精度與性能評(píng)測(cè)
 為檢驗(yàn)CPLineCounter統(tǒng)計(jì)精度和性能,作者從網(wǎng)上下載幾款常見的行數(shù)統(tǒng)計(jì)工具,即cloc1.64(10.9MB)、linecount3.7(451KB)、SourceCounter3.4(8.34MB)和SourceCount_1.0(644KB)。

首先測(cè)試統(tǒng)計(jì)精度。以line.c為目標(biāo)代碼,上述工具的統(tǒng)計(jì)輸出如下表所示("-"表示該工具未直接提供該統(tǒng)計(jì)項(xiàng)):

      經(jīng)

人工檢驗(yàn),CPLineCounter的統(tǒng)計(jì)結(jié)果準(zhǔn)確無(wú)誤。linecount和SourceCounter統(tǒng)計(jì)也較為可靠。
 然后,統(tǒng)計(jì)82個(gè)源代碼文件,上述工具的統(tǒng)計(jì)輸出如下表所示:    

通常,文件總行數(shù)和空行數(shù)統(tǒng)計(jì)規(guī)則簡(jiǎn)單,不易出錯(cuò)。因此,選取這兩項(xiàng)統(tǒng)計(jì)重合度最高的工具作為基準(zhǔn),即CPLineCounter和linecount。同時(shí),對(duì)于代碼行數(shù)和注釋行數(shù),CPLineCounter和SourceCounter的統(tǒng)計(jì)結(jié)果重合。根據(jù)統(tǒng)計(jì)重合度,有理由認(rèn)為CPLineCounter的統(tǒng)計(jì)精度最高。 

最后,測(cè)試統(tǒng)計(jì)性能。在作者的Windows XP主機(jī)(Pentium G630 2.7GHz主頻2GB內(nèi)存)上,統(tǒng)計(jì)5857個(gè)C源代碼文件,總行數(shù)接近千萬(wàn)級(jí)。上述工具的性能表現(xiàn)如下表所示。表中僅顯示總計(jì)項(xiàng),實(shí)際上仍統(tǒng)計(jì)單個(gè)文件的行數(shù)信息。注意,測(cè)試時(shí)linecount要勾選"目錄統(tǒng)計(jì)時(shí)包含同名文件",cloc要添加--skip-uniqueness和--by-file選項(xiàng)?!   ?br />

其中,CPLineCounter的性能因運(yùn)行場(chǎng)景而異,統(tǒng)計(jì)耗時(shí)少則29秒,多則281秒。。需要注意的是,cloc僅統(tǒng)計(jì)出5733個(gè)文件。
以條形圖展示上述工具的統(tǒng)計(jì)性能,如下所示:

  

 圖中"Opt-c"表示CPLineCounter以-c選項(xiàng)運(yùn)行,"CPython2.7+ctypes(O)"表示以CPython2.7環(huán)境運(yùn)行附帶舊DLL庫(kù)的CPLineCounter,"Pypy5.1+cffi1.6(N)"表示以Pypy5.1環(huán)境運(yùn)行附帶新DLL庫(kù)的CPLineCounter,以此類推。 

由于CPLineCounter并非純粹的CPU密集型程序,因此DLL庫(kù)算法本身的優(yōu)化并未帶來(lái)性能的顯著提升(對(duì)比舊DLL庫(kù)和新DLL庫(kù))。對(duì)比之下,Pypy內(nèi)置JIT(即時(shí)編譯)解釋器,可從整體上極大地���升Python腳本的運(yùn)行速度,加速效果甚至可與C匹敵。此外,性能測(cè)試數(shù)據(jù)會(huì)受到目標(biāo)代碼、CPU架構(gòu)、預(yù)熱、緩存、后臺(tái)程序等多方面因素影響,因此不同工具或組合的性能表現(xiàn)可能與作者給出的數(shù)據(jù)略有出入。 

綜合而言,CPLineCounter統(tǒng)計(jì)速度最快且結(jié)果可靠,軟件體積也小(exe1.3MB,dll11KB)。SourceCounter統(tǒng)計(jì)結(jié)果比較可靠,速度較快,且內(nèi)置項(xiàng)目管理信息。cloc文件數(shù)目統(tǒng)計(jì)誤差大,linecount代碼行統(tǒng)計(jì)誤差大,兩者速度較慢。但cloc可配置項(xiàng)豐富,并且可自行編譯以壓縮體積。SourceCount統(tǒng)計(jì)速度最慢,結(jié)果也不太可靠。 

了解Python并行計(jì)算的讀者也可修改CPLineCounter源碼實(shí)現(xiàn),加入多進(jìn)程處理,壓滿多核處理器;還可嘗試多線程,以改善IO性能。以下截取CountFileLines()函數(shù)的部分line_profiler結(jié)果:

 E:\PyTest>kernprof -l -v CPLineCounter.py source -d > out.txt
140872  93736  32106   16938  0.26   <Total:82 Code Files>
Wrote profile results to CPLineCounter.py.lprof
Timer unit: 2.79365e-07 s

Total time: 5.81981 s
File: CPLineCounter.py
Function: CountFileLines at line 143

Line #  Hits   Time Per Hit % Time Line Contents
==============================================================
 143           @profile
 144           def CountFileLines(filePath, isRawReport=True, isShortName=False):
... ... ... ... ... ... ... ...
 162  82  7083200 86380.5  34.0  with open(filePath, 'r') as file:
 163 140954  1851877  13.1  8.9   for line in file:
 164 140872  6437774  45.7  30.9    lineType = CalcLines(fileType, line.strip(), isBlockComment)
 165 140872  1761864  12.5  8.5    lineCountInfo[0] += 1
 166 140872  1662583  11.8  8.0    if lineType == 0: lineCountInfo[3] += 1
 167 123934  1499176  12.1  7.2    elif lineType == 1: lineCountInfo[1] += 1
 168  32106  406931  12.7  2.0    elif lineType == 2: lineCountInfo[2] += 1
 169  1908  27634  14.5  0.1    elif lineType == 3: lineCountInfo[1] += 1; lineCountInfo[2] += 1
... ... ... ... ... ... ... ... 

line_profiler可用pip install line_profiler安裝。在待評(píng)估函數(shù)前添加裝飾器@profile后,運(yùn)行kernprof命令,將給出被裝飾函數(shù)中每行代碼所耗費(fèi)的時(shí)間。-l選項(xiàng)指明逐行分析,-v選項(xiàng)則指明執(zhí)行后屏顯計(jì)時(shí)信息。Hits(執(zhí)行次數(shù))或Time(執(zhí)行時(shí)間)值較大的代碼行具有較大的優(yōu)化空間。

由line_profiler結(jié)果可見,該函數(shù)偏向CPU密集型(75~80行占用該函數(shù)56.7%的耗時(shí))。然而考慮到目錄遍歷等操作,很可能整體程序?yàn)镮O密集型。因此,選用多進(jìn)程還是多線程加速還需要測(cè)試驗(yàn)證。最簡(jiǎn)單地,可將73~80行(即讀文件和統(tǒng)計(jì)行數(shù))均改為C實(shí)現(xiàn)。其他部分要么為IO密集型要么使用Python庫(kù),用C語(yǔ)言改寫事倍功半。 

最后,若僅僅統(tǒng)計(jì)代碼行數(shù),Linux或Mac系統(tǒng)中可使用如下shell命令:
 find ./codeDir -name "*.c" -or -name "*.h" | xargs wc -l  #除空行外的總行數(shù)
find ./codeDir -name "*.c" -or -name "*.h" | xargs wc -l  #各文件行數(shù)及總和

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • Pygame?精準(zhǔn)檢測(cè)圖像碰撞的問(wèn)題

    Pygame?精準(zhǔn)檢測(cè)圖像碰撞的問(wèn)題

    這篇文章主要介紹了Pygame?精準(zhǔn)檢測(cè)圖像碰撞,在用Pygame寫游戲的時(shí)候,有人可能會(huì)遇到兩個(gè)Rect對(duì)象碰撞但是對(duì)象之間還有空間間隔的問(wèn)題,這里,將教大家用一種方法精準(zhǔn)地檢測(cè)圖像碰撞,需要的朋友可以參考下
    2022-06-06
  • python3兩數(shù)相加的實(shí)現(xiàn)示例

    python3兩數(shù)相加的實(shí)現(xiàn)示例

    這篇文章主要介紹了python3兩數(shù)相加的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • Pandas將列表(List)轉(zhuǎn)換為數(shù)據(jù)框(Dataframe)

    Pandas將列表(List)轉(zhuǎn)換為數(shù)據(jù)框(Dataframe)

    這篇文章主要介紹了Pandas將列表(List)轉(zhuǎn)換為數(shù)據(jù)框(Dataframe),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • Python數(shù)據(jù)分析Pandas?Dataframe排序操作

    Python數(shù)據(jù)分析Pandas?Dataframe排序操作

    這篇文章主要介紹了Python數(shù)據(jù)分析Pandas?Dataframe排序操作,數(shù)據(jù)的排序是比較常用的操作,DataFrame?的排序分為兩種,一種是對(duì)索引進(jìn)行排序,另一種是對(duì)值進(jìn)行排序,接下來(lái)就分別都介紹一下,需要的小伙伴可以參考一下
    2022-05-05
  • 最新評(píng)論