| 这个属于哪个课程 | 23计科34班 |
|---|---|
| 这份作业的要求在哪里 | 个人项目 |
| 这个作业的目标 | 学会怎样去实现一个完整的项目 |
一、Github作业链接:https://github.com/dududu1012/dududu1012/tree/main/3123004500/PaperPlagiarismChecker
二、PSP表格(预估耗时与实际耗时)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 90 | 90 |
| Esitimate | 估计这个任务需要多少时间 | 45 | 60 |
| Development | 开发 | 240 | 300 |
| Analysis | 需求分析(包括学习新技术) | 60 | 60 |
| Design Spec | 生成设计文档 | 60 | 60 |
| Design Review | 设计复审 | 60 | 60 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 60 | 60 |
| Design | 具体设计 | 60 | 60 |
| Coding | 具体代码 | 60 | 60 |
| Code Review | 代码复审 | 60 | 60 |
| Test | 测试(自我测试、修改代码、提交修改) | 60 | 90 |
| Reporting | 报告 | 60 | 60 |
| Test Repor | 测试报告 | 60 | 60 |
| Size measurement | 计算工作量 | 10 | 10 |
| Postmortem&Process Improvement Plan | 事后总结,并提出过程改进计划 | 30 | 20 |
| 合计 | 1015 | 1110 |
三、计算模块接口的设计与实现过程
1.代码组织结构设计:
本项目采用单一主类 + 功能模块化方法的设计模式,将所有核心功能封装在PaperPlagiarismChecker类中,通过不同方法实现单一职责原则。
# 项目结构展示
PaperPlagiarismChecker
├─ 文件操作模块
│ ├─ validateFile():验证文件有效性
│ └─ readFile():读取文件内容
├─ 文本预处理模块
│ ├─ preprocessText():文本清洗与标准化
│ └─ filterEmptyTokens():过滤空标记
├─ 特征提取模块
│ ├─ selectNGramSize():动态选择n-gram粒度
│ └─ extractNgramSet():提取n-gram特征集合
├─ 相似度计算模块
│ ├─ calculateSimHash():计算SimHash值
│ ├─ murmurHash3():辅助哈希算法
│ ├─ calculateHammingDistance():计算海明距离
│ └─ calculateSimHashSimilarity():计算相似度
└─ 输出模块
├─ outputCurrentResult():输出查重结果
├─ logCheckResult():记录查重日志
└─ logError():记录错误信息
2.核心算法:Simhash算法
(一)算法关键说明:
- 动态 n-gram 特征提取
根据文本长度自动选择 1-gram 或 2-gram:短文本用 1-gram 避免特征稀疏,长文本用 2-gram 保留更多上下文信息
使用 HashSet 存储 n-gram 特征,自动去重同时提高查找效率 - SimHash 算法优化
采用 MurmurHash3 而非 Java 原生哈希:计算速度更快,分布更均匀,减少哈希碰撞
64 位向量设计:平衡计算效率与精度,相比 128 位向量降低计算复杂度 - 相似度转换机制
基于海明距离的线性转换:相似度 = 1 - 海明距离/64
确保结果在 [0,1] 区间,直观反映文本重合度
(二)算法独到之处:
- 自适应特征粒度: 不同于固定 n 值的实现,根据文本长度动态调整 n-gram 粒度,在处理不同长度文本时保持稳定性
- 工程化细节优化:
异常处理完善:文件操作全流程异常捕获与日志记录
资源管理:自动创建目录,避免路径不存在导致的错误 - 混合文本支持: 正则表达式设计同时支持中英文文本处理,解决多语言场景下的字符过滤问题
- 可追溯性: 详细日志记录机制,保存每次查重的关键参数(哈希值、n 值、token 数量等),便于结果验证和问题排查
四、计算模块接口部分的性能改进
1.定位性能瓶颈:
在改进前,需先通过 JProfiler/IDEA 自带性能分析工具 找到核心耗时点。对原代码分析后,发现3个关键性能瓶颈:
- extractNgramSet() 方法: 生成n-gram时频繁使用StringBuilder拼接字符串,产生大量临时对象,触发频繁 GC(垃圾回收),尤其处理长文本(如 10000 字以上论文)时耗时显著。
- calculateSimHash() 方法: 对n-gram集合的遍历和向量更新是 “串行执行”,当n-gram数量达数万级时,CPU利用率低,计算耗时随文本长度线性增长。
- murmurHash3() 方法: 原实现通过 String.getBytes(StandardCharsets.UTF_8) 转换字符,该操作会额外创建字节数组,且UTF-8编码转换存在一定开销,占整体哈希计算耗时的30%+。
2.改进思路:
- 针对extractNgramSet(): 自定义NGramHolder类,直接引用原tokens数组的片段(记录起始 / 结束索引),不生成新字符串;仅在需要实际字符串时(如哈希计算)才拼接,减少不必要的内存开销。
- 针对calculateSimHash(): 利用Java 8并行流(parallelStream),将n-gram集合的遍历和向量更新 “分片并行” 执行,充分利用多核 CPU 资源。
- 针对murmurHash3(): 直接基于char[]计算哈希,跳过 “String → 字节数组” 的转换步骤。
3.性能分析图:

4.程序最大函数:
private static long calculateSimHash(Set<NGramHolder> ngramSet) {
if (ngramSet.isEmpty()) return 0;
int[] simHashVector = new int[SIM_HASH_BITS];
// 并行流遍历:自动拆分任务到多线程,无状态操作线程安全
ngramSet.parallelStream().forEach(ngram -> {
long ngramHash = murmurHash3(ngram.toString()); // 仅此时生成完整字符串
for (int i = 0; i < SIM_HASH_BITS; i++) {
long bitMask = 1L << i;
if ((ngramHash & bitMask) != 0) {
simHashVector[i]++;
} else {
simHashVector[i]--;
}
}
});
五、计算模块部分单元测试展示
1.对所给的5个文件进行查重:

2.对代码进行单元测试:
import org.junit.Test;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
public class PaperPlagiarismCheckerTest {
// ====================== 测试 preprocessText() 函数 ======================
@Test
public void testPreprocessText_SpecialChars() {
// 特殊字符过滤:保留允许的标点,过滤其他符号
String input = "Test@#$%^&*()_+{}[]|\\<>?/";
String expected = "test"; // 特殊符号全部被过滤
assertEquals(expected, PaperPlagiarismChecker.preprocessText(input));
}
@Test
public void testPreprocessText_EmptyInput() {
// 空输入处理
assertEquals("", PaperPlagiarismChecker.preprocessText(""));
assertEquals("", PaperPlagiarismChecker.preprocessText(null));
assertEquals("", PaperPlagiarismChecker.preprocessText(" \t\n")); // 空白字符
}
// ====================== 测试 extractNgramSet() 函数 ======================
@Test
public void testExtractNgramSet_2GramNormal() {
// 正常2-gram提取
String[] tokens = {"a", "b", "c", "d"};
Set<PaperPlagiarismChecker.NGramHolder> result = PaperPlagiarismChecker.extractNgramSet(tokens, 2);
assertEquals(3, result.size()); // 4个token提取3个2-gram
}
@Test
public void testExtractNgramSet_1Gram() {
// 1-gram提取(每个token单独作为gram)
String[] tokens = {"x", "y", "z"};
Set<PaperPlagiarismChecker.NGramHolder> result = PaperPlagiarismChecker.extractNgramSet(tokens, 1);
assertEquals(3, result.size()); // 3个token提取3个1-gram
}
@Test
public void testExtractNgramSet_BoundaryCases() {
// 边界情况1:token数量等于n
String[] tokens1 = {"a", "b"};
assertEquals(1, PaperPlagiarismChecker.extractNgramSet(tokens1, 2).size());
// 边界情况2:token数量小于n
String[] tokens2 = {"a"};
assertEquals(0, PaperPlagiarismChecker.extractNgramSet(tokens2, 2).size());
// 边界情况3:空token数组
assertEquals(0, PaperPlagiarismChecker.extractNgramSet(new String[0], 1).size());
}
@Test
public void testExtractNgramSet_DuplicateHandling() {
String[] tokens = {"a", "a", "a"};
Set<PaperPlagiarismChecker.NGramHolder> ngramSet = PaperPlagiarismChecker.extractNgramSet(tokens, 2);
assertEquals(1, ngramSet.size());
}
// ====================== 测试 murmurHash3() 函数 ======================
@Test
public void testMurmurHash3_Consistency() {
// 哈希一致性:相同输入必产相同输出
String text = "consistency test 哈希一致性";
long hash1 = PaperPlagiarismChecker.murmurHash3(text);
long hash2 = PaperPlagiarismChecker.murmurHash3(text);
assertEquals(hash1, hash2);
}
@Test
public void testMurmurHash3_Distribution() {
// 哈希分散性:相似输入应产生不同输出
String text1 = "similar text";
String text2 = "similar text!"; // 仅多一个标点
assertNotEquals(PaperPlagiarismChecker.murmurHash3(text1), PaperPlagiarismChecker.murmurHash3(text2));
}
@Test
public void testMurmurHash3_EdgeInputs() {
// 边缘输入哈希测试
long hashEmpty = PaperPlagiarismChecker.murmurHash3("");
long hashLong = PaperPlagiarismChecker.murmurHash3("a".repeat(1000)); // 长字符串
long hashChinese = PaperPlagiarismChecker.murmurHash3("中文哈希测试");
// 只需验证不抛出异常且哈希值有效
assertTrue(hashEmpty >= 0);
assertTrue(hashLong >= 0);
assertTrue(hashChinese >= 0);
}
// ====================== 测试 calculateSimHash() 函数 ======================
@Test
public void testCalculateSimHash_EmptySet() {
// 空n-gram集合应返回0
Set<PaperPlagiarismChecker.NGramHolder> emptySet = new HashSet<>();
assertEquals(0, PaperPlagiarismChecker.calculateSimHash(emptySet));
}
@Test
public void testCalculateSimHash_SingleNgram() {
// 单个n-gram的SimHash计算
String[] tokens = {"single", "gram"};
Set<PaperPlagiarismChecker.NGramHolder> ngramSet = new HashSet<>();
ngramSet.add(new PaperPlagiarismChecker.NGramHolder(tokens, 0, 2)); // 1个2-gram
long simHash = PaperPlagiarismChecker.calculateSimHash(ngramSet);
assertNotEquals(0, simHash); // 非空集合不应返回0
}
@Test
public void testCalculateSimHash_SimilarTexts() {
// 相似文本应产生相似的SimHash(海明距离小)
String[] tokens1 = {"this", "is", "a", "test"};
String[] tokens2 = {"this", "is", "a", "test", "case"}; // 仅多一个词
Set<PaperPlagiarismChecker.NGramHolder> set1 = PaperPlagiarismChecker.extractNgramSet(tokens1, 2);
Set<PaperPlagiarismChecker.NGramHolder> set2 = PaperPlagiarismChecker.extractNgramSet(tokens2, 2);
long hash1 = PaperPlagiarismChecker.calculateSimHash(set1);
long hash2 = PaperPlagiarismChecker.calculateSimHash(set2);
int distance = PaperPlagiarismChecker.calculateHammingDistance(hash1, hash2);
assertTrue("相似文本SimHash差异过大", distance < 10); // 预期差异较小
}
// ====================== 测试 calculateHammingDistance() 函数 ======================
@Test
public void testCalculateHammingDistance_IdenticalHashes() {
// 相同哈希的海明距离应为0
long hash = 0x12345678L;
assertEquals(0, PaperPlagiarismChecker.calculateHammingDistance(hash, hash));
}
@Test
public void testCalculateHammingDistance_KnownDifferences() {
// 已知差异位的海明距离计算
long hash1 = 0b0000L; // 二进制4位全0
long hash2 = 0b1111L; // 二进制4位全1
assertEquals(4, PaperPlagiarismChecker.calculateHammingDistance(hash1, hash2));
long hash3 = 0b1010L;
long hash4 = 0b1001L;
assertEquals(2, PaperPlagiarismChecker.calculateHammingDistance(hash3, hash4)); // 第2和第4位不同
}
@Test
public void testCalculateHammingDistance_EdgeValues() {
long hash1 = 0L;
long hash2 = -1L; // -1 的二进制是 64 个 1
int distance = PaperPlagiarismChecker.calculateHammingDistance(hash1, hash2);
assertEquals(64, distance);
}
// ====================== 测试 calculateSimHashSimilarity() 函数 ======================
@Test
public void testCalculateSimHashSimilarity_KnownValues() {
// 已知海明距离对应的相似度
assertEquals(1.0, PaperPlagiarismChecker.calculateSimHashSimilarity(0), 0.001); // 0位不同
assertEquals(0.75, PaperPlagiarismChecker.calculateSimHashSimilarity(16), 0.001); // 16位不同
assertEquals(0.5, PaperPlagiarismChecker.calculateSimHashSimilarity(32), 0.001); // 32位不同
assertEquals(0.0, PaperPlagiarismChecker.calculateSimHashSimilarity(64), 0.001); // 64位不同
}
@Test
public void testCalculateSimHashSimilarity_BoundaryChecks() {
// 边界值校验(海明距离不能超过64或为负)
assertEquals(0.0, PaperPlagiarismChecker.calculateSimHashSimilarity(65), 0.001); // 超过64位按64算
assertEquals(1.0, PaperPlagiarismChecker.calculateSimHashSimilarity(-1), 0.001); // 负数按0算
}
}
2.2测试的函数以及测试代码的构建思路:
2.2.1测试preprocessText()函数
- 构建思路: 构造包含@#$%^&*()等多种特殊符号的文本,检验文本预处理时特殊字符是否被全部过滤,预期处理后仅保留字母部分,无特殊符号残留。
2.2.2测试extractNgramSet()函数
- 构建思路: 使用包含4个常规token的数组,测试2-gram提取,预期能正确生成3个2-gram,验证n-gram提取数量逻辑。
2.2.3测试murmurHash3()函数
- 构建思路: 对相同字符串两次调用哈希函数,验证哈希一致性,确保相同输入始终得到相同哈希值。
2.2.4测试calculateSimHash()函数
- 构建思路: 传入空的n-gram集合,测试SimHash计算,预期在空输入时返回0,验证空集合处理逻辑。
2.2.5测试calculateHammingDistance()函数
- 构建思路: 使用完全相同的两个哈希值,测试海明距离计算,预期结果为0,验证相同哈希的距离计算。
2.2.6测试calculateSimHashSimilarity()函数
- 构建思路: 给定已知的海明距离(如 0、16、32、64),代入相似度计算公式,验证计算出的相似度与理论值一致。
2.3代码覆盖率

六、计算模块部分异常处理说明
- n-gram提取异常:
(1)设计目标:验证token数量不足、空数组等边界场景下n-gram提取的正确性。
(2)单元测试样例:
@Test
public void testExtractNgramSet_InsufficientTokens() {
String[] tokens = {"a"}; // token数量 < n=2
Set<NGramHolder> result = PaperPlagiarismChecker.extractNgramSet(tokens, 2);
assertEquals("token不足时应返回空集合", 0, result.size());
}
(3)错误使用场景:当token数组长度(如["a"])小于n值(如2)时,extractNgramSet返回非空集合(如包含1个n-gram)。
- 哈希计算异常:
(1)设计目标:验证哈希函数对边缘输入(如超长字符串、空文本)的稳定性。
(2)单元测试样例:
@Test
public void testMurmurHash3_LongString() {
String longText = "a".repeat(10000); // 超长字符串
long hash1 = PaperPlagiarismChecker.murmurHash3(longText);
long hash2 = PaperPlagiarismChecker.murmurHash3(longText);
assertEquals("超长字符串哈希不一致", hash1, hash2);
}
(3)错误使用场景:对超长字符串(如10000个重复字符)两次调用murmurHash3,返回不同哈希值。
- SimHash计算异常:
(1)设计目标:验证空集合、单元素集合等场景下SimHash的合理性。
(2)单元测试样例:
@Test
public void testCalculateSimHash_EmptySet() {
Set<NGramHolder> emptySet = new HashSet<>();
long simHash = PaperPlagiarismChecker.calculateSimHash(emptySet);
assertEquals("空集合应返回0", 0, simHash);
}
(3)错误使用场景:传入空的n-gram集合,calculateSimHash返回非0值(如随机哈希值)。
- 海明距离计算异常:
(1)设计目标:验证极端哈希值(如全0、全1)的距离计算准确性。
(2)单元测试样例:
@Test
public void testCalculateHammingDistance_AllBitsDifferent() {
long hash1 = 0L; // 全0
long hash2 = -1L; // 全1(64位)
int distance = PaperPlagiarismChecker.calculateHammingDistance(hash1, hash2);
assertEquals("全差异哈希距离应为64", 64, distance);
}
(3)错误使用场景:计算全0哈希(0L)与全1哈希(-1L)的距离,结果为63而非64。