第一次个人编程作业
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 完成个人项目:论文查重 |
| 作业GitHub网址 | https://github.com/ww7c/www7c |
一、psp表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 20 |
| Estimate | · 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 420 | 480 |
| Analysis | · 需求分析 (包括学习新技术) | 60 | 90 |
| Design Spec | · 生成设计文档 | 10 | 30 |
| Design Review | · 设计复审 | 30 | 50 |
| Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 20 |
| Design | · 具体设计 | 60 | 40 |
| Coding | · 具体编码 | 120 | 180 |
| Code Review | · 代码复审 | 60 | 30 |
| Test | · 测试(自我测试,修改代码,提交修改) | 50 | 40 |
| Reporting | 报告 | 90 | 120 |
| Test Report | · 测试报告 | 30 | 50 |
| Size Measurement | · 计算工作量 | 30 | 30 |
| Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 40 |
| ·合计 | 530 | 620 |
二、计算模块接口的设计与实现过程
本程序的核心计算模块主要负责文本预处理、3-Gram 特征提取以及文本相似度计算。为了提高程序的模块化程度,将不同功能分别封装在不同的类中,使文本处理和相似度计算相互独立,便于后续测试和维护。
代码共有三个核心类
| 类 | 对应模块 | 主要功能 |
|---|---|---|
TextPreprocessor |
文本预处理模块 | 去空格、去标点、UTF-8 文本预处理 |
TextSimilarity |
3-Gram + 相似度计算模块 | 生成 3-Gram、计算 Jaccard 相似度 |
FileManager |
文件读写模块 | 读取文本文件、输出查重结果 |
文本预处理模块:
接口
class TextPreprocessor { public: static std::string process(const std::string& text); };
功能:文本预处理模块负责对读取到的文本进行规范化处理(去除空白字符,保留有效的中文,英文及数字字符。)返回处理后的文本。
3-Gram 哈希表模块
接口
class TextSimilarity { public: static std::set<std::string> generateNGram( const std::string& text, int n = 3 ); };
功能、
3-Gram 模块负责将预处理后的文本按照连续的三个UTF-8 字符划分为 3-Gram 特征。
相似度计算模块
接口
class TextSimilarity { public: static double calculateJaccard( const std::set<std::string>& setA, const std::set<std::string>& setB ); };
功能
相似度计算模块负责根据两篇文本生成的 3-Gram 特征集合计算文本相似度。
文件读写模块
接口
class FileManager { public: static std::string readFile(const std::string& filePath);
static bool writeResult(const std::string& filePath, double result); };
功能
文件读写模块负责程序与文本文件之间的数据交互。
流程图:

算法关键
本程序算法的核心是UTF-8 字符级 N-Gram + 集合 + Jaccard 相似度。
首先,将文本按照 UTF-8 字符进行切分,然后通过滑动窗口生成连续的 3-Gram 特征,每三个连续字符组成一个特征。随后将这些特征存入集合中,最后,通过计算两个文本 N-Gram 集合的交集和并集,使用 Jaccard 公式得到两篇文本的相似程度。
本设计的独到之处
- 采用模块化设计
将文本预处理和相似度计算分离到不同类中,代码可读性更高。 - 采用 UTF-8 字符级 N-Gram
不直接按照 std::string 的字节进行划分,而是先识别 UTF-8 字符,避免中文字符被错误拆分。 - 使用 std::set 保存 N-Gram
去除重复特征,同时方便后续进行集合交集、并集计算。 - 采用 Jaccard 相似度进行计算
算法原理简单,可以直观反映两篇文本的特征重合程度。
三、计算模块接口部分的性能改进
性能分析图由VS 2022的性能分析工具自动生成:

cpu火焰图

| 排名 | 函数 / 模块名称 | CPU 总计 |
|---|---|---|
| 1 | TextSimilarity::generateNGram | 25.00% |
| 2 | TextSimilarity::calculateJaccard | 22.2% |
| 3 | FileManager::readFile | 8.33% |
优化方案:
根据 VS 2022 性能分析结果,程序运行过程中消耗 CPU 时间最多的函数为 TextSimilarity::generateNGram(),其 CPU 时间占比约为 33.33%。分析其代码后发现,主要开销来自 UTF-8 字符处理、3-Gram 字符串构造以及 std::set 插入。针对上述问题,对 vector 和 string 进行预分配,,可降低 3-Gram 生成过程中的开销。
四、计算模块部分单元测试展示
部分测试单元代码


单元测试得到的测试覆盖率截图

五、计算模块部分异常处理说明
对于非法 N-Gram 长度、空文本以及文本长度不足等情况,程序提前终止计算并返回空集合;对于 Jaccard 相似度计算中的空集合情况,程序提前判断,避免出现除零错误。
异常一:非法N-Gram 长度
如果传入:generateNGram(text, 0);generateNGram(text, -1);则 N-Gram 长度没有实际意义。
设计目标:该异常处理的目标是避免程序对不足以生成 N-Gram 的文本进行滑动窗口操作。
代码:
if (text.length() < static_cast<size_t>(n)) { return nGramSet; }
当文本长度不足时,函数直接返回空集合。
单元测试用例
std::string text = "abcdef";
std::set<std::string> result =TextSimilarity::generateNGram(text, 0);
预期结果:N-Gram数量:0
错误对应场景:
当用户或者其他模块传入 n=0 或负数时,该测试用于验证计算模块能否正确识别非法 N-Gram 长度,并安全返回空集合。
异常二:空文本异常
如果经过文本预处理之后,文本变成空字符串,例如原始文本只有空格、换行或者其他被过滤掉的内容:原文本: 预处理后:""
此时没有任何有效字符可以用于生成 N-Gram。
设计目标: 程序不应该因为空文本而崩溃,也不应该生成 N-Gram。因此可以在 generateNGram() 中直接判断:
if (text.empty()) { return nGramSet; }
单元测试样例:
测试输入:
std::string text = "";
std::set<std::string> result = TextSimilarity::generateNGram(text, 3);
预期结果:
N-Gram数量:0
错误对应场景:
当用户输入空文本,或者文本经过预处理后没有剩余有效内容时,该测试用于验证计算模块是否能够正常处理空输入。
异常三: 两篇文本均为空时的相似度计算
如果两篇文本都为空,那么:
A = {}B = {}
此时:
|A ∩ B| = 0
|A ∪ B| = 0
会出现:
0 / 0
这是一个没有定义的数学运算。
设计目标:为了避免出现除零错误,需要在计算 Jaccard 相似度之前判断并集是否为空。
单元测试样例
std::set<std::string> set1; std::set<std::string> set2;
double result = TextSimilarity::calculateJaccard(set1, set2);
预期结果相似度:0
错误对应场景:
当两篇文本经过预处理和 N-Gram 提取后都没有产生有效特征时,如果直接按照 Jaccard 公式计算,会产生 0/0。

浙公网安备 33010602011771号