软件工程主页
作业要求
作业目标
代码实现、单元测试、性能分析、PSP表格统计
作业需求:
题目:论文查重
描述如下:
设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。
原文示例:今天是星期天,天气晴,今天晚上我要去看电影。
抄袭版示例:今天是周天,天气晴朗,我晚上要去看电影。
要求输入输出采用文件输入输出,规范如下:
从命令行参数给出:论文原文的文件的绝对路径。
从命令行参数给出:抄袭版论文的文件的绝对路径。
从命令行参数给出:输出的答案文件的绝对路径。
我们提供一份样例,课堂上下发,上传到班级群,使用方法是:orig.txt是原文,其他orig_add.txt等均为抄袭版论文。
注意:答案文件中输出的答案为浮点型,精确到小数点后两位
导读:
阅读这篇博客可能需要花费:10-20mins
阅读这篇博客可能会收获/学习到:
——程序设计思想方面:
1、在对一个文本进行处理前,文本清洗步骤是必不可少的环节。
2、不是所有的项目都可以使用多进程、多线程编程。
——编程实践方面:
1、了解到 pytest、memory_profile、coverage、cProfile 等单元测试、性能测试工具的基础使用方式以及一些异常情况的处理
脚本编写心路:
一开始拿到作业要求,联想到当初 python 入门的时候恰好学了 jieba 这个专用于分词的第三方库。成,就使用 jieba 分词,并统计重复词在原文中的占比,立刻就能得到文本相似度。为了提高“逼格”,当时设想将两篇文章都拆分成一个个句子,然后以句子为单位分词并得到相似率,将两篇文章句子们的相似率来一个加权平均,啪,文章的重复率不就出来了?同时,为了提高“性能”,使用多线程和多进程来完成句子的分词和相似率计算任务。然而现实是十分残酷的,这个方法在简单的,不调乱语句顺序的文本对比案例中是可行的。然而当我拿到第二个对比案例,好家伙,语句顺序调乱了不说,语句和语句之间还插入了若干不明html代码。于是,前面所设想的解决方案彻底失效,只能写一个极其简单的文本查重脚本出来。
从这个作业中,我们就认识到了,并不是所有的项目都可以使用多进程或多线程来完成。就本人目前所知,多线程和多进程技术应当使用在:科学计算、网页爬虫、爆破(重放)攻击(验证码爆破、密码爆破、ARP毒害等)、端口扫描,以及一些特殊情形中(如文件上传漏洞的条件竞争利用),其余项目应当不使用多线程和多进程技术。同时,通过这个项目,我们也认识到,当处理的任务量不大的时候,相对于单线程或单进程,使用多进程技术并不会带来显著的速度提升。
通过这次文本处理作业,我们也知道了,在拿到一个文本的时候,必须对其进行基本的处理(文本内容清洗)。将这个体会抽象概括一下:当拿到一个数据的时候,在使用、对这个数据进行操作之前,必须要对数据进行事先的处理。而处理的细则应当结合当前项目要求来定。
程序运行逻辑
技术要点陈列:
PS:由于并没有使用数学工具,因此技术要点乏善可陈,下面只做简单陈述,本文着重关注单元测试、性能测试部分。
1、文本清洗:
大体思路:使用正则表达式将中文字符、中文标点符号找出,然后将他们拼接起来,组成一个没有英文文本、特殊字符的纯中文文本(因此该脚本只能够用于中文文本的查重)。代码如下所示:
self.__f2 = re.findall('[\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b\u4e00-\u9fa5]',file2_content)
self.__f1 = ''.join(self.__f1)
self.__f2 = ''.join(self.__f2)
代码释义:
其中,正则表达式
“\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b”表示查找中文标点;
“\u4e00-\u9fa5”表示查找中文字符。
2、词频统计
大体思路:使用 jieba 来对第一篇文章整篇进行分词,并统计词语出现次数,生成一个词频字典。然后对第二篇文章进行分词,遍历这些词语,统计这些词语在词频字典中的出现次数,再跟第一篇文章所出现的词语数量作比,得到文章相似度(此计差矣,应当使用数学工具统计距离来得到文章相似度)。代码如下(代码过于简单,可以不看):
'''
__analysis()私有方法:
传入参数:
text1:要进行比对的字符串1
text2:要进行比对的字符串2
方法功能:
使用jieba分词,并统计词频,查看两个字符串中重复的词语数目,并以此得到两个文本的相似率。并统计所花费的时间。将重复率、所花费时间打包成为列表
作为返回值。
'''
def __analysis(self,text1,text2):#该函数应当返回:查重率。
return_list = []
t1 = time.time()
word_ls1 =jieba.lcut(text1)
word_ls2 = jieba.lcut(text2)
word_ls1 = [i for i in word_ls1 if i not in ('', ',', '、', '-', '“', '”', ':','。')] # 去除特殊符号
word_ls2 = [i for i in word_ls2 if i not in ('', ',', '、', '-', '“', '”', ':','。')]
dict = {}
same_words = 0
for w in word_ls1:
dict[w] = dict.get(w,0)+1
#print(dict)
for w in word_ls2:
try:
if (dict[w]):
same_words+=1
except:
pass
t2 = time.time()
take_time = t2-t1
return_list.append(take_time)
rate = same_words/len(word_ls1)
return_list.append(rate)
return return_list #返回值是一个包含相似率、所花费时间的一个列表
单元测试:
此处将介绍,使用 pytest 对代码进行自动化测试,使用 memoory_profile 对代码进行进行内存测试,使用 cProfile 进行函数运行时间测试,以及使用 coverage 进行代码覆盖量的测试。
·使用pytest进行自动化测试
使用pytest进行单元测试大致有以下几个步骤:
(1、编写测试脚本;
(2、pycharm或命令行环境运行pytest进行单元测试
(3、导出测试报告
事先配置:
pytest是一个第三方自动测试框架,需要进行事先安装:
pip install pytest
如果需要导出测试报告,还需要额外安装用于导出测试报告的插件:
pip install pytest-html
编写测试脚本
推荐在项目根目录下新建一个“case”文件夹,用于存放测试脚本和测试用例,在后续测试过程中,生成的测试报告文件都将存放在该文件夹下,避免“污染”项目文件。编写测试脚本的时候,首先第一件事就是引入自己所写的模块,然后再调用模块中的方法和函数。在此处,我们在项目根目录编写一个 main.py 文件,并编写一个test.py 测试脚本,将 test.py 放在项目根目录下的 case 文件夹下。编写的测试脚本代码如下:
import sys
sys.path.append('..')
from main import diff_find
def test_legal_bench():
f1 = diff_find()
assert f1.f_open(r'C:\test\orig.txt',r'C:\test\orig_0.8_del.txt','abc.txt') #第一个测试点
assert f1.f_open(r'C:\test\orig.txt',r'C:\orig_0.8_add.txt','abc.txt')
assert f1.f_open(r'C:\test\orig.txt', r'C:\test\orig_0.8_dis_1.txt','abc.txt')
assert f1.f_open(r'C:\test\orig.txt', r'C:\\test\orig_0.8_dis_10.txt','abc.txt')
assert f1.f_open(r'C:\test\orig.txt', r'C:\test\orig_0.8_dis_15.txt','abc.txt')
assert f1.f_open(r'C:\test\file1.txt',r'C:\test\file2.txt','abc.txt')
def test_ilegal_bench():
f2 = diff_find()
assert f2.f_open(r'C:\test\orig.tbt',r'C:\test\orig_0.8_del.txt','abc.txt')==None
test_legal_bench()
test_ilegal_bench()
其中,测试可以使用assert断言来实现。当assert关键字后边表达式结果为 True 时,测试通过,否则将抛出Assertion error,终止测试,使得测试不通过。由此,我们就得到自动测试的思想,即需要人为事先判断测试函数的返回值。使用assert关键字来判断函数执行返回值与预期结果是否相符,如果不相符即告测试失败,终止测试。
第一次运行 pytest 时,需要在 pycharm 中配置测试框架为pytest:
然后就可以在run选项框中选择 pytest in 来进行测试:
当然我们亦可在命令行中运行pytest并导出测试报告。
打开命令行,切换命令行目录到测试脚本路径,运行命令:
pytest test.py --html=report.html --self-contained-html
命令参数注解:
report.html是保存测试报告的文件名(由于使用的时pytest-html插件,因此生成的肯定就是html文件啦)
--self-contained-html 参数是用来防止 pytest 生成 css 文件的。当不填该参数的时候,就会在命令运行路径生成一个“assets”文件夹,文件夹中保存有测试报告 html 页面所使用的css层级样式表文件。
回车运行,即可在当前路径下得到测试报告文件 report.html。
使用浏览器打开测试报告:
注意事项:
使用 chrome 浏览器打开英文版的测试报告的时候,应当把“自动翻译”关掉,否则就会影响排版。
·使用 coverage 测试代码覆盖率
安装coverage:
pip install coverage
接下来,还是利用我们先前编写的测试脚本test.py,调出命令行界面,切换目录至测试脚本所在目录,运行以下命令即可:
coverage run test.py
运行完之后即可在命令运行路径下得到一个“.coverage”文件。该文件用来存放最近一次 coverage 测试的结果。接下来使用命令:
coverage report -m
即可查看测试报告文件 .coverage 中的内容:
·使用 memory_profile 来测试内存占用
事先安装:
pip install memory_profile
接下来,需要在脚本的待测试函数(方法)前添加 profile 装饰器,如下图所示:
然后进入命令行界面,切换到待测试脚本main.py所在路径,运行命令:
python -m memory-profile [脚本文件接收的命令行参数]
即可看到运行main.py时各个函数的内存分布情况:
附:单位问题:MiB跟MB两个单位是不一样的。其中:
1MiB = 1024KiB
1MB = 1000KB
·使用cProfile进行函数时间分析
使用方法:
命令行切换到项目 main.py 所在路径,运行命令:
python -c cProfile [脚本参数]
即可给出时间测试的结果:
其中,显示的字段的含义如下:
ncalls:表示函数调用的次数。
tottime:函数内部消耗的时间,不包括调用其他函数的时间。
percall:即tottime/ncalls,每次调用时函数的平均内部执行时间。
cumtime:是该函数执行的总时间(包括子函数的执行时间)
percall:即cumtime/ncalls,即整个函数(包括子函数)运行一次的平均时间。
filename:lineno(function):指明了被分析函数的文件名、所处行号、函数名。
终于差不多结束了,我们对以上单元测试过程总结一下:
1、新建存放测试脚本、测试样本的文件夹,测试脚本,测试样本的测试范围应尽可能广;
2、配置pytest、coverage,并使用它们来运行测试脚本(test.py),得到各个样例的测试结果和代码覆盖率。
3、配置memory-profile、cProfile,并使用它们运行并测试项目文件(main.py),得到内存占用情况和函数的CPU运行时间分布。
当然,python的测试工具还有很多,各位可以按需选择。
简述踩过的几个坑:
1、如何引入上层文件夹中的模块
——当没有成功导入待测试模块时,coverage 测试、cProfile等测试运行会报错。
如下所示的文件层级目录:
project_file文件夹
main.py
case文件夹
test.py
于是,要使得test.py中能够引入main.py中的类或方法,需要如下三步:
①:在项目根目录下新建“init.py”文件,文件内不要填写任何内容。
②:在test.py中引入sys库,填写代码:sys.path.append('..')
③:即可完成引入。
全部的代码如下:
import sys
sys.path.append('..')
from main import *
运行memory-profile进行测试时出现“codeDecodeError”异常:
异常如下图:
显然就是打开测试样例文本的时候产生了编码错误问题。此时我们可以定位第三方库中出现异常的代码,并对之进行修改:
未修改时:
with open(filename) as f:
exec(compile(f.read(), filename, 'exec'), ns, ns)
修改之后:
f = open(filename,encoding='utf-8')
#with open(filename) as f:
exec(compile(f.read(), filename, 'exec'), ns, ns)
注意,对第三方库代码进行修改的时候,一定别忘了注释备份,以免搞乱。
最后,附上PSP表: