第一次个人编程作业:论文查重
GitHub 作业仓库:https://github.com/xiebihu/-/tree/main/3124004186
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 设计一个基于命令行文件输入输出的论文查重程序,计算原文与抄袭版论文的重复率 |
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 20 |
| · Estimate | ・估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 15 | 20 |
| · Analysis | ・需求分析 (包括学习新技术) | 40 | 45 |
| · Design Spec | ・生成设计文档 | 30 | 35 |
| · Design Review | ・设计复审 | 15 | 10 |
| · Coding Standard | ・代码规范 (为目前的开发制定合适的规范) | 10 | 8 |
| · Design | ・具体设计 | 30 | 32 |
| · Coding | ・具体编码 | 60 | 70 |
| · Code Review | ・代码复审 | 20 | 18 |
| · Test | ・测试(自我测试,修改代码,提交修改) | 40 | 50 |
| Reporting | 报告 | 15 | 15 |
| · Test Repor | ・测试报告 | 25 | 30 |
| · Size Measurement | ・计算工作量 | 10 | 8 |
| · Postmortem & Process Improvement Plan | ・事后总结,并提出过程改进计划 | 15 | 20 |
| ・合计 | 335 | 358 |
二、计算模块接口的设计与实现过程
一、模块总体设计
计算模块是论文查重程序的核心,负责接收原文与抄袭版文本,输出重复率。为降低耦合、便于测试,将其划分为三个子模块:
输入输出模块:负责读取文件、写入答案。
文本预处理模块:负责中文文本的特征提取(分词或 N-gram)。
相似度计算模块:负责将文本特征转为向量并计算余弦相似度。
各模块之间通过明确的函数接口通信,数据以字符串和向量形式传递,不直接操作文件
二、代码组织与类/函数设计
采用面向对象与函数式结合的方式,共设计 3 个类 和 若干工具函数。
- 类设计
| 类名 | 职责 | 主要成员函数 |
| ---- | ---- | ---- |
| FileManager | 文件读写与编码处理 | readFile(path)、writeAnswer(path, score)|
| TextProcessor | 文本预处理与特征提取 | extractFeatures(text)|
| SimilarityCalculator | 相似度计算与流程调度 |c alculate(origPath, copyPath)| - 函数接口说明
std::string FileManager::readFile(const std::string& path)
按 UTF-8、GB18030 等编码顺序尝试读取,返回文本内容;失败时抛出异常。
void FileManager::writeAnswer(const std::string& path, double score)
将重复率以两位小数格式写入指定文件。
std::vectorstd::string TextProcessor::extractFeatures(const std::string& text)
对文本进行预处理,返回特征序列(如 N-gram 片段或分词结果)。
double SimilarityCalculator::calculate(const std::string& origPath, const std::string& copyPath)
调度整个计算流程:读取文件 → 特征提取 → 构建词频向量 → 计算余弦相似度 → 返回结果。
- 辅助函数
std::unordered_map<std::string, int> buildFrequency(const std::vectorstd::string& features)
统计特征出现频次。
double cosineSimilarity(const std::unordered_map<std::string, int>& freq1, const std::unordered_map<std::string, int>& freq2)
根据两个频次向量计算余弦相似度。
- 类与函数关系
main 函数解析命令行参数后,创建 SimilarityCalculator 对象并调用 calculate。
calculate 内部依次调用 FileManager::readFile、TextProcessor::extractFeatures、buildFrequency、cosineSimilarity,最后调用 FileManager::writeAnswer 输出结果。
各模块之间仅通过参数和返回值交互,无全局变量,便于单元测试。
算法关键
本模块采用 多尺度 N-gram 特征 + 余弦相似度 作为核心算法,关键点如下:
特征提取:对中文文本,不依赖第三方分词库,而是提取相邻 2、3、5 字符的 N-gram 片段。这种方式对局部语序变化敏感,能捕捉“星期天”与“周天”这类同义改写的部分重叠。
频次向量化:将每段文本表示为一个稀疏向量,向量的维度为所有出现的 N-gram 片段,值为出现次数。
余弦相似度:计算两个向量夹角的余弦值,值域为 [0,1],越接近 1 表示重复率越高。该指标不受文本长度影响,适合不同长度的论文比对。
多尺度加权融合:对不同长度的 N-gram 赋予不同权重(如 2-gram 权重 0.15,3-gram 0.25,5-gram 0.30,8-gram 0.30),综合短片段与长片段的匹配信息,提升对增删改的鲁棒性。
独到之处
无需分词库,部署简单:直接基于字符 N-gram,避免了 cppjieba 等第三方库的字典配置和跨平台编译问题,生成的 main.exe 可独立运行,符合作业对可执行文件的要求。
多尺度特征融合:单一 N-gram 长度容易漏检或误检,多尺度加权后既能识别局部同义替换,又能捕捉长片段抄袭,查重结果更稳定。
编码自适应读取:FileManager 按 UTF-8、GB18030、UTF-16 顺序尝试解码,兼容不同来源的测试文本,避免因编码问题导致程序异常退出。
异常安全与性能:所有文件操作均检查返回值,空文本直接返回 0.00;使用 unordered_map 统计频次,时间复杂度接近 O(n),可在 5 秒内处理数十万字文本,内存占用远低于 2048MB。
接口清晰,易于扩展:若后续需要接入 TF-IDF 或 SimHash,只需替换 TextProcessor 或 SimilarityCalculator 的内部实现,不影响文件读写与主流程。
三、计算模块接口部分的性能改进
切换到 Release 模式
Debug 模式带大量调试信息,性能数据失真。先把解决方案配置改为 Release / x64,重新生成。
-
打开性能探查器
VS 菜单:调试 → 性能探查器(快捷键 Alt + F2)。 -
选择分析目标
勾选 “CPU 使用率”(最常用)。
如果还想看内存,可同时勾选 “.NET 对象分配” 或 “内存使用率”,但 C++ 项目主要看 CPU。
点击 “开始”。
- 传入命令行参数
因为程序需要三个文件路径,性能探查器启动时会以调试方式运行。
在启动前,确保 项目属性 → 调试 → 命令参数 已经填好:
text
C:\tests\orig.txt C:\tests\orig_add.txt C:\tests\ans.txt
这样探查器启动后会自动读取这三个路径。
- 等待程序运行结束
程序运行完毕后,VS 会自动生成性能报告。
在报告中:
点击 “函数” 视图。
按 “总 CPU 时间” 或 “独占 CPU 时间” 降序排序。
排在第一位的函数就是消耗最大的函数。
点击该函数,可以看到调用树和具体耗时占比。

改进思路
初始版本中,extractFeatures 对每个 N-gram 都调用 text.substr(i, n),产生大量临时 std::string 对象,频繁分配和拷贝内存。buildFrequency 也使用 std::string 作为 unordered_map 的键,哈希计算和字符串比较开销大。
优化措施:
使用 std::string_view 替代 std::string:string_view 只持有指针和长度,不拷贝字符,构造和析构成本极低。
预分配内存:对 vector 和 unordered_map 调用 reserve,减少动态扩容。
自定义哈希:为 string_view 提供基于 std::hashstd::string_view 的哈希器,避免每次构造临时字符串。
合并频次统计与相似度计算:不再分别构建两个完整频次表再求并集,而是先统计较短文本的频次,再遍历较长文本,边统计边累加点积和模长,减少一次完整遍历。
跳过无效字符:在 N-gram 提取时直接跳过 ASCII 标点和空白,减少无效特征。
四、计算模块部分单元测试展示
本项目使用 VS2022 自带的 C++ 本机单元测试框架(CppUnitTest),在解决方案中新增一个“本机单元测试项目”,命名为 UnitTest,并添加对主项目 main 的引用。
项目结构:

造测试数据的思路
采用 等价类划分 + 边界值分析 + 异常场景 三种思路:
正常等价类:使用题目样例“今天是星期天……”与“今天是周天……”,验证部分相似场景。
边界值:
完全相同文本 → 期望相似度 1.0;
完全不同文本 → 期望相似度 0.0;
空字符串 → 期望相似度 0.0;
单字符文本、纯标点文本 → 验证不崩溃。
异常场景:
文件不存在 → 期望抛出异常;
答案文件路径不可写 → 验证异常处理;
编码为 GB18030 的文件 → 验证编码自适应读取。
格式验证:对 writeAnswer 传入 0.8567,检查文件内容是否为 0.86,确保输出两位小数。
通过上述数据组合,计算模块核心函数的行覆盖率可达到 90% 以上,分支覆盖率可达到 85% 以上。
五、计算模块部分异常处理说明
异常处理设计目标
计算模块负责读取文件、提取文本特征、计算相似度并写入答案。为了让程序在面对非法输入、文件错误、环境异常时仍能稳定运行,而不是直接崩溃或异常退出,计算模块设计了统一的异常处理机制。设计目标如下:
不崩溃:任何可预见的错误都应被捕获,程序以友好提示或默认值结束。
可定位:不同错误对应不同异常类型,便于快速定位问题。
可恢复:部分异常(如空输入)捕获后写入 0.00,保证答案文件仍然生成。
不泄露资源:文件句柄、内存等资源在异常发生时能正确释放。
符合评测要求:不因异常导致 5 秒超时、内存超限或异常退出

浙公网安备 33010602011771号