这个作业属于哪个课程 | 2021秋软工实践 |
---|---|
这个作业要求在哪里 | 2021秋软工实践第一次个人编程作业-CSDN社区 |
这个作业的目标 | github的使用;编写程序实现对C/C++代码不同等级的关键字提取 |
学号 | 031902218 |
GitHub使用情况
-
GitHub 仓库地址
-
仓库截图
-
commit 截图
代码规范
PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实践耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 300 | 360 |
Analysis | 需求分析(包括学习新技术) | 60 | 60 |
Design Spec | 生成设计文档 | 120 | 180 |
Design Review | 设计复审 | 20 | 25 |
Coding Standard | 代码规范 | 15 | 20 |
Design | 具体设计 | 120 | 120 |
Coding | 具体编码 | 300 | 360 |
Code Review | 代码复审 | 60 | 40 |
Test | 测试(自我测试,修改代码,提交修改) | 100 | 120 |
Reporting | 报告 | 90 | 120 |
Test Report | 测试报告 | 60 | 60 |
Size Measurement | 计算工作量 | 15 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 90 |
合计 | 1410 | 1650 |
解题思路描述
-
题目初印象
一开始看到这道题的时候感觉还挺平易近人的,起码能够完全看懂题目,没有一开始就出现一些让人需要搜索才能懂的词汇;题目主要涉及到的是对于文本的字符串操作,需要你从C/C++文件中提取出足够的信息,用于判断代码的结构,最终达到关键字提取的目的;题目主要难度在于选取合适的方式从文件中提取信息
-
语言选择
在读完该题后,我首先想到了使用正则表达式来解题,因为本题可能涉及到大量的字符串操作,而且文本信息量较大、可能存在较多的干扰项,使用正则表达式处理可以很大程度上减小工作量;而Python拥有较为成熟的正则库re,我之前也有接触过一些,所以这题我选择使用python来解决
-
关键字统计
在进行关键字统计之前,我先较为完整地再次学习了一遍正则的用法,查找了一些正则相关的资料,regex101 可以实时查看正则表达式的效果还是蛮好用的。
要对关键字进行可以使用正则表达式将所有只含有字母的字符串提取出来,再用count函数遍历关键字列表,统计所有符合条件的字符串。但是想要实现以上想法首先得解决对数据的清洗,根据C/C++知识可以得知,文本中的字符串内和注释中的出现满足条件的字串不属于关键字,是不需要统计的;除此之外如果变量名中出现了与关键字相同的字串也是不需要统计的,所以我们得首先排除以下几种情况再进行统计:
-
//注释
-
/* */注释
-
/* 注释(单符号会注释掉以下所有行)
-
' '单引号字符串
-
” “双引号字符串
-
变量名中“关键字”(例如int1qq)
以上前五种都可以使用正则表达式去掉,最后一个其实也可以,但是由于在关键字统计这一环节会把所有只含字母的字符串放在列表中再统计,所以不会产生影响
以下是实际使用时对应的正则表达式
-
所有注释:(//.*)|(/\*[\s\S]*?\*/)|(/\*[\s\S]*)
-
单引号字符串:(\'[\s\S]*?\')|(\'[\s\S]*)
-
双引号字符串:(\”[\s\S]*?\“)|(\”[\s\S]*)
-
所有可能为关键字的字符串:[a-zA-Z]{2,}
PS. 在VSCode中实测的时候发现 单个单引号‘或者单个双引号”也有可能将以下所有行都包括,所以在正则的时候也将这一部分考虑进去
-
-
switch-case统计
这一部分可以直接使用前面关键字统计中从C/C++程序代码中获取的关键字列表,然后计算每个switch之后到下一个switch之前的case数(最后一个switch就统计到末尾),在有前面关键字统计的基础上个这一块是比较简单的。但是有一点需要非常注意,内部没有case的switch应该排除(虽然可能不一定会出现这种情况)
-
if-else统计
这一部分思路也是比较明确的,可以通过正则表达式将文本中所有的if和else提取出来,放在列表里,然后由于if-else只有两种状态,可以使用类似于括号匹配问题的方式,使用栈来处理,这样的话就算有多余落单的if也可以处理。
但是这种方法也是有前提的:首先,使用这种方法统计,文本中不能包含有if-elseif-else结构;其次,也需要排除干扰项,例如qifsss,aelset的变量名会对if-else的统计产生干扰首先通过[\w](if|else)[\w]先将这些干扰项筛除,再使用(if|else)筛出if-else
-
if-(else if)-else统计
这一块就是本题的难点,首先如果采取跟if-else统计一样的方法,使用栈匹配,会出现一个问题:if-(else if)-else拥有三种状态,而栈只有进栈和出栈两种状态,只通过栈匹配时无法得出答案的,这点时很明确的,如果想要使用栈匹配进行解题需要额外的信息,这里我能够想到的信息有两种:
1.选择语句中可能有的括号信息
2.所有if/else if/else前面空格长度信息
如果使用括号信息,需要考虑在C/C++中语句如果只有单行的话是可以不需要括号的,所以使用括号信息需要分类讨论而且是否能够解决在分析阶段是无法确定的;而用正则去获取关键字前的空格长度作为辅助判断的信息,只要样例的代码风格能够保持一定,也就是属于同一个选择语句的关键字是对齐的,那么这样这个问题是一定能够解决的,
所以这里我选择当赌狗,所以这里我选择了用正则去获取关键字前的空格长度,然后与栈匹配一起使用,就可以解决这个问题
设计实现过程
代码整体框架
关键函数流程图
-
clean_data()
-
if_elseif_else_count()
基于前向空格数的栈操作说明
- 从提取出的关键字列表取出关键字后,首先判断该关键字与栈顶关键字的空格位置关系,若栈顶关键字的前向空格数比该关键字大,则将栈顶弹出;重复以上过程至不满足判断条件为止
- 若从提取出的关键字列表中取出的关键字是if直接入栈
- 若从提取出的关键字列表中取出的关键字是else if,若栈不为空且栈顶为if则入栈
- 若从提取出的关键字列表中取出的关键字是else,若栈顶if,则栈顶弹出,if_else结构数加1;若栈顶是else if,则将从栈顶开始所有的else if弹出,最后再弹出一个if,if_elseif_else结构数加1
- 循环以上步骤,至提取出的关键字列表遍历完成
代码说明
-
数据处理
# 去除文本干扰成分(注释、字符串) def clean_data(text): # 匹配所有注释 pattern_notes = r'(//.*)|(/\*[\s\S]*?\*/)|(/\*[\s\S]*)' # 匹配单引号字符串 pattern_str1 = r'(\'[\s\S]*?\')|(\'[\s\S]*)' # 匹配双引号字符串 pattern_str2 = r'(\"[\s\S]*?\")|(\"[\s\S]*)' text = re.sub(pattern_notes, lambda x: generate_str(x.group()), text, flags=re.MULTILINE) text = re.sub(pattern_str1, lambda x: generate_str(x.group()), text, flags=re.MULTILINE) text = re.sub(pattern_str2, lambda x: generate_str(x.group()), text, flags=re.MULTILINE) return text # 用与替换时生成等长空字符串 def generate_str(str): temp = "" for i in range(len(str)): temp += " " return temp
使用正则去除文本中的干扰成分(注释,字符符串)
PS.由于在第四步中使用到了统计if/else if/else前面的空格,所以在输出预处理阶段要非常注意,这里使用lambda表达式将干扰项替换成等长的空格,防止干扰if_elseif_else_count()的统计
-
关键字统计
# 得出关键字数 def key_count(text): pattern_num = r'[a-zA-Z]{2,}' key_data = re.findall(pattern_num, text) num = 0 for key in key_list: num += key_data.count(key) return key_data, num
这里使用[a-zA-Z]{2,}是由于最小关键字的长度为2,所以只筛选长度大于2且只含字母的字串
-
switch-case统计
# 得出switch_case结构数 def switch_case_count(key_data): case_num = [] switch_num = 0 temp_case = 0 for value in key_data: if value == "switch": if switch_num > 0: case_num.append(temp_case) temp_case = 0 switch_num += 1 if value == "case": temp_case += 1 case_num.append(temp_case) # 处理不带有case的switch num = case_num.count(0) for i in range(num): case_num.remove(0) switch_num -= num return switch_num, case_num
这里需要注意的就是不含case的switch,需要将其排除
-
if-else统计
# 单纯只有if_else def if_else_count(text): pattern_out = r'[\w](if|else)[\w]' pattern_key = r'(if|else)' # 排除变量名干扰 text = re.sub(pattern_out, ' ', text, flags=re.MULTILINE) key_data = re.findall(pattern_key, text) # print(key_data) stack = [] if_else_num = 0 for index, values in enumerate(key_data): if values == 'if': stack.append(index) else: if len(stack) == 0: continue stack.pop() if_else_num += 1 return if_else_num
由于此函数设计时不考虑if-elseif-else结构,所以直接使用栈匹配操作,统计if-else的数量
-
if-(else if)-else统计
# if-else 与 if—elseif-else混合 def if_elseif_else_count(text): pattern_out = r'[\w](else if|if|else)[\w]' pattern_key = r'(else if|if|else)' # 排除变量名干扰 text = re.sub(pattern_out, ' ', text, flags=re.MULTILINE) key_data = re.findall(pattern_key, text) # 统计if/else if/else前向空格 pattern_front_space = r'\n( *)(?=if|else if|else)' space_data = re.findall(pattern_front_space, text) space_data = [len(i) for i in space_data] # 1代表if/ 2代表else if/ 3代表else/ stack = [] if_else_num = 0 if_elseif_else_num = 0 for index, values in enumerate(key_data): while len(stack) > 0: if space_data[index] < space_data[stack[len(stack) - 1]]: stack.pop() else: break if values == 'if': stack.append(index) elif values == 'else if': if len(stack) == 0: continue if key_data[stack[len(stack) - 1]] == 'if': stack.append(index) else: if len(stack) == 0: continue if key_data[stack[len(stack) - 1]] == 'if': if_else_num += 1 stack.pop() else: while len(stack) > 0: if key_data[stack[len(stack) - 1]] == 'else if': stack.pop() else: break stack.pop() if_elseif_else_num += 1 return if_else_num, if_elseif_else_num
这里使用的是基于前向空格数的栈操作,流程图阶段已经说明,不再赘述
单元测试
-
测试文本
为测试构造了三份测试文本,以下是其中一份
#include <stdio.h> void test(){ printf("asdfasdf xxd dsff"); printf('d'); } int main(){ int i=1; double j=0; long f;//sssasddd dcad //asdad switch(i){ case 0: break; case 1: break; case 2: break; default: break; }/*ddsfsfsfd*/ switch(i){ case 0: break; case 1: break; default: break; } if(i<0){ if(i<-1){} else{ if(i==2); } } else if(i>0){ if (i>2){ if(i!=1); else if(i>1); } else if (i==2) { if(i<-1){} else if(i==1){} else{} } else if (i>1) {} else { } } else{ if(j!=0){} else{} } switch(i){} return 0; } /*ds asdasdasd asdsadasd as?
-
测试代码
import unittest import sys sys.path.insert(0, "../code") from keyword_recognition import * class MyTestCase(unittest.TestCase): def test_clean_data(self): str1 = "/*sss*/ 'dd'" str_ans1 = " " str2 = "/*ddddd \n*/" str_ans2 = " " str3 = '"xxsubtxxxy"' str_ans3 = " " self.assertEqual(clean_data(str1), str_ans1) self.assertEqual(clean_data(str2), str_ans2) self.assertEqual(clean_data(str3), str_ans3) def test_key_count(self): text = read_file("../data/key.c") temp, num = key_count(text) self.assertEqual(num, 35) def test_switch_case_count(self): key_data = ['include', 'stdio', 'int', 'main', 'int', 'double', 'long', 'switch', 'case', 'break', 'case', 'break', 'case', 'break', 'default', 'break', 'switch', 'case', 'break', 'case', 'break', 'default', 'break', 'if', 'if', 'else', 'else', 'if', 'if', 'else', 'if', 'else', 'if', 'else', 'else', 'if', 'else', 'return'] switch_num, case_num = switch_case_count(key_data) self.assertEqual(switch_num, 2) self.assertEqual(case_num, [3, 2]) def test_if_else_count(self): text = read_file("../data/text.c") if_else_num = if_else_count(text) self.assertEqual(if_else_num, 4) def test_if_elseif_else_count(self): text = read_file("../data/key.c") temp, if_elseif_else_num = if_elseif_else_count(text) self.assertEqual(if_elseif_else_num, 2) if __name__ == '__main__': unittest.main()
单元测试结果显示函数功能正常
-
覆盖性测试截图
由于这里只测试了几个关键函数,所以覆盖率只有58%
-
性能测试
由于单次运行文件时间太短,不能很好的呈现测试结果,这里将文件运行10000次
import sys sys.path.insert(0, "../code") from code.keyword_recognition import start if __name__ == '__main__': for i in range(10000): start(filepath='../data/key.c', level=4)
通过性能测试可以了解到,由于在模式level选择时,我采取了嵌套的方式,所以这里level类型函数占用时间非常高,后续可以考虑将level重新设计解耦合,来提高效率
困难描述与解决方案
-
问题描述:在进行数据清洗的时候,由于使用/* */注释时可以跨多行,但是只使用re.sub进行替换时,识别不到\n换行符
解决方案:在查找资料后,发现其实re.sub中可以设置参数flags=re.MULTILINE,进行多行识别
-
问题描述:if-(else if)-else统计时,发现其实前面的清洗数据的方式有漏洞,我原本采用的时直接把所有的干扰项变成“ ”单空格,但是这样会使得统计出的前向空格失效
解决方案:这里曾经想过改变函数的使用,不再使用re.sub而是用比较复杂的方法先用finditer找到所有干扰项再一个一个替代,但是实现起来非常复杂;在查找资料和思考后,发现re.sub操作的match对象可以使用lambda式,所以这里直接使用lambda式返回一个与匹配到的字符串等长的空字符串
总结
-
通过这次的作业,我进一步学习了正则表达式的使用,对字符串操作有了更加深刻的认识;同时也首次接触到了单元测试与覆盖率、还有性能测试,对于如何更好使用编码完成一个问题有了更深刻的理解,除此之外,还学习到了如何更加规范地书写python代码。
-
通过这次作业也让我明白了以下这些道理:
1.选择和规划比努力更重要:如果事先规划好整体的工作流程,预估好大致的工作量,就会更加有方向,盲目的努力很可能只是浪费时间
2.Learning by doing可以很好的调动你以前学习过和正在学习的知识,能够增加你对知识的理解,而且运用实践的过程可以增加兴奋感,让你在快乐中提升自己
3.虽然deadline是第一生产力,但是还是应该早点开始,多一点时间可以让自己更从容
(可以多睡觉QAQ)
总体来讲,这次作业还是收获满满!