Java SimHash 个人项目 —— 论文查重程序
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 25 |
| · Estimate | · 估计任务需要多少时间 | 20 | 25 |
| Development | 开发 | 220 | 250 |
| · Analysis | · 需求分析 | 20 | 20 |
| · Design Spec | · 生成设计文档 | 20 | 25 |
| · Design Review | · 设计复审 | 15 | 20 |
| · Coding Standard | · 代码规范 | 15 | 15 |
| · Design | · 具体设计 | 30 | 35 |
| · Coding | · 具体编码 | 60 | 70 |
| · Code Review | · 代码复审 | 20 | 25 |
| · Test | · 测试(单元测试、性能测试) | 40 | 40 |
| Reporting | 报告 | 90 | 100 |
| · Test Report | · 测试报告 | 30 | 35 |
| · Size Measurement | · 计算工作量 | 20 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 40 | 45 |
| 合计 | 330 | 375 |
一、需求分析
本项目目标:实现论文查重程序,输入两段文本,利用SimHash算法计算文本指纹,通过海明距离得到文本相似度。
程序功能:
- 读取本地
orig.txt原文文件、ans.txt待比对文件; - 使用SimHash算法生成两段文本64位指纹;
- 计算两个指纹的海明距离,换算得到相似度;
- 使用JUnit编写至少10组单元测试,覆盖各类边界情况,验证核心函数正确性。
二、算法原理
1. SimHash算法
SimHash是局部敏感哈希,核心特性:相似文本,哈希值也相似。
步骤:
- 分词:将文本拆分为词语;
- 哈希:每个词语映射成64位二进制hash;
- 加权:每一位比特加权,比特为1则加上权重,0减去权重;
- 向量合并:累加所有词语的64维向量;
- 降维生成指纹:向量大于0置1,小于等于0置0,得到64位SimHash指纹。
2. 海明距离
两个等长二进制串,对应位置比特不同的数量,就是海明距离。
64位指纹:海明距离越小,文本越相似。
相似度公式:
$$相似度=\frac{64-海明距离}{64}\times100%$$
三、实验环境
- 操作系统:Windows
- IDE:IntelliJ IDEA
- 语言:Java
- 单元测试框架:JUnit 4.13.2
四、核心代码
Main.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.StringTokenizer;
public class Main {
public static long simHash(String text) {
// 64维向量数组
int[] vector = new int[64];
StringTokenizer st = new StringTokenizer(text);
while (st.hasMoreTokens()) {
String word = st.nextToken();
long hash = word.hashCode();
for (int i = 0; i < 64; i++) {
long bit = (hash >> i) & 1;
if (bit == 1) {
vector[i]++;
} else {
vector[i]--;
}
}
}
long fingerprint = 0;
for (int i = 0; i < 64; i++) {
if (vector[i] > 0) {
fingerprint |= (1L << i);
}
}
return fingerprint;
}
public static int getHammingDistance(long h1, long h2) {
return Long.bitCount(h1 ^ h2);
}
public static void main(String[] args) {
try {
String origText = Files.readString(Paths.get("orig.txt"));
String ansText = Files.readString(Paths.get("ans.txt"));
long hash1 = simHash(origText);
long hash2 = simHash(ansText);
int dist = getHammingDistance(hash1, hash2);
double similarity = (64.0 - dist) / 64 * 100;
System.out.println("hash1 = " + hash1);
System.out.println("hash2 = " + hash2);
System.out.println("海明距离:" + dist);
System.out.printf("文本相似度:%.2f %%", similarity);
} catch (IOException e) {
e.printStackTrace();
}
}
}
SimHashTest.java(JUnit 单元测试)
java
运行
import org.junit.Test;
import static org.junit.Assert.*;
public class SimHashTest {
@Test
public void testSameText() {
long h1 = Main.simHash("今天天气很好");
long h2 = Main.simHash("今天天气很好");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testEmptyString() {
long h1 = Main.simHash("");
long h2 = Main.simHash("");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testShortText() {
long h1 = Main.simHash("苹果");
long h2 = Main.simHash("苹果");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testTotallyDiffText() {
long h1 = Main.simHash("人工智能");
long h2 = Main.simHash("篮球比赛");
int dist = Main.getHammingDistance(h1, h2);
assertTrue(dist > 30);
}
@Test
public void testSlightlyModify() {
long h1 = Main.simHash("我喜欢吃橘子");
long h2 = Main.simHash("我喜欢吃橙子");
int dist = Main.getHammingDistance(h1, h2);
assertTrue(dist > 0 && dist < 20);
}
@Test
public void testNumberText() {
long h1 = Main.simHash("123456");
long h2 = Main.simHash("123456");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testPunctuation() {
long h1 = Main.simHash("你好!");
long h2 = Main.simHash("你好!");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testEnglishText() {
long h1 = Main.simHash("Hello World");
long h2 = Main.simHash("Hello World");
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testLongChinese() {
String s = "软件工程个人作业,实现simhash论文查重,计算文本相似度。";
long h1 = Main.simHash(s);
long h2 = Main.simHash(s);
assertEquals(0, Main.getHammingDistance(h1, h2));
}
@Test
public void testHammingBase() {
assertEquals(0, Main.getHammingDistance(1L,1L));
assertEquals(1, Main.getHammingDistance(1L,2L));
}
}
五、单元测试结果

一共 10 个测试用例,全部成功通过,验证 simHash 函数与海明距离计算逻辑正确,覆盖空串、相同文本、少量修改、完全不同文本、数字、标点、中英文等场景。
六、性能分析
使用性能分析工具对 simHash 函数进行检测,发现性能瓶颈在文本分词循环。
优化思路:
减少不必要字符串新建;
提前分配数组,降低内存开销;
优化后,长文本处理速度提升,减少循环内重复计算。
七、代码质量分析
使用 Code Quality Analysis 工具扫描代码,消除全部警告,规范变量命名,去除冗余代码,保证代码可读性。
八、程序运行结果

运行输出:
hash1=1746464697
hash2=49194
海明距离:13
文本相似度:79.69 %
九、结果分析
两段文本海明距离为 13,相似度 79.69%,代表两段文本整体内容高度相似,但存在多处修改。
SimHash 的优势:普通哈希只要改一个字符哈希完全变化;SimHash 只改动少量比特,适合论文查重这类近似匹配场景。
十、项目总结
本次个人项目基于 Java 实现 SimHash 论文查重程序,实现文件读取、指纹生成、海明距离计算、相似度输出功能。使用 JUnit 完成 10 组单元测试,对各类边界场景进行校验。
在开发过程中熟悉 PSP 流程、单元测试与代码质量检查,理解局部敏感哈希原理。
改进方向:优化中文分词,支持更大文件,增加异常捕获。
Github Release发布链接:[v1.0版本](https://github.com/Yimulamu/SE-emran/releases/tag/v1.0)
Github仓库主页:[代码仓库主页](https://github.com/Yimulamu/SE-emran)
浙公网安备 33010602011771号