个人项目
这个作业属于哪个课程 | 计科22级12班 |
---|---|
这个作业要求在哪里 | |
这个作业的目标 | 完成个人项目,实现论文查重的功能,了解软件开发流程 |
Github链接
一.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 20 |
Estimate | 估计这个任务需要多少时间 | 60 | 70 |
Development | 开发 | 180 | 200 |
Analysis | 需求分析 (包括学习新技术) | 30 | 45 |
Design Spec | 生成设计文档 | 60 | 70 |
Design Review | 设计复审 | 30 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 15 |
Design | 具体设计 | 25 | 20 |
Coding | 具体编码 | 90 | 110 |
Code Review | 代码复审 | 60 | 40 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 50 |
Reporting | 报告 | 40 | 60 |
Test Repor | 测试报告 | 30 | 25 |
Size Measurement | 计算工作量 | 15 | 15 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 40 | 40 |
合计 | 720 | 810 |
二.项目设计
功能实现
为了比较两个文本的相似程度,这里先使用HanLp的分词工具将文本划分成多个关键词,之后用SimHash算法将这些关键词向量化再经过加权合并,合并的结果进行降维处理后获得文本的Simhash值,之后求出两个文本Simhash的海明距离,即两个字符串相同位置不同字符的个数,最后得出两个文本的重复率。
项目流程
文件结构
类图
实现关键
该算法的重点在于对文本文件内容的分词处理和相似度的计算,一开始我使用的是Ansj分词工具,可以运行出结果,但是会包含一部分的运行信息(如下图)。
所以后面改用了HanLp实现分词功能,相关配置参考了博客,而在得到文章的分词结果之后问题就变为如何计算这些词语的相似度,这里我用的是SimHash算法加海明距离来计算两个文本内容的重复率。由于SimHash算法中会对所有分词进行加权处理,所以即使将语句次序变换或者将文章打乱,使用SimHash算法所得出的结果也不会有太大变化。
三.性能分析
性能分析部分使用了JProfiler工具。
内存分配情况:
由于需要用列表记录分词结果,还要设置向量进行计算处理,消耗的内存还是比较大的。
目前打算的改进方向是看看能不能在分词的同时计算特征向量,可以减少空间开销,以及在这个基础用字词库(像Ansj在运行时用字词库可以提供更准确的分词结果),加快分词速度并提高查重结果的准确度。
四.单元测试
单元测试时我用的是Junit,但其实在IDEA里面新建Maven项目时会自动创建一个test文件夹用于编写测试类。
点击查看代码
@Test
public void a_testParameterNum() {//参数个数测试
assertEquals("提供的参数个数不正确",
assertThrows(Exception.class, ()-> Main.main(new String[]{""})).getMessage());
}
@Test
public void b_testNullPath() {//查重文件路径为空测试
assertEquals("参数中的文件路径为空",
assertThrows(Exception.class, ()-> Main.main(new String[]{null, "", ""})).getMessage());
}
@Test
public void c_testNull_ansPath() {//答案文件路径为空测试
assertEquals("答案文件路径为空,无法将结果写入",
assertThrows(Exception.class, ()-> Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
null})).getMessage());
}
@Test
public void d_testNotExist_ansFile() {//答案文件不存在测试
assertEquals("答案文件不存在",
assertThrows(Exception.class, ()-> Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
"D:\\Javacode\\PersonalProject\\a.txt"})).getMessage());
}
@Test
public void e_testNotExist_File(){//查重文件不存在测试
assertEquals("文件testfile1.txt不存在,无法查重",
assertThrows(Exception.class, ()-> Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\testfile1.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"})).getMessage());
}
@Test
public void f_testEmpty_File(){//查重文件为空测试
assertEquals("文件testfile2.txt内容为空",
assertThrows(Exception.class, ()-> Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\testfile2.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"})).getMessage());
}
@Test
public void g_testShort_File(){//查重文件过短测试
assertEquals("文件testfile3.txt内容过短",
assertThrows(Exception.class, ()-> Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\testfile3.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"})).getMessage());
}
@Test
public void h_test1() throws Exception {
Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_add.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"});
}
@Test
public void i_test2() throws Exception {
Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_del.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"});
}
@Test
public void j_test3() throws Exception {
Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_dis_1.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"});
}
@Test
public void k_test4() throws Exception {
Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_dis_10.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"});
}
@Test
public void l_test5() throws Exception {
Main.main(new String[]{
"D:\\Javacode\\PersonalProject\\orig.txt",
"D:\\Javacode\\PersonalProject\\orig_0.8_dis_15.txt",
"D:\\Javacode\\PersonalProject\\ans.txt"});
}
前面的测试主要是用Assert断言验证异常是否正确抛出(异常处理在下一个部分,包括空文件,文件不存在或文件过短等情况),后面5个是原文和各个抄袭文本的结果输出。
这里是各个测试命名开头是因为Jvm返回的结果有随机性,用了语句@FixMethodOrder(MethodSorters.NAME_ASCENDING)后会按方法名的字典序返回测试结果,为了让结果规范点就用abcdef开头了。
测试结果如下:
时间花费主要在HanLp的extractKeyword方法上,基本上不到4s可以跑完5个抄袭文件的查重结果。
测试覆盖率如下:
五.异常处理
在设计模块时只考虑了实现功能,没有将异常情况都划分到一个类里面,有点零零散散,不过这些异常基本上都是在读写文件时抛出。
main参数异常
点击查看代码
if(args.length !=3){
System.out.println("提供的参数个数不正确");
throw new Exception("提供的参数个数不正确");
}
原文或抄袭文件路径为空
点击查看代码
if(Path1==null||Path1.isEmpty()||Path2==null||Path2.isEmpty())//判断文件路径是否为空
{
System.out.println(NullPathErrorMessage);
throw new Exception(NullPathErrorMessage);
}
答案文件的路径为空
点击查看代码
if(Path3==null||Path3.isEmpty()) {//路径为空
System.out.println(nullPathErrorMessage);
throw new Exception(nullPathErrorMessage);
}
路径为有效字符串,但原文或抄袭文件不存在
点击查看代码
if(!file.exists()) {//判断文件是否存在
System.out.println(FileNotExistsErrorMessage);
throw new Exception(FileNotExistsErrorMessage);
}
路径为有效字符串,但答案文件不存在
点击查看代码
if(!new File(Path3).exists())
{
throw new Exception("答案文件不存在");
}
原文或抄袭文件内容为空
点击查看代码
if(str.toString().isEmpty()) {//判断文件内容是否为空
String EmptyFileErrorMessage="文件"+filename+"内容为空";
System.out.println(EmptyFileErrorMessage);
throw new Exception(EmptyFileErrorMessage);
}
原文或抄袭文件内容过短
点击查看代码
if(str.toString().isEmpty()) {//判断文件内容是否为空
String EmptyFileErrorMessage="文件"+filename+"内容为空";
System.out.println(EmptyFileErrorMessage);
throw new Exception(EmptyFileErrorMessage);
}
异常的各个测试用例结果:
后续修改代码的话应该会把这些异常写在用一个专门的Exception类,方便分模块处理。
六.运行结果
这个是打包后的jar包运行,我放在了main的resources文件夹下,由于我用的环境是jdk21,在其他jdk环境的机器上运行可能会提示版本不适配的问题。
可以正常运行出结果并且写入指定的文件中(前五个是单元测试的结果),第二次运行是答案文件不存在的情况,抛出了异常。
打包下载链接:Releases