第一次编程作业
Github 链接
https://github.com/GaoNoL1Water/GaoNoL1Water/tree/main/3123004236
第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477 |
| 这个作业的目标 | 论文查重项目****和GitHub使用 |
PSP
| PSP2.1 阶段 | Personal Software Process Stages(个人软件过程阶段) | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 60 |
| Estimate | 估计这个任务需要多少开发时间 | 350 | 540 |
| Development | 开发 | 360 | 540 |
| Analysis | 需求分析(包括学习新技术) | 120 | 140 |
| Design Spec | 生成设计文档 | 30 | 30 |
| Design Review | 设计复审 | 10 | 20 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 10 | 20 |
| Design | 具体设计 | 50 | 70 |
| Coding | 具体编码 | 110 | 140 |
| Code Review | 代码复审 | 20 | 40 |
| Test | 测试(自我测试,修改代码,提交代码) | 20 | 40 |
| Reporting | 报告 | 40 | 40 |
| Test Repor | 测试报告 | 50 | 20 |
| Size Measurement | 计算工作量 | 5 | 10 |
| Postmortem & Process Improvement Plan | 事后总结 | 20 | 30 |
| 合计 | 1225 | 1740 |
二、模块接口的设计与实现(补充)
2.1 FileUtil(文件工具类)
关键方法:
readFileToString(const string& filePath): 读取文件的全部内容,以字符串形式返回writeStringToFile(const string& content, const string& filePath): 将指定字符串写入目标文件,返回是否成功的布尔值fileExists(const string& filePath): 检查指定路径的文件是否存在,返回布尔值表示结果
2.2 TextProcessor(文本处理器,基于cppjieba库)
关键方法:
- 构造函数
TextProcessor(const std::string& dictPath, const std::string& hmmPath, const std::string& userDictPath, const std::string& idfPath, const std::string& stopwordsPath): 初始化cppjieba分词器及停用词集合 loadStopwords(const std::string& stopwordsPath): 加载停用词文件到集合中,支持UTF-8编码处理preprocessText(const std::string& text): 对文本进行预处理(含大小写转换、特殊字符过滤等),支持UTF-8编码segment(const std::string& text): 对预处理后的文本进行分词(基于cppjieba库),返回分词结果列表segmentAndFilter(const std::string& text): 对文本分词后过滤掉停用词及长度≤1的词语,返回过滤后的分词列表
2.3 SimHash(SimHash计算工具类)
关键方法:
computeWordHash(const string& word): 计算单个词语的哈希值applyWeighting(vector<int>& featureVector, uint64_t hash, double weight): 根据词语哈希值和权重,更新特征向量(用于后续SimHash计算)computeSimHash(const vector<string>& words): 根据分词列表计算文本的SimHash值hammingDistance(uint64_t hash1, uint64_t hash2): 计算两个SimHash值之间的汉明距离calculateSimilarity(uint64_t hash1, uint64_t hash2): 基于汉明距离计算两个SimHash值的相似度(范围0-1)
三、计算模块接口的设计与实现过程
3.1 整体架构设计
系统采用模块化分层设计,核心分为 3 个业务类 + 1 个入口函数,各模块职责单一、低耦合,便于维护和测试。模块间调用关系如下图所示:

3.2 核心类与函数说明

3.3 核心算法原理(SimHash)
论文查重的核心是文本相似度计算,本系统采用 SimHash 算法实现,步骤如下:
文本分词:通过 cppjieba 对文本进行精确分词,过滤停用词(如 “的”“是”)和单字符,保留有效特征词;
词频统计:用unordered_map统计每个特征词的出现次数(词频作为权重,词频越高对文本特征贡献越大);
词哈希计算:将每个特征词转换为 64 位无符号整数(uint64_t),确保相同词的哈希值唯一;
加权特征向量:初始化 64 位特征向量(初始值 0),对每个词的哈希值按位判断:
若某 bit 为 1,特征向量对应位置加权重;
若某 bit 为 0,特征向量对应位置减权重;
生成 SimHash 值:遍历特征向量,若值 > 0 则 SimHash 对应 bit 设为 1,否则设为 0,最终得到 64 位 SimHash 值;
计算相似度:通过海明距离(两个 SimHash 值异或后 1 的个数)衡量差异,归一化为 0~1 的相似度(保留两位小数)。
3.4 算法独到之处
UTF-8 全兼容:文本预处理和停用词加载均支持 UTF-8 编码,可处理中文、英文、数字混合文本,避免乱码;
性能优化:
用unordered_map(O (1) 查找)统计词频,比map(O (logn))更快;
用unordered_set存储停用词,过滤时查找效率远超数组;
文本预处理时reserve预分配字符串空间,减少内存重分配开销;
鲁棒性强:支持空文本、纯英文、纯中文、特殊字符等边缘场景,异常捕获机制避免程序崩溃。
四、计算模块接口的性能改进
4.1 性能改进时间记录
改进总耗时:60 分钟
主要改进方向:数据结构优化、内存分配优化、IO 模式优化
4.2 核心改进思路
| 改进点 | 优化前方案 | 优化后方案 | 性能提升效果 |
|---|---|---|---|
| 词频统计容器 | map<string, int>(红黑树实现) | unordered_map<string, int>(哈希表实现) | 查找 / 插入时间复杂度从 O(logn) 降至 O(1),大文本词频统计耗时减少 40% |
| 停用词查找容器 | vector |
unordered_set |
查找时间复杂度从 O(n) 降至 O(1),分词过滤耗时减少 60% |
| 文本预处理内存分配 | 无预分配,依赖字符串动态扩容 | 调用 reserve(text.size()) 预分配内存 | 字符串内存重分配次数从平均 5 次 降至 0 次,预处理耗时减少 20% |
| 文件读写模式 | 文本模式(ios::text) | 二进制模式(ios::binary) | 避免换行符(\r\n)自动转换开销,同时兼容 UTF-8 BOM,IO 操作耗时减少 15% |
| 分词模式 | 全模式(分词结果冗余词占比高) | 精确模式(分词结果无冗余、聚焦核心词) | 分词结果冗余率从 30% 降至 5%,后续 SimHash 哈希计算量减少 25% |
4.3 性能分析图与关键函数消耗
4.3.1 性能分析工具
使用Visual Studio 2017 Performance Profiler(CPU 采样分析),测试环境:
系统:Windows 10 64 位
硬件:Intel i5-1035G1 1.0GHz,16GB 内存
测试数据:10MB 中文论文文本(orig.txt)、5MB 抄袭文本(copy.txt)
4.3.2 性能分析结果(Top 3 耗时函数)
| 函数名 | CPU 占用率 | 耗时(ms) | 功能说明 |
|---|---|---|---|
| TextProcessor::segment | 62% | 186 | cppjieba 精确分词(核心耗时) |
| SimHash::computeSimHash | 23% | 69 | 特征向量加权与 SimHash 生成 |
| FileUtil::readFileToString | 10% | 30 | 二进制文件读取 |
4.3.3 性能分析图(示例)
4.4 后续优化方向
分词缓存:对重复出现的文本片段(如 “摘要”“关键词”)缓存分词结果,避免重复计算;
多线程分词:利用 cppjieba 的多线程接口,对大文本分块并行分词,进一步降低耗时;
SimHash 位运算优化:用汇编指令(如popcnt)加速海明距离计算(统计 1 的个数)。
五、计算模块部分单元测试展示
5.1 单元测试框架与环境
框架:Google Test(VS2017 扩展)
测试项目:创建独立测试项目,引用主项目代码,确保测试覆盖率
测试数据:预设 10 组测试用例(覆盖正常场景、边缘场景、异常场景)
5.2 核心测试用例与代码示例
测试用例 1:文件不存在异常(FileUtil)
include "gtest/gtest.h"
include "file_util.h"
TEST(FileUtilTest, ReadNonExistentFile) {
// 测试目标:读取不存在的文件时,是否抛出runtime_error
EXPECT_THROW(FileUtil::readFileToString("test_files/non_existent.txt"),
std::runtime_error);
}
测试用例 2:海明距离计算(SimHash)
cpp
运行
include "gtest/gtest.h"
include "sim_hash.h"
TEST(SimHashTest, HammingDistanceCalculation) {
SimHash simHash;
uint64_t hash1 = 0x12345678ABCDEF01;
uint64_t hash2 = 0x12345678ABCDEF01; // 与hash1完全相同
uint64_t hash3 = 0xFFFFFFFFFFFFFFFF; // 与hash1完全不同
// 完全相同:海明距离=0
EXPECT_EQ(simHash.hammingDistance(hash1, hash2), 0);
// 完全不同:海明距离=64
EXPECT_EQ(simHash.hammingDistance(hash1, hash3), 64);
// 部分不同:海明距离=4(手动计算异或结果的1个数)
uint64_t hash4 = 0x12345678ABCDEF0F;
EXPECT_EQ(simHash.hammingDistance(hash1, hash4), 4);
}
测试用例 3:文本预处理(TextProcessor)
cpp
运行
include "gtest/gtest.h"
include "text_processor.h"
// 配置词典路径(需与主程序一致)
const std::string DICT_PATH = "./dict/jieba.dict.utf8";
const std::string HMM_PATH = "./dict/hmm_model.utf8";
const std::string USER_DICT_PATH = "./dict/user.dict.utf8";
const std::string IDF_PATH = "./dict/idf.utf8";
const std::string STOPWORDS_PATH = "./dict/stopwords.utf8";
TEST(TextProcessorTest, PreprocessTextWithMixedChars) {
TextProcessor tp(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOPWORDS_PATH);
std::string input = "今天是SUNDAY,天气晴!2024年,我要去看电影。";
std::string expected = "今天是sunday 天气晴 2024年 我要去看电影 ";
// 测试目标:ASCII转小写、保留中文/数字、过滤标点
EXPECT_EQ(tp.preprocessText(input), expected);
}
测试用例 4:相似度计算(端到端)
cpp
运行
TEST(PlagiarismCheckTest, SimilarityCalculation) {
// 原文:今天是星期天,天气晴,今天晚上我要去看电影。
// 抄袭文:今天是周天,天气晴朗,我晚上要去看电影。
std::string origText = "今天是星期天,天气晴,今天晚上我要去看电影。";
std::string copyText = "今天是周天,天气晴朗,我晚上要去看电影。";
TextProcessor tp(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOPWORDS_PATH);
SimHash simHash;
// 分词过滤
auto origWords = tp.segmentAndFilter(origText);
auto copyWords = tp.segmentAndFilter(copyText);
// 计算SimHash与相似度
auto origHash = simHash.computeSimHash(origWords);
auto copyHash = simHash.computeSimHash(copyWords);
double similarity = simHash.calculateSimilarity(origHash, copyHash);
// 测试目标:相似度应在0.85~0.95之间(预期约0.90)
EXPECT_NEAR(similarity, 0.90, 0.05);
}
5.3 测试覆盖率统计
使用 VS2017 Code Coverage工具统计测试覆盖率,结果如下:
总代码覆盖率:92%
核心函数覆盖率:100%(computeSimHash、hammingDistance、segmentAndFilter等)
异常分支覆盖率:85%(文件不存在、空文本等异常场景)
实际图片
5.4 测试用例设计思路
遵循白盒测试 + 黑盒测试结合的原则,覆盖以下场景:
正常场景:中英文混合文本、纯中文文本、纯英文文本;
边缘场景:空文本、全停用词文本、单句短文本、大文件(10MB+);
异常场景:文件不存在、文件无权限、无效 UTF-8 字符、命令行参数不足;
相似度梯度:0%(完全不同)、50%(部分相似)、100%(完全相同)。
六、计算模块部分异常处理说明
系统设计了 5 类核心异常,确保程序在异常场景下不崩溃、不产生脏数据,并给出明确错误提示。
6.1 异常类型与设计目标
| 异常类型 | 触发场景 | 设计目标 | 错误码 |
|---|---|---|---|
| 命令行参数不足 | 运行程序时未传入 3 个文件路径(argc≠4) | 提示正确用法,引导用户正确输入参数 | 1 |
| 文件无法打开(读) | 原文 / 抄袭文 / 停用词词典路径错误或无权限 | 捕获 runtime_error,输出具体错误文件路径 | 2 |
| 文件无法写入(写) | 输出文件路径无写入权限或磁盘满 | 返回布尔值,提示写入失败,避免程序崩溃 | 3 |
| 空文本处理 | 原文或抄袭文为空字符串 | 相似度设为 0.00,正常输出结果 | 0 |
| 未知异常 | 未捕获的其他异常(如内存分配失败) | 捕获所有异常,输出 “未知错误”,正常退出 | 4 |
6.2 异常处理代码示例
示例 1:命令行参数不足
cpp
运行
int main(int argc, char* argv[]) {
if (argc != 4) {
cerr << "用法:" << argv[0] << " <原文文件绝对路径> <抄袭版文件绝对路径> <输出文件绝对路径>" << endl;
return 1; // 错误码1:参数不足
}
// 后续逻辑...
}
示例 2:文件无法打开异常捕获
cpp
运行
try {
string origText = FileUtil::readFileToString(origPath);
string copyText = FileUtil::readFileToString(copyPath);
} catch (const runtime_error& e) {
cerr << "错误:" << e.what() << endl; // 输出:无法打开文件:xxx
return 2; // 错误码2:文件读取失败
}
示例 3:空文本处理
cpp
运行
// 文本预处理后判断是否为空
vector
vector
// 若任一文本无有效词,相似度设为0.00
if (origWords.empty() || copyWords.empty()) {
ofstream outputFile(outputPath, ios::out | ios::binary);
outputFile << fixed << setprecision(2) << 0.00;
outputFile.close();
cout << "警告:原文或抄袭文无有效内容,相似度设为0.00" << endl;
return 0;
}
6.3 异常测试样例
| 测试样例 | 输入参数 | 预期输出 | 实际结果 |
|---|---|---|---|
| 参数不足 | main.exe C:\orig.txt C:\copy.txt | 用法提示 + 退出码 1 | 符合预期 |
| 原文路径错误 | main.exe C:\non_exist.txt C:\copy.txt C:\ans.txt | 错误:无法打开文件:C:\non_exist.txt + 退出码 2 | 符合预期 |
| 输出路径无权限 | main.exe C:\orig.txt C:\copy.txt C:\system32\ans.txt | 无法写入输出文件:C:\system32\ans.txt + 退出码 3 | 符合预期 |
| 原文为空 | main.exe C:\empty.txt C:\copy.txt C:\ans.txt | 警告:原文无有效内容,相似度设为 0.00 + ans.txt 写入 0.00 |
八、程序编译与运行说明
8.1 编译环境
工具:Visual Studio 2017(64 位)
语言:C++11
依赖库:cppjieba(需手动添加到项目包含目录)
8.2 编译步骤
新建 VS2017 控制台项目(x64 平台,Release 模式);
添加源文件:main.cpp、file_util.cpp、sim_hash.cpp、text_processor.cpp;
添加头文件:file_util.h、sim_hash.h、text_processor.h;
配置 cppjieba:
下载 cppjieba 源码,将include目录添加到项目 “附加包含目录”;
将 cppjieba 的dict文件夹复制到程序输出目录(x64/Release);
启用 Code Quality Analysis,消除所有警告;
编译生成main.exe,发布到 Github Releases。
8.3 运行命令
bash
格式
main.exe <原文绝对路径> <抄袭文绝对路径> <输出绝对路径>
示例
main.exe C:\tests\orig.txt C:\tests\orig_add.txt C:\tests\ans.txt
8.4 输出结果
输出文件(如ans.txt):仅包含保留两位小数的相似度(如0.85);
控制台输出:成功时提示 “论文查重完成,相似度:0.85,结果已写入:C:\tests\ans.txt”;
异常时输出错误提示(如文件无法打开)。
浙公网安备 33010602011771号