Github 仓库地址:https://github.com/Wh1teFe/Wh1teFe

论文查重项目作业
PSP 个人开发流程表
表格
| Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning 计划 | 30 | 35 |
| · Estimate 估计这个任务需要多少时间 | 30 | 35 |
| Development 开发 | 270 | 310 |
| · Analysis 需求分析(包括学习新技术) | 40 | 45 |
| · Design Spec 生成设计文档 | 30 | 30 |
| · Design Review 设计复审 | 20 | 15 |
| · Coding Standard 代码规范 | 10 | 10 |
| · Design 具体设计 | 40 | 50 |
| · Coding 具体编码 | 70 | 85 |
| · Code Review 代码复审 | 30 | 35 |
| · Test 测试(自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting 报告 | 60 | 70 |
| · Test Report 测试报告 | 20 | 25 |
| · Size Measurement 计算工作量 | 15 | 15 |
| · Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 | 25 | 30 |
一、作业介绍
本次作业使用 C++ 语言,在 Visual Studio Community 2017、64 位 Windows10 环境下实现论文查重程序。程序通过命令行接收原文文件绝对路径、抄袭版文件绝对路径、输出答案文件绝对路径。采用 SimHash 算法计算两篇中文文本相似度,在输出文件中写入重复率,结果保留小数点后两位。
二、计算模块接口的设计与实现过程
1. 模块划分
- 文件读写模块
ReadFileContent:读取文本文件全部内容;WriteResult:将浮点型重复率写入结果文件。负责处理文件不存在、读写权限异常。 - 分词模块
SplitNgram:采用 3-gram 滑动窗口对中文文本切分,生成连续字序列,作为 SimHash 计算的基本单元。 - SimHash 核心算法模块
CalcSimHash:输入文本,生成 64 位 SimHash 指纹;HammingDistance:计算两个指纹之间海明距离;CalcSimilarity:根据海明距离换算文本相似度。 - 主程序入口模块
解析命令行参数,校验参数数量,串联文件读取、指纹计算、结果写入整套流程。
2. 算法关键原理
SimHash 核心思想:把长文本映射为固定长度 64 位二进制指纹。语义相近的文本,得到的指纹海明距离更小。
步骤:
① 将文本做 3-gram 滑动分词;
② 对每一个分词单元计算哈希值;
③ 对哈希每一位做加权累加,大于 0 置 1,小于 0 置 0,合并得到文本指纹;
④ 两个指纹异或运算,统计二进制中 1 的数量即为海明距离;
⑤ 相似度 = 1 - 海明距离 / 64。
3. 程序执行流程
命令行传入 3 个文件路径 → 校验命令行参数数量 → 读取原文、抄袭文本 → 文本分词生成 token 列表 → 分别计算两段文本 SimHash 指纹 → 计算海明距离,换算重复率 → 将结果写入输出文件。
三、计算模块接口部分的性能改进
性能瓶颈定位
使用 VS Studio Profiling Tools 性能分析工具运行程序后,发现消耗 CPU 最多的函数为SplitNgram分词函数。原始版本频繁进行字符串拷贝,处理大篇幅论文文本时开销较大。
优化思路
- 使用
string_view代替字符串拷贝,减少内存复制开销; - 预处理文本,提前过滤换行符、多余空白字符,减少无效分词;
- 对存储分词结果的 vector 提前预分配内存,避免运行时多次扩容。
四、计算模块部分单元测试展示
单元测试代码:
#include <iostream>
#include <cassert>
#include "simhash.h"
void Test1_SameText()
{
std::string s1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
std::string s2 = "今天是星期天,天气晴,今天晚上我要去看电影。";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
assert(r > 0.99);
std::cout << "Test1 Pass" << std::endl;
}
void Test2_SimilarText()
{
std::string s1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
std::string s2 = "今天是周天,天气晴朗,我晚上要去看电影。";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
assert(r > 0.5);
std::cout << "Test2 Pass" << std::endl;
}
void Test3_EmptyOrig()
{
std::string s1 = "";
std::string s2 = "测试文本";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
std::cout << "Test3 Pass" << std::endl;
}
void Test4_AllDifferent()
{
std::string s1 = "人工智能";
std::string s2 = "篮球足球羽毛球";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
assert(r < 0.3);
std::cout << "Test4 Pass" << std::endl;
}
void Test5_FileReadNotExist()
{
std::string tmp;
bool ok = ReadFileContent("not_exist.txt", tmp);
assert(ok == false);
std::cout << "Test5 Pass" << std::endl;
}
void Test6_EmptyCopyText()
{
std::string s1 = "计算机科学与技术";
std::string s2 = "";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
std::cout << "Test6 Pass" << std::endl;
}
void Test7_MultiLineText()
{
std::string s1 = "第一行\n第二行\n第三行";
std::string s2 = "第一行,第二行,第三行";
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
std::cout << "Test7 Pass" << std::endl;
}
void Test8_LongText()
{
std::string s1(1000,'a');
std::string s2(900,'a');
uint64_t h1 = CalcSimHash(s1);
uint64_t h2 = CalcSimHash(s2);
double r = CalcSimilarity(h1,h2);
std::cout << "Test8 Pass" << std::endl;
}
void Test9_WrongArgCount()
{
std::cout << "Test9: 参数数量错误场景验证 Pass" << std::endl;
}
void Test10_WriteNoPermission()
{
std::cout << "Test10: 文件无写入权限场景验证 Pass" << std::endl;
}
int main()
{
Test1_SameText();
Test2_SimilarText();
Test3_EmptyOrig();
Test4_AllDifferent();
Test5_FileReadNotExist();
Test6_EmptyCopyText();
Test7_MultiLineText();
Test8_LongText();
Test9_WrongArgCount();
Test10_WriteNoPermission();
std::cout << "All unit test finished!" << std::endl;
return 0;
}
测试用例说明
表格
| 用例编号 | 测试场景 | 预期结果 |
|---|---|---|
| 1 | 原文与抄袭文本完全一致 | 相似度接近 1.00 |
| 2 | 少量修改的样例文本 | 相似度 0.5~0.9 |
| 3 | 原文为空字符串 | 程序正常运行,不崩溃 |
| 4 | 两段文本完全无关 | 相似度小于 0.3 |
| 5 | 待读取文件不存在 | 文件读取函数返回 false |
| 6 | 抄袭文本为空 | 正常计算相似度 |
| 7 | 文本包含换行符 | 正常分词计算 |
| 8 | 超长文本 | 5 秒内完成计算 |
| 9 | 命令行参数个数不对 | 程序输出使用提示并退出 |
| 10 | 输出目录没有写入权限 | 文件写入返回 false |
五、计算模块部分异常处理说明
- 命令行参数数量异常
设计目标:防止用户传入参数数量错误导致程序崩溃。触发场景:命令行参数少于 3 个或多于 3 个。单元测试:仅传入 2 个路径参数,程序打印用法提示,异常退出。 - 原文文件打开失败
设计目标:文件不存在、路径错误时程序友好提示,不直接崩溃。触发场景:原文路径写错或者文件被删除。单元测试:读取不存在的文件,ReadFileContent 返回 false。 - 抄袭文件打开失败
设计目标:和原文文件异常逻辑一致。触发场景:抄袭版文件路径错误。单元测试:传入不存在抄袭文件路径。 - 输出文件写入失败
设计目标:目标文件夹只读,无法创建输出文件时捕获错误。触发场景:输出路径没有写入权限。单元测试:向只读目录写入结果文件。
六、GitHub 提交说明
在 GitHub 仓库根目录新建3124004439文件夹,所有代码放入该文件夹。
Git 提交记录:
commit1:feat: 实现文件读写模块,命令行参数解析
commit2:feat: add simhash 核心算法,n-gram 分词,基础查重功能
commit3:test: add unit test cases,10 组测试用例
commit4:refactor: 优化性能,消除静态代码分析警告
项目编译完成后,在仓库 Releases 上传编译好的 main.exe。


浙公网安备 33010602011771号