[关闭]
@cloverwang 2016-06-02T06:58:04.000000Z 字数 5706 阅读 2184

Python代码统计工具

Python 代码统计


声明

本文将对《Python实现C代码统计工具(一)~(三)》中的C代码统计工具进行扩展,以支持Python脚本自身的行数统计。

一. 问题提出

此前实现的C代码统计工具仅能分析和统计C语言代码文件,但其设计思想也适用于Python代码及其他编码语言。

Python行数统计的难点在于注释行,因为Python有两种注释方式:简单明了的单行注释和复杂含糊的多行注释(块注释)。单行注释以#(pound或hash)符号起始,直至物理行的末尾(但字符串内的#并无注释作用)。多行注释可在每行头部添加#号,也可包入未命名的三引号字符串(triple-quoted strings,即多行字符串)内。除非未命名三引号字符串作为对象的文档字符串(docstring),即模块、类、或函数体的第一条语句为未命名字符串,否则可作为多行注释。

下面以总27_代7_注15_空5.py脚本为例,演示不同的注释方式。注意,该脚本仅作测试数据用,并非真实世界中的脚本文件。

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #comment3
  4. print 'code1'
  5. '''comment4
  6. print """comment5"""
  7. comment6'''
  8. """comment7
  9. '''print 'comment8 and comment9'
  10. """
  11. print 'code2'
  12. def code3():
  13. """f = open('whatever', 'r')
  14. multiline comment 10,11,12 make up a doc string
  15. """
  16. print 'code4'
  17. '''
  18. print 'comment13, comment14 and comment15'
  19. '''
  20. return 'code5'
  21. help(code3); print 'code6'
  22. print code3.__doc__, 'code7'

运行该脚本后,输出如下:

  1. code1
  2. code2
  3. Help on function code3 in module __main__:
  4. code3()
  5. f = open('whatever', 'r')
  6. multiline comment 10,11,12 make up a doc string
  7. code6
  8. f = open('whatever', 'r')
  9. multiline comment 10,11,12 make up a doc string
  10. code7

使用未命名三引号字符串做注释时,存在如下缺点:
1. 未命名字符串本质上并非注释,而是不生成字节码的语句。因此,需要满足缩进要求(常错点)。
2. 无法注释掉已包含相同三引号字符串的代码。
3. IDE的语法高亮会将三引号字符串标记为字符串,而不是注释区。
此外,大多数IDE均支持选择代码片段,并自动使用单行注释符对选区添加注释。以IDLE(Python GUI)为例,快捷键Alt+3可添加注释,Alt+4可删除注释。因此,建议总是使用#号添加多行注释,而三引号字符串仅用于调试过程中临时性地注释代码块。

二. 代码实现

为同时支持统计C和Python代码,需对CalcLines()和CountFileLines()函数稍作修改。其他函数实现参考C代码统计工具前述系列文章。可以看出,绝大部分实现只需少量或无需修改,这表明前期的函数划分和布局得当。

为求直观,将原先的CalcLines()函数重命名为CalcLinesCh()。接着,实现统计Python脚本行信息的CalcLinesPy()函数:

  1. def CalcLinesPy(line, isBlockComment):
  2. #isBlockComment[single quotes, double quotes]
  3. lineType, lineLen = 0, len(line)
  4. line = line + '\n\n' #添加两个字符防止iChar+2时越界
  5. iChar, isLineComment = 0, False
  6. while iChar < lineLen:
  7. #行结束符(Windows:\r\n; MacOS 9:\r; OS X&Unix:\n)
  8. #不可写为"if line[iChar] in os.linesep"(文件可能来自异种系统)
  9. if line[iChar] == '\r' or line[iChar] == '\n':
  10. break
  11. elif line[iChar] == ' ' or line[iChar] == '\t': #空白字符
  12. iChar += 1; continue
  13. elif line[iChar] == '#': #行注释
  14. isLineComment = True
  15. lineType |= 2
  16. elif line[iChar:iChar+3] == "'''": #单引号块注释
  17. if isBlockComment[0] or isBlockComment[1]:
  18. isBlockComment[0] = False
  19. else:
  20. isBlockComment[0] = True
  21. lineType |= 2; iChar += 2
  22. elif line[iChar:iChar+3] == '"""': #双引号块注释
  23. if isBlockComment[0] or isBlockComment[1]:
  24. isBlockComment[1] = False
  25. else:
  26. isBlockComment[1] = True
  27. lineType |= 2; iChar += 2
  28. else:
  29. if isLineComment or isBlockComment[0] or isBlockComment[1]:
  30. lineType |= 2
  31. else:
  32. lineType |= 1
  33. iChar += 1
  34. return lineType #Bitmap:0空行,1代码,2注释,3代码和注释

相应地,CountFileLines()函数作如下修改:

  1. def CountFileLines(filePath, isRawReport=True, isShortName=False):
  2. fileExt = os.path.splitext(filePath)
  3. if fileExt[1] == '.c' or fileExt[1] == '.h':
  4. CalcLinesFunc = CalcLinesCh
  5. elif fileExt[1] == '.py':
  6. CalcLinesFunc = CalcLinesPy
  7. else:
  8. return
  9. isBlockComment = [False]*2 #或定义为全局变量,以保存上次值
  10. lineCountInfo = [0]*4 #[代码总行数, 代码行数, 注释行数, 空白行数]
  11. with open(filePath, 'r') as file:
  12. for line in file:
  13. lineType = CalcLinesFunc(line, isBlockComment)
  14. lineCountInfo[0] += 1
  15. if lineType == 0: lineCountInfo[3] += 1
  16. elif lineType == 1: lineCountInfo[1] += 1
  17. elif lineType == 2: lineCountInfo[2] += 1
  18. elif lineType == 3: lineCountInfo[1] += 1; lineCountInfo[2] += 1
  19. else:
  20. assert False, 'Unexpected lineType: %d(0~3)!' %lineType
  21. if isRawReport:
  22. global rawCountInfo
  23. rawCountInfo[:-1] = [x+y for x,y in zip(rawCountInfo[:-1], lineCountInfo)]
  24. rawCountInfo[-1] += 1
  25. elif isShortName:
  26. detailCountInfo.append([os.path.basename(filePath), lineCountInfo])
  27. else:
  28. detailCountInfo.append([filePath, lineCountInfo])

CountFileLines()函数根据后缀判断文件类型并调用相应的统计函数,并扩展isBlockComment列表以存储两种Python块注释(三单引号和三双引号)。除此之外,别无其他修改。

三. 效果验证

将本文的统计实现命名为PCLineCounter.py。首先,混合统计C文件和Python脚本:

  1. E:\PyTest>PCLineCounter.py -d lctest
  2. FileLines CodeLines CommentLines EmptyLines CommentPercent FileName
  3. 6 3 4 0 0.57 E:\PyTest\lctest\hard.c
  4. 33 19 15 4 0.44 E:\PyTest\lctest\line.c
  5. 243 162 26 60 0.14 E:\PyTest\lctest\subdir\CLineCounter.py
  6. 44 34 3 7 0.08 E:\PyTest\lctest\subdir\test.c
  7. 44 34 3 7 0.08 E:\PyTest\lctest\test.c
  8. 27 7 15 5 0.68 E:\PyTest\lctest\总27_7_15_5.py
  9. ------------------------------------------------------------------------------------------
  10. 397 259 66 83 0.20 <Total:6 Code Files>

然后,统计纯Python脚本,并通过cProfile命令分析性能:

  1. E:\PyTest>python -m cProfile -s tottime PCLineCounter.py -d -b C:\Python27\Lib\encodings_trim > out.txt

截取out.txt文件部分内容如下:

  1. FileLines CodeLines CommentLines EmptyLines CommentPercent FileName
  2. 157 79 50 28 0.39 __init__.py
  3. 527 309 116 103 0.27 aliases.py
  4. 50 27 8 15 0.23 ascii.py
  5. 80 37 22 21 0.37 base64_codec.py
  6. 103 55 24 25 0.30 bz2_codec.py
  7. 69 38 10 21 0.21 charmap.py
  8. 307 285 262 16 0.48 cp1252.py
  9. 39 26 5 8 0.16 gb18030.py
  10. 39 26 5 8 0.16 gb2312.py
  11. 39 26 5 8 0.16 gbk.py
  12. 80 37 22 21 0.37 hex_codec.py
  13. 307 285 262 16 0.48 iso8859_1.py
  14. 50 27 8 15 0.23 latin_1.py
  15. 47 24 10 13 0.29 mbcs.py
  16. 83 58 36 17 0.38 palmos.py
  17. 175 148 127 18 0.46 ptcp154.py
  18. 238 183 28 30 0.13 punycode.py
  19. 76 45 15 16 0.25 quopri_codec.py
  20. 45 24 8 13 0.25 raw_unicode_escape.py
  21. 38 24 4 10 0.14 string_escape.py
  22. 49 26 9 14 0.26 undefined.py
  23. 45 24 8 13 0.25 unicode_escape.py
  24. 45 24 8 13 0.25 unicode_internal.py
  25. 126 93 10 23 0.10 utf_16.py
  26. 42 23 6 13 0.21 utf_16_be.py
  27. 42 23 6 13 0.21 utf_16_le.py
  28. 150 113 16 21 0.12 utf_32.py
  29. 37 23 5 9 0.18 utf_32_be.py
  30. 37 23 5 9 0.18 utf_32_le.py
  31. 42 23 6 13 0.21 utf_8.py
  32. 117 84 14 19 0.14 utf_8_sig.py
  33. 130 70 32 28 0.31 uu_codec.py
  34. 103 56 23 25 0.29 zlib_codec.py
  35. ------------------------------------------------------------------------------------------
  36. 3514 2368 1175 635 0.33 <Total:33 Code Files>
  37. 10180 function calls (10146 primitive calls) in 0.168 seconds
  38. Ordered by: internal time
  39. ncalls tottime percall cumtime percall filename:lineno(function)
  40. 3514 0.118 0.000 0.122 0.000 PCLineCounter.py:45(CalcLinesPy)
  41. 56 0.015 0.000 0.144 0.003 PCLineCounter.py:82(CountFileLines)
  42. 33 0.005 0.000 0.005 0.000 {open}
  43. 1 0.004 0.004 0.005 0.005 collections.py:1(<module>)
  44. 4028/4020 0.004 0.000 0.004 0.000 {len}
  45. 57 0.004 0.000 0.004 0.000 {nt._isdir}
  46. 259 0.002 0.000 0.003 0.000 ntpath.py:96(splitdrive)
  47. 1 0.002 0.002 0.007 0.007 argparse.py:62(<module>)
  48. 1 0.002 0.002 0.168 0.168 PCLineCounter.py:6(<module>)

为避免制作单个exe时体积过大,作者拷贝Lib\encodings目录后,删除该目录内不需要的语言文件并重命名为encodings_trim。

最后,需要指出的是,本文实现未区分三引号块注释与参与赋值或计算的字符串(如s='''devil'''money=10 if '''need''' else 0),也未处理单个单引号或双引号括起的未命名字符串(如"I'm a bad comment")。毕竟,这些并非良好的Python编程风格。而且,实际应用中其实并不要求非常准确地统计代码行和注释行。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注