最近部门成立了一个python学习小组,旨在让大家在做项目中开始成长起来,于是老大就给布置了第一个小任务:代码统计工具,具体的需求如下:

需求:
. 能够统计指定目录下C++程序的代码行数。
. C++程序文件包括.cpp和.h文件。
. 如果指定目录下有子目录,需要能够递归遍历所有子目录。
. 能够区分注释和代码。
. 不用考虑语句跨行问题。
. 输出.cpp、.h文件个数、代码行数、注释行数及处理时间。
. 基于python3开发。

乍一看,感觉好像有点难度,一下要处理这么多的功能,又是统计特定文件的个数,又是要遍历子目录,还要统计注释行。后面网上百度了一下,万能的python提供的函数简直是方便用到不行,os的walk函数实现了目录的遍历,os.path的splitext能得到文件后缀名,现在最主要的问题来了:怎么识别代码里的注释。

在c++里,注释主要分行注释(//)和块注释(/* */), 起初有2个想法短暂的想法觉得可以实现这个功能

1. 读入的每一行当成一个字符串,判断是否存在行注释符或者块注释符

2. 使用正则表达式

很快这个想法就觉得行不通了,就一个简单的例子,如果一个代码行是这样的:

"// test /*  */ ";

就一个简单的字符串,不管是第一种还是第二种方法,对于诸如此类的奇怪写法,都是非常不好处理的,还有以下的几种奇葩的写法:

//  int  c = 0; \
int b = ;

实际上面的2行都应该算是注释的,单单用方式1,方式2就已经无法做了,因为每行之间不是独立的,行与行之间可能还存在一定的关系,正在百感交集之时,开发哥哥向我抛出了一个关键字:状态机。读行一行的每一个字符,然后根据字符的不同,设置不同的状态,很多的集成开发工具代码行的统计都是采用此类方法,于是,我毅然决定采用此方法来实现注释码的统计。

在实现之处,确实有一种一头雾水的感觉,因为不清楚到底有哪些状态,于是用vs工具,结合开发哥哥提的意见,整了一个特别奇葩的cpp文档,如下所示:

  //
/* 123
*/
/* /* */
int g_i;
/* " /*
int g_j;
*/
#include <iostream>
int main(int argc, char *argv[])
{
/* " test */int i; // */ int j; /* test */
int k; // test
/* test */ j = ;
const char *test = "test // ";
int m; /* /*
const char *test2 = "test /*";
const char *test3 = "*/ "test"; // */
/* * /
*
*/
/* // *//* */int n;
const char *test4 = "/*"
"//"
" "
"";
// t1 \
;
cout<<"i am \
loleina";
// "test\"
int a=,\
c;
// \test
b=
cout<<"i am \r loleima";
return ;
}

然后就发现了一些规律:当字符是 “ 或者是 / 或者是 * 或者是 \ 的时候,代码行的状态是有可能发生改变的,假设有3个状态,初始状态,正常状态,字符串状态,最开始打开一个文本,设置行状态为初始状态,读入第一行,如果读入的字符是” ,则表示进入字符串状态,在字符串状态内,读入字符// 或者 /* 这些都是不管的,只有当再次读到”时,状态才会发生改变,此时则变成正常状态。如果读字符是/ , 则有可能进入注释状态了,设定为预注释状态,如果下一个字符读入的是/ ,则表示进入行注释状态,如果读入的是*,则表示进入块注释状态,在行注释里,只需要考虑读入换行符或者读入拼接符\,其他字符的读入都不会引起状态的改变,而在块注释就跟简单,只需要考虑正常退出即可。整理下思维逻辑后,弄了个状态转换图,如下所示:

画完状态图后,感觉状态之间的转换关系理得差不多了,但是还有2个问题没解决,第一个问题:

1. 怎么计算有效代码数?

2. 行与行之间如果有关联关系的时候怎么处理?

第一个问题,可以为每行设置一个状态:is_effetive_code, 默认设置为false,但是只要在正常状态下,读入到一个有效字符(非/)就判断当前行为有效代码行,就设置bool值为true,在每行最后再判断该值,如果是true,则代码行加1;

第二个问题,只有代码行处于拼接状态和块注释状态时,才会影响第二行第一个字符的状态,所以在每行最后,保存当前状态,如果当前状态拼接状态和块注释状态时,设置第二行状态为拼接状态和块注释状态就可以了。

问题基本都解决了,开始实现了,总共分2个部分,一个函数CountFies实现从路径得到有效文件。一个函数ReadEffetiveCodeNumber则是统计有效文件的所有有效代码:

def CountFies(path):
filesList = []
for root, dirs, files in os.walk(path):
for file in files:
if (os.path.splitext(file)[] == '.h' or os.path.splitext(file)[] == '.cpp'):
filename = os.path.join(root, file)
filesList.append(filename)
return filesList
def ReadEffetiveCodeNumber(filesList):
codes_numbers =
for i in range(len(filesList)):
filename=filesList[i]
infp = open(filename,encoding = 'gbk', errors = 'ignore')
lines = infp.readlines()
row_cur_status = Status.Common
for li in lines:
row_pre_status = Status.Init
li = li.strip("\r\t ")
if len(li) == :
continue
is_effective_code = False
for charli in li:
if row_cur_status == Status.Common:
if charli == '/':
row_cur_status = Status.PreComment
continue
if charli == '\"':
row_cur_status = Status.CharString
continue
if charli == '\n':
continue
if charli == ' ':
continue
else:
is_effective_code = True
continue
elif row_cur_status == Status.CharString:
if charli == '\"':
row_cur_status = Status.Common
is_effective_code = True
continue
if charli == '\\':
row_pre_status = row_cur_status
row_cur_status = Status.PreCombination
continue
else:
continue
elif row_cur_status == Status.PreComment:
if charli == '/':
row_cur_status = Status.LineComment
continue
if charli == '*':
row_cur_status = Status.BlockComments
continue
else:
continue
elif row_cur_status == Status.LineComment:
if charli == '\n':
row_cur_status = Status.Common
continue
if charli == '\\':
row_pre_status = row_cur_status
row_cur_status = Status.PreCombination
continue
else:
continue
elif row_cur_status == Status.BlockComments:
if charli == '*':
row_cur_status = Status.PreExitComment
continue
else:
continue
elif row_cur_status == Status.PreExitComment:
if charli == '/':
row_cur_status = Status.Common
continue
else:
row_cur_status = Status.BlockComments
continue
elif row_cur_status == Status.PreCombination:
if charli == '\n':
row_cur_status = Status.Combination
else:
row_cur_status = row_pre_status if is_effective_code == True:
codes_numbers +=
if row_cur_status not in (Status.BlockComments, Status.Combination):
row_cur_status = Status.Common
if row_cur_status == Status.Combination:
row_cur_status = row_pre_status infp.close()
return codes_numbers
if __name__=="__main__":
userdir = input("input the directory :")
while os.path.isdir(userdir) == False:
userdir = input("it is wrong,input the directory again : ") # .表示普通状态 .表示字符串状态 .表示预注释状态 .表示行注释状态 .表示块注释状态 .表示预退出块注释状态 .表示预拼接状态 .表示拼接模式
Status = enum(Init=,Common=, CharString=, PreComment=,LineComment=,BlockComments=,PreExitComment=,PreCombination=,Combination=) # .计算给定路径下存在有效文件(.cpp,.h)的个数,并把有效文件的绝对路径保存在list列表返回
# .读取list下每个文件的有效代码行数,返回总的有效代码行数
# A.读取每个文件的每行
# B.去掉每行的前后空格,以及空行
# C.依次读取每行的每个字符并设置状态
start = time.time()
filesList = CountFies(userdir)
codes_numbers = ReadEffetiveCodeNumber(filesList)
end = time.time()
dealTime = end - start
print("it has %d files(.cpp,.h) and all effective code lines count:%d" %(len(filesList),codes_numbers ))
print ("Code execution consumes %s seconds" %dealTime)

于是用简单的方法就实现了整体的需求,小组内总共有7位童鞋都写了,但是绝大部分用的是方法1查找是否为代码行,有一位童鞋是用方法2正则表达式来实现的,但大家都考虑不够全面,只能统计所谓‘正常人’编写的代码行,而第二种用正则表达式实现的,在性能上还有很大的问题,大家基本都是1s或者1s内计算完成,而正则花了近7s才计算完成。完了,老大点评了下,我这种是最佳实现方式,然后我笑了~~~

这次的练习,实际收获还是挺大的,因为实际编写的过程还遇到一些问题, 比如:写完代码后,把代码放在linux下运行老大指定的特定文件夹,结果程序崩掉了,提示是字符码的问题,但是测试自己放上去的文件夹,没事;放在linux上运行的时候,提示语乱码。

最后,针对2周的相关学习,总结如下:

1. 读批量文件时,应该提供异常捕获机制。可能会由于文件内的编解码格式与读入的格式不一致,会直接导致程序崩溃。

2. 尽量少的使用正则表达式,正则表达式使用多的时候,可能会存在性能上的某些瓶颈。

3. 学习os.walk函数:官网文档:https://docs.python.org/2/library/os.html,属于os模块

os.walk(top, topdown=True, onerror=None, followlinks=False)
Generate the file names in a directory tree by walking the tree either top-down or bottom-up. For each directory in the tree rooted at directory top (including top itself), it yields a -tuple (dirpath, dirnames, filenames). dirpath is a string, the path to the directory. dirnames is a list of the names of the subdirectories in dirpath (excluding '.' and '..'). filenames is a list of the names of the non-directory files in dirpath. Note that the names in the lists contain no path components. To get a full path (which begins with top) to a file or directory in dirpath, do os.path.join(dirpath, name).

4. 学习打开文件函数 open。https://docs.python.org/2/library/io.html,属于IO模块。

io.open(file, mode='r', buffering=-, encoding=None, errors=None, newline=None, closefd=True)
Open file and return a corresponding stream. If the file cannot be opened, an IOError is raised.

5. 学习splitext 函数。https://docs.python.org/2/library/os.path.htm,l属于IO-path模块。

os.path.splitext(path)
Split the pathname path into a pair (root, ext) such that root + ext == path, and ext is empty or begins with a period and contains at most one period. Leading periods on the basename are ignored; splitext('.cshrc') returns ('.cshrc', ''). Changed in version 2.6: Earlier versions could produce an empty root when the only period was the first character.

6. 对python各个模块的官网学习,显得更加迫在眉睫,之前看各种文档和别人的博客,总感觉少了什么,果然每次使用还是得去查官网文档才能正确使用,接下来的一段日子,会静下心去学习python官网的各类文档,尽力每学一篇,再自己总结下~~~

python 练习(一)代码统计工具的实现的更多相关文章

  1. Python实现C代码统计工具(四)

    目录 Python实现C代码统计工具(四) 标签: Python 计时 持久化 声明 运行测试环境 一. 自定义计时函数 1.1 整个程序计时 1.2 代码片段计时 1.3 单条语句计时 二. 性能优 ...

  2. Python实现C代码统计工具(三)

    目录 Python实现C代码统计工具(三) 声明 一. 性能分析 1.1 分析单条语句 1.2 分析代码片段 1.3 分析整个模块 二. 制作exe Python实现C代码统计工具(三) 标签: Py ...

  3. Python实现C代码统计工具(二)

    目录 Python实现C代码统计工具(二) 声明 一. 问题提出 二. 代码实现 三. 效果验证 Python实现C代码统计工具(二) 标签: Python 代码统计 声明 本文将对<Pytho ...

  4. Python实现C代码统计工具(一)

    目录 Python实现C代码统计工具(一) 声明 一. 问题提出 二. 代码实现 三. 效果验证 四. 后记 Python实现C代码统计工具(一) 标签: Python 代码统计 声明 本文将基于Py ...

  5. Python代码统计工具

    目录 Python代码统计工具 声明 一. 问题提出 二. 代码实现 三. 效果验证 Python代码统计工具 标签: Python 代码统计 声明 本文将对<Python实现C代码统计工具(一 ...

  6. Python实现代码统计工具——终极加速篇

    Python实现代码统计工具--终极加速篇 声明 本文对于先前系列文章中实现的C/Python代码统计工具(CPLineCounter),通过C扩展接口重写核心算法加以优化,并与网上常见的统计工具做对 ...

  7. C++代码统计工具

    自己前几天写的C++代码统计工具. http://pan.baidu.com/s/17SnnH

  8. svn代码统计工具的金额

    StatSVN介绍 StatSVN是Java写开源统计程序,从statCVS从移植.从能Subversion版本号来获取信息库,该项目开发的叙述性说明,然后生成各种表格和图表.例:时间线.针对每一个开 ...

  9. 代码统计工具-cloc

    官网地址:http://cloc.sourceforge.net/ https://sourceforge.NET/projects/cloc/files/ 下载得到cloc-1.64.exe Clo ...

随机推荐

  1. 二 、打开地图《苹果iOS实例编程入门教程》

    该app为应用的功能为给你的iPhone打开google地图有效地址连接 现版本 SDK 8.4 Xcode 运行Xcode 选择 Create a new Xcode project ->Si ...

  2. 1022. D进制的A+B (20)

    1022. D进制的A+B (20) 时间限制 100 ms 内存限制 32000 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHEN, Yue 输入两个非负10进制整数A和 ...

  3. js事件冒泡和事件捕获的区别

  4. ArcGIS JavaScript API异常之onExtentChange事件覆盖onClick事件

    利用Esri官方提供的聚合类进行聚合,由于数据较多,为了加快速度,在聚合之前对当期范围进行判断,如果不在当前视图范围的,就不聚合了. 所以,由于Esri官方的类是监听了zoomEnd事件,如下代码 t ...

  5. HTTP协议 (六) 状态码详解

    HTTP协议 (六) 状态码详解 HTTP状态码,我都是现查现用. 我以前记得几个常用的状态码,比如200,302,304,404, 503. 一般来说我也只需要了解这些常用的状态码就可以了.  如果 ...

  6. vbox下Oracle Enterprise liunx5.4虚拟机安装10G RAC实验(四)

    接第3篇 http://www.cnblogs.com/myrunning/p/4003527.html 5.安装配置数据库 5.1安装数据库软件 5.2配置监听 5.3创建ASM磁盘 5.4创建服务 ...

  7. 配置samba服务一例

    问题: 在/data/share目录下建立三个子目录public.training.devel用途如下 public目录用于存放公共数据,如公司的规章制度 training目录用于存放公司的技术培训资 ...

  8. 微信APP支付(Java后台生成签名具体步骤)

    public class PayCommonUtil { //定义签名,微信根据参数字段的ASCII码值进行排序 加密签名,故使用SortMap进行参数排序 public static String ...

  9. Lambda表达式公共拼接函数(原创)

    #region Lambda公共拼接函数 /// <summary> /// LambdaWhere(枚举) /// </summary> public enum Lambda ...

  10. 怎么启动或停止mysql服务

    在linux下,  启动mysql用 service mysql start   停止用 service mysql stop 在windows下, 启动用 net start mysql    停止 ...