第一次个人编程作业
个人项目作业-论文查重
| 这个作业属于哪个课程 | 双学位22级 (广东工业大学) |
|---|---|
| 这个作业要求在哪里 | 个人项目作业-论文查重 |
| 这个作业的目标 | 开发一个论文查重软件,并通过Git提交到远程代码库,熟悉软件开发流程 |
| 其他参考文献 | 现代软件工程讲义 2 工程师的能力评估和发展 Commit message 和 Change log 编写指南 现代软件工程讲义 2 开发技术 - 单元测试 & 回归测试 |
链接:https://gitcode.com/m0_75104457/3122000597
模块接口的设计与实现过程
本项目的代码结构用以下这张类图来表示:

本项目构建的jar包以com.leonard.util为根路径。核心类和接口包括:
- App类:主程序(main方法)所在的类。
- SimHasher类:由SimHashChecker类使用,包含SimHash算法的具体实现。
- similarity包:包含论文查重相关的类和接口
- analysis包:文本处理相关包
- WordAnalyzer接口:提供统一的文本处理方法的规范,核心方法为analyze()、splitWords()。对于analyze()方法的其中一个重载,该接口提供了默认的、通用的具体实现逻辑——直接计算各个分词在文本中出现的频率(即词频)。
- AnsjWordAnalyzer类:WordAnalyzer接口的实现类,使用ansj中文分词工具做文本处理工作。
- HanLPWordAnalyzer类:WordAnalyzer接口的实现类,使用HanLP中文分词工具做文本处理工作。本项目生成的应用默认使用此分词器。
- checker包:相似度分析相关包
- Checker接口:提供统一的分析两个文本相似度的规范,核心方法为checkSimilarity()。
- SimHashChecker类:Checker接口的实现类,采用SimHash算法来分析两个文本的相似度,其中使用到了SimHasher类。
- CosineChecker类:Checker接口的实现类,采用余弦相似度算法来分析两个文本的相似度。
- SimilarityChecker类:主程序使用的类,核心方法为check(),用于接收文本输入,调用相应的Checker的checkSimilarity方法,并将处理结果输出到指定文件中。
- SimilarityCheckUtils类:论文查重工具类,封装了各类使用不同分词器、不同算法的Checker,通过方法返回给调用方。
- analysis包:文本处理相关包
算法关键:
- App类中的主程序在创建一个SimilarityChecker对象时,向其构造方法传入SimHashChecker或CosineChecker对象(指明要使用基于哪种算法的文本相似度分析器,方便更改),接着再向SimHashChecker的构造方法传入AnsjWordAnalyzer或HanLPWordAnalyzer对象(指明要使用哪种分词工具做文本处理),随后调用check()方法,向该方法传入三个参数:程序从命令行接收的原文文件、待查重论文文件、目标输出文件的路径。
- SimilarityChecker.check()方法读取原文文件、待查重论文文件的内容,并传给分析器的checkSimilarity()方法。分析器计算完成后,返回两个文件内容的相似度,check()方法将结果写入到答案文件中。
- 分析器执行文本相似度分析逻辑:以SimHashChecker为例,它会调用SimHasher的hammingDistance()方法来计算两个文本的海明距离d,取SimHash值的位数为64,公式为:相似度=1 - d/64。(这里不展开讲述SimHash算法的原理及其Java实现)
计算模块接口部分的性能改进
在改进计算模块的性能上花费了大约三个小时左右。下图分别为IntelliJ IDEA的测试运行结果以及JProfiler的性能分析结果:


性能在空间方面没有问题,主要问题在于耗费的时间。程序占用的内存大小最大为218.7MB,不超过2048MB。从运行结果可以看到,程序处理orig.txt(原文文件)的文本的耗时长达10秒,严重影响程序性能,而处理其它文件的内容耗时基本不超过100ms。程序已经使用到parallelStream来实现大文本的分块并行处理,但性能提升效果仍然不太明显。
后续的测试中程序耗时都是在6~8秒之间,但在IDE被重新打开后第一次运行程序,耗时依然长达10秒。原因有待进一步探究。
经过查阅网上资料后发现,原因在于ansj文本处理库中的ToAnalysis().parse()方法在第一次加载词典时会特别久,但使用自定义词典会对分析结果有所影响。尝试改用HanLPWordAnalyzer后,性能测试结果如下:


可以看到,速度显著提升了十几倍,运行结果也显示程序在处理orig.txt文件的内容时也仅耗时643ms。两种分词工具在程序首次运行时都会自动加载词典,但HanLP更加高效、简单、轻量级,其默认词典大小仅600MB,因此它适用于大多数中文分词场景。
这次改动在提升性能的同时,也难免对处理结果产生了影响,可以看到改动前后程序的输出结果明显不同:只讨论SimHash相似度,在对比orig.txt和orig_0.8_add.txt这两个文件内容的相似度时,使用ansj分词器的结果是0.9375,而使用HanLP分词器的结果却是0.984375。得出结论:对于需要兼顾效率和准确率的中文文本处理场景,采用HanLP较为合适;对于注重准确度的场景,可以选择采用ansj。
计算模块部分单元测试展示
测试源码:
package com.leonard;
import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import com.leonard.util.similarity.checker.Checker;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import com.leonard.util.similarity.SimilarityCheckUtils;
import static org.junit.jupiter.api.Assertions.*;
public class CheckerTest {
/** 项目根路径 */
private static final String root = System.getProperty("user.dir");
/** 测试文件的文件名前缀 */
private static final String prefix = "orig_0.8_";
/** 测试文件所在路径 */
private static final String dirName = "src/test/resources";
/** 原文文件名 */
private static final String originalFileName = "orig.txt";
/** 原文文件路径 */
private static final Path originalFilePath = Paths.get(root, dirName, originalFileName);
/**
* 获取测试文件的文件名及路径,并提供给测试方法 test()
*
* @return 为 test() 方法提供的参数
* @throws IOException Files.list(Path) 抛出
*/
static Stream<Arguments> loadTestFiles() throws IOException {
return Files.list(Paths.get(root, dirName))
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().startsWith(prefix))
.map(path -> Arguments.of(path.getFileName().toString(), path));
}
/** 测试开始之前,显示原文的文件名 */
@BeforeAll
public static void beforeTest() {
System.out.println("原文文件: " + originalFileName);
}
/**
* 单元测试
*
* @param fileName 测试文件名
* @param filePath 测试文件所在路径
* @throws IOException Files.readString(Path) 抛出
*/
@ParameterizedTest(name = "[{index}] 测试文件: {0}")
@MethodSource("loadTestFiles")
public void test(String fileName, Path filePath) throws IOException {
String originalContent = Files.readString(originalFilePath);
assertNotSame(originalContent.trim(), "");
String content = Files.readString(filePath);
assertNotSame(content.trim(), "");
// 使用 HanLP 分词器
double simHashSimilarity =
testChecker(SimilarityCheckUtils.simHashCheckerWithHanLP(), originalContent, content);
BigDecimal bd = new BigDecimal(simHashSimilarity);
// 结果精确到小数点后两位
simHashSimilarity = bd.setScale(2, RoundingMode.HALF_UP).doubleValue();
System.out.println("测试文件: " + fileName + ", SimHash 相似度: " + simHashSimilarity);
System.out.println();
}
/**
* 测试主体,要测试的是 checkSimilarity() 这个方法
*
* @param checker 欲使用的文本相似度分析器
* @param s1 文本1
* @param s2 文本2
* @return s1 和 s2 的内容相似度
*/
private double testChecker(Checker checker, String s1, String s2) {
long start = System.currentTimeMillis();
// 测试对象
double result = checker.checkSimilarity(s1, s2);
long end = System.currentTimeMillis();
// 必须 5 秒之内给出答案
assertTrue(end - start <= 5000);
// 结果必须是 0 到 1 之间的小数
assertTrue(result < 1 && result > 0);
System.out.println(checker.getClass().getName() + " 耗时 " + (end - start) + " ms");
return result;
}
}
测试目标:checkSimilarity()方法。
测试思路:读取测试用例中所有待查重论文文件的内容,分别与原文文件的内容作文本相似度分析,并在测试前后分别计时,以计算单次处理所耗费的时间。使用HanLP作为分词器。
测试覆盖率:如下图所示。

计算模块部分异常处理说明
-
IllegalArgumentException:输入文本为空,通常是文件的内容为空。
// ExceptionTest.java package com.leonard; import org.junit.Test; import com.leonard.util.similarity.SimilarityCheckUtils; public class ExceptionTest { @Test(expected = IllegalArgumentException.class) public void testEmptyInput() { SimilarityCheckUtils.simHashCheckerWithHanLP().checkSimilarity("", ""); } } // AnsjWordAnalyzer.java public class AnsjWordAnalyzer implements WordAnalyzer { //... @Override public List<String> splitWords(String text) { if (StringUtils.isBlank(text)) { throw new IllegalArgumentException("空白文本"); } //... } //... } // HanLPWordAnalyzer.java public class HanLPWordAnalyzer implements WordAnalyzer { //... @Override public List<String> splitWords(String text) { if (StringUtils.isBlank(text)) { throw new IllegalArgumentException("空白文本"); } //... } //... } // WordAnalyzer.java public interface WordAnalyzer { //... default Map<String, Integer> analyze(List<String> words) { if (words.isEmpty()) { throw new IllegalArgumentException("没有分词,文本可能为空"); } //... } //... } -
其它类型异常的处理待开发
PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | · 估计这个任务需要多少时间 | 5 | 3 |
| Development | 开发 | ||
| · Analysis | · 需求分析 (包括学习新技术) | 5 | 8 |
| · Design Spec | · 生成设计文档 | 15 | 20 |
| · Design Review | · 设计复审 | 5 | 4 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 2 |
| · Design | · 具体设计 | 10 | 15 |
| · Coding | · 具体编码 | 30 | 40 |
| · Code Review | · 代码复审 | 10 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 15 | 35 |
| Reporting | 报告 | ||
| · Test Repor | · 测试报告 | 10 | 5 |
| · Size Measurement | · 计算工作量 | 5 | 3 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 35 |
| · 合计 | 145 | 190 |

浙公网安备 33010602011771号