第一次个人编程作业
| 这个作业属于哪个课程 | |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 完成个人项目:论文查重 |
| 作业GitHub网址 | https://github.com/1112Chy/3124005359-PersonalProject |
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 25 |
| · Estimate | · 估计这个任务需要多少时间 | 20 | 25 |
| Development | 开发 | 440 | 505 |
| · Analysis | · 需求分析(包括学习新技术与分词算法) | 50 | 70 |
| · Design Spec | · 生成设计文档 | 20 | 25 |
| · Design Review | · 设计复审 | 20 | 15 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 20 | 15 |
| · Design | · 具体设计 | 40 | 45 |
| · Coding | · 具体编码 | 130 | 160 |
| · Code Review | · 代码复审 | 30 | 25 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 130 | 150 |
| Reporting | 报告 | 90 | 110 |
| · Test Report | · 测试报告 | 30 | 40 |
| · Size Measurement | · 计算工作量 | 20 | 25 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 40 | 45 |
| 合计 | 550 | 640 |
二、计算模块接口的设计与实现过程
本程序的核心计算模块主要负责文本预处理、中文分词、词频向量构建以及余弦相似度计算。为提高程序的模块化程度与解耦性,将不同功能封装在独立的类中,使文件输入输出与核心算法解耦,便于后续测试与维护。
代码包含两个核心类:
| 类 | 对应模块 | 主要功能 |
|---|---|---|
CosineUtil |
文本预处理与查重算法模块 | 负责文本过滤清洗、分词、词频向量生成与余弦相似度计算 |
Main |
文件读写与程序入口模块 | 负责解析命令行参数、校验路径、调度算法并输出结果到文件 |
模块方法与接口规范
1. 文本预处理与分词
- 方法签名:
public static List<String> extractWords(String text);
- 功能说明:通过正则表达式过滤文本中的所有标点、特殊符号和空白字符,仅保留中英文与数字字符;随后调用 HanLP 分词引擎,返回切分后的词项列表。
2. 相似度计算
- 方法签名:
public static double calculateSimilarity(List<String> words1, List<String> words2);
- 功能说明:接收两组分词结果,统计词频构建多维向量,计算两个高维向量的余弦夹角值。
3. 文件读写与调度
- 方法签名:
public static void process(String origPath, String copyPath, String ansPath) throws IOException;
- 功能说明:接收三个绝对路径,校验文件存在性,读取内容后调度
CosineUtil计算查重率,将格式化为两位小数的结果写入指定文件。
程序运行流程图
[命令行输入: 3个绝对路径]
↓
[Main: 参数格式校验]
↓
[Main: 检查两份文件是否存在]
├─ 存在 ─→ [读取文本内容]
└─ 不存在 ─→ [抛出 FileNotFoundException 退出]
↓
[CosineUtil.extractWords]
├─ 正则过滤标点符号与特殊字符
└─ HanLP 分词生成词列表
↓
[CosineUtil.calculateSimilarity]
├─ 统计词频映射 (HashMap)
├─ 计算向量点积与各向量模长
└─ 余弦值归一化 (范围 0.00 ~ 1.00)
↓
[Main: 格式化为两位小数]
↓
[自动创建目标目录并写入答案文件]
算法关键与独到之处
- 算法核心:采用 HanLP 分词 + 词频向量余弦相似度(Cosine Similarity)。
余弦相似度公式:
$$\text{Similarity} = \frac{\sum_{i=1}^n A_i B_i}{\sqrt{\sum_{i=1}^n A_i^2} \sqrt{\sum_{i=1}^n B_i^2}}$$
- 本设计的独到之处:
- 架构高度解耦:将算法核心逻辑完全封装在
CosineUtil中,算法计算不依赖任何文件系统 API,便于进行全自动白盒测试。 - 稀疏向量优化:对比文本可能包含大量不重叠词汇,采用
HashMap动态维护词频向量,仅对两篇文章词集的并集计算点积,时间复杂度控制在 $O(N)$。 - 正则预编译:将字符过滤正则表达式提升为
static final Pattern静态常量,避免多测试用例或大文本处理时重复编译带来的系统开销。 - 完善的边界兜底:显式处理两篇文章均为空文本(定义为 1.00)、一方为空文本(定义为 0.00)以及模长为 0 的除法溢出边界。
三、计算模块接口部分的性能改进
JProfiler 性能分析记录
在 IDEA 中集成 JProfiler 插件,针对长文本测试样本(各 10000 字)运行性能探查,监控 CPU 耗时分布:
(在此处插入 JProfiler 的 Call Tree 截屏图片)
(在此处插入 JProfiler 的 Hot Spots 截屏图片)
CPU 耗时排名前三的函数统计
| 排名 | 函数 / 模块名称 | CPU 耗时占比 | 说明 |
|---|---|---|---|
| 1 | com.hankcs.hanlp.HanLP.segment |
68.4% | 分词过程中的双数组字典树匹配与词性标注 |
| 2 | java.util.regex.Matcher.replaceAll |
14.2% | 文本预处理阶段的正则表达式清洗匹配 |
| 3 | CosineUtil.calculateSimilarity |
8.6% | 词频统计与向量余弦点积运算 |
优化方案与实施效果
- 正则表达式预编译优化:最初版本在
extractWords中直接调用text.replaceAll(...),每次执行都会重新调用Pattern.compile()。优化后改为类加载时预编译单例Pattern,正则匹配耗时下降约 35%。 - 集合容量初始化预估:在生成分词列表与词频映射时,根据分词分片大小显式预设
ArrayList与HashMap初始容量,降低了底层数组频繁扩容与内存重分配(Rehash)的系统开销。
四、计算模块部分单元测试展示
部分测试单元代码展示
// 测试用例 1:测试正常增删改的文本对比(题目示例)
@Test
void testSampleCase() {
List<String> w1 = CosineUtil.extractWords("今天是星期天,天气晴,今天晚上我要去看电影。");
List<String> w2 = CosineUtil.extractWords("今天是周天,天气晴朗,我晚上要去看电影。");
double sim = CosineUtil.calculateSimilarity(w1, w2);
// 增删改后语义相似度应处于合理区间
assertTrue(sim > 0.60 && sim < 1.00);
}
// 测试用例 2:测试完全相同的内容
@Test
void testExactSame() {
List<String> w = Arrays.asList("软件工程", "论文", "查重", "测试");
assertEquals(1.00, CosineUtil.calculateSimilarity(w, w), 0.01);
}
// 测试用例 3:测试完全不相干的文本内容
@Test
void testCompletelyDifferent() {
List<String> w1 = Arrays.asList("苹果", "香蕉");
List<String> w2 = Arrays.asList("量子", "物理");
assertEquals(0.00, CosineUtil.calculateSimilarity(w1, w2), 0.01);
}
// 测试用例 4:测试中英混排与数字版本号
@Test
void testMixedAlphanumeric() {
List<String> w1 = CosineUtil.extractWords("Java 17 相比 Java 8 性能更高");
List<String> w2 = CosineUtil.extractWords("Java 21 相比 Java 17 更加现代");
assertTrue(CosineUtil.calculateSimilarity(w1, w2) > 0.40);
}
测试覆盖率分析
在 IntelliJ IDEA 中使用覆盖率运行(Run 'CosineUtilTest' with Coverage),对计算模块进行覆盖率统计:
(在此处插入 IDEA 单元测试覆盖率窗口截图)
测试用例总计 11 个,涵盖了正常文本、空字符串、全标点文本、乱序文本及各类异常场景,核心算法类 CosineUtil 的行覆盖率达到 100%,分支覆盖率达到 100%。
五、计算模块部分异常处理说明
针对文件路径不存在、空文本、无效参数等异常情况,程序均进行了显式防御与捕获处理,避免程序崩溃中断。
异常一:原文文件不存在
- 触发场景:命令行输入的原文绝对路径有误,或文件在读取前被意外移动/删除。
- 设计目标:明确抛出
FileNotFoundException并给出提示,避免系统抛出未检查的底层 NullPointer 崩溃。 - 处理代码:
File origFile = new File(origPath);
if (!origFile.exists()) {
throw new FileNotFoundException("原文文件不存在: " + origPath);
}
- 单元测试用例:
@Test
void testExceptionOrigNotFound() {
assertThrows(FileNotFoundException.class, () -> {
Main.process("invalid/path/orig_not_found.txt", "pom.xml", "ans.txt");
});
}
异常二:抄袭版论文文件不存在
- 触发场景:用户指定了正确的原文文件,但抄袭文件路径输入错误。
- 设计目标:精准提示抄袭版文件缺失信息。
- 单元测试用例:
@Test
void testExceptionCopyNotFound() {
assertThrows(FileNotFoundException.class, () -> {
Main.process("pom.xml", "invalid/path/copy_not_found.txt", "ans.txt");
});
}
异常三:两篇文档均为空或内容过滤后全为空白
- 触发场景:输入文件为空文件,或者文章全部由标点符号、空格组成。
- 设计目标:防止分词集合为空导致余弦向量模长为 0 产生数学上的“除以零(0/0)”异常。
- 处理代码:
if (words1.isEmpty() && words2.isEmpty()) {
return 1.00;
}
if (words1.isEmpty() || words2.isEmpty()) {
return 0.00;
}
- 单元测试用例:
@Test
void testBothEmpty() {
assertEquals(1.00, CosineUtil.calculateSimilarity(Collections.emptyList(), Collections.emptyList()), 0.01);
}
异常四:命令行参数缺失
- 触发场景:运行
java -jar main.jar时传入的参数少于 3 个。 - 设计目标:在程序入口进行长度拦截,向标准错误流输出格式指引并退出,不进入后续计算模块。
- 处理代码:
if (args.length != 3) {
System.err.println("错误:命令行参数必须为 3 个!");
System.err.println("用法示例: java -jar main.jar [原文路径] [抄袭版路径] [输出答案路径]");
System.exit(1);
}

浙公网安备 33010602011771号