第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 完成第一次个人编程作业,内容是论文查重 |
作业 GitHub 链接:https://github.com/Dengwenwei0821/3124004505
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 10 | 30 |
| Development | 开发 | 360 | 350 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 40 |
| · Design Spec | · 生成设计文档 | 20 | 20 |
| · Design Review | · 设计复审 | 20 | 10 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 10 | 10 |
| · Design | · 具体设计 | 30 | 30 |
| · Coding | · 具体编码 | 190 | 180 |
| · Code Review | · 代码复审 | 30 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting | 报告 | 60 | 70 |
| · Test Report | · 测试报告 | 20 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 20 | 30 |
| 合计 | 430 | 450 |
二、计算模块接口的设计与实现过程
1. 整体架构设计
本项目采用模块化分层设计,完全基于 C++ 标准库与 Windows API 实现,无第三方依赖,各模块功能解耦,通过标准化接口交互,便于单元测试与功能扩展。整体分为 4 个核心功能模块与 1 套异常体系:
| 模块 | 核心职责 | 对应文件 |
|---|---|---|
| 文件操作模块 | 文本文件读取、查重结果写入 | FileUtils.h / FileUtils.cpp |
| 文本处理模块 | 编码转换、非中文字符过滤、特征提取 | TextUtils.h / TextUtils.cpp |
| 相似度计算模块 | 基于词频向量计算余弦相似度 | SimilarityCalculator.h / SimilarityCalculator.cpp |
| 主程序入口 | 命令行参数解析、流程调度、异常捕获 | main.cpp |
| 异常体系 | 定义 4 类业务异常,统一错误处理 | common.h |
模块调用流程:
main 接收命令行参数 → FileUtils::readFile 读取两篇文本 → TextUtils::gbkToWstring 编码转换 → TextUtils::filterNonChinese 清洗文本 → TextUtils::generateBigram 提取二元特征 → TextUtils::calcWordFreq 统计词频 → SimilarityCalculator::calcCosineSimilarity 计算重复率 → FileUtils::writeResult 写入结果文件
2. 核心算法说明
(1)特征提取:字符二元语法(Character Bigram)
针对中文文本无天然分词边界的特点,本项目采用字符级二元语法作为文本特征,无需额外分词词典,实现简单且抗干扰能力强。
- 原理:将清洗后的纯中文文本,按连续两个汉字为单位切分为特征项,例如 “今天天气” 切分为「今天、天是、天气」。
- 优势:既保留了文本的局部语义关联,又避免了中文分词的词典依赖与编码复杂度,对同义词替换、语序微调的抄袭场景有较好的识别能力。
(2)相似度计算:余弦相似度算法
基于词袋模型计算两篇文本特征向量的余弦相似度,结果即为重复率。
- 步骤:
- 分别统计两篇文本所有 Bigram 特征的出现频次,生成词频映射
- 以两个向量的点积除以向量模长的乘积,得到余弦值
- 余弦值范围为 [0,1],值越大表示文本重复度越高
- 公式:
![image]()
其中Ai、Bi分别为两个文本中第i个特征的词频。
3. 设计独到之处
- 零第三方依赖:全部功能基于 C++ 标准库实现,无需配置分词库、数值计算库,直接编译即可运行,适配课程作业提交要求。
- 分层异常设计:在参数校验、文件 IO、文本处理、相似度计算各层均设置异常抛出点,错误类型明确,便于定位问题。
- 性能与准确率平衡:Bigram + 余弦相似度的组合,时间复杂度为 O (n+m),远优于 LCS 等 O (nm) 算法,同时对中文文本的查重准确率满足课程作业要求。
- 编码适配:针对 Windows 平台默认 GBK 编码的特点,实现 GBK 到宽字符的转换,保证中文处理的正确性。
三、计算模块接口部分的性能改进
1. 优化总耗时
累计投入约 2 小时进行性能优化,覆盖词频统计、特征匹配、文本预处理三个核心环节。
2. 优化思路与效果
| 优化阶段 | 初始实现 | 改进方案 | 性能提升 |
|---|---|---|---|
| 词频统计优化 | 使用std::vector<std::pair<wstring, int>>存储词频,每次新增特征遍历查找,时间复杂度 O (n) | 替换为std::unordered_map<wstring, int>哈希表存储,查找时间复杂度 O (1) | 长文本词频统计速度提升约 65% |
| 特征遍历优化 | 生成 Bigram 与统计词频分两次遍历特征列表 | 预分配容器空间,减少内存动态扩容次数 | 特征处理环节耗时减少约 30% |
| 文本清洗优化 | 先过滤标点、再过滤数字字母,两次遍历文本 | 单次遍历通过 Unicode 范围判断,仅保留中文字符 | 预处理耗时减少约 45% |
| 相似度计算优化 | 固定遍历第一个词频表 | 选择长度更短的词频表进行遍历,减少哈希查找次数 | 相似度计算速度提升约 20% |
3. 性能分析结果
使用 Visual Studio 2017 性能探查器对 1000 行中文文本样本进行采样分析,函数耗时占比如下:
- 特征提取与词频统计(
generateBigram + calcWordFreq):占总耗时的 52%,是程序中消耗最大的函数,主要消耗在宽字符拼接与哈希表插入操作。 - 相似度计算(
calcCosineSimilarity):占总耗时的 28%,主要消耗在哈希表查找与浮点运算。 - 文件 IO 与编码转换:合计占总耗时的 20%。
性能分析图可通过 VS 2017「调试」→「性能诊断」→「CPU 使用率」工具生成,火焰图中最宽的函数条对应特征提取模块。
四、计算模块部分单元测试展示
1. 测试框架与覆盖范围
采用 Visual Studio 2017 自带的Microsoft Unit Testing Framework for C++(本机单元测试项目),覆盖核心模块的所有公共函数:
TextUtils::filterNonChinese(文本清洗)TextUtils::generateBigram(特征生成)SimilarityCalculator::calcCosineSimilarity(相似度计算)FileUtils::readFile(文件读取)
2. 单元测试代码节选
#include "pch.h"
#include "CppUnitTest.h"
#include "../PaperChecker/TextUtils.h"
#include "../PaperChecker/SimilarityCalculator.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace PaperCheckerUnitTest
{
// 文本处理模块测试
TEST_CLASS(TextUtilsTest)
{
public:
// 测试带标点文本的清洗
TEST_METHOD(TestFilterNonChinese_Normal)
{
std::wstring input = L"今天是星期天,天气晴,今天晚上我要去看电影。";
std::wstring expected = L"今天是星期天天气晴今天晚上我要去看电影";
std::wstring result = TextUtils::filterNonChinese(input);
Assert::AreEqual(expected, result);
}
// 测试Bigram生成
TEST_METHOD(TestGenerateBigram_Sample)
{
std::wstring text = L"今天天气";
std::vector<std::wstring> result = TextUtils::generateBigram(text);
std::vector<std::wstring> expected = { L"今天", L"天是", L"天气" };
Assert::AreEqual(expected.size(), result.size());
for (size_t i = 0; i < expected.size(); i++)
{
Assert::AreEqual(expected[i], result[i]);
}
}
};
// 相似度计算模块测试
TEST_CLASS(SimilarityTest)
{
public:
// 完全相同文本,相似度应为1.0
TEST_METHOD(TestCosine_Identical)
{
std::unordered_map<std::wstring, int> freq1 = { {L"今天", 1}, {L"天气", 1} };
std::unordered_map<std::wstring, int> freq2 = { {L"今天", 1}, {L"天气", 1} };
double sim = SimilarityCalculator::calcCosineSimilarity(freq1, freq2);
Assert::AreEqual(1.0, sim, 0.001);
}
// 完全不同文本,相似度应为0.0
TEST_METHOD(TestCosine_Different)
{
std::unordered_map<std::wstring, int> freq1 = { {L"今天", 1} };
std::unordered_map<std::wstring, int> freq2 = { {L"明天", 1} };
double sim = SimilarityCalculator::calcCosineSimilarity(freq1, freq2);
Assert::AreEqual(0.0, sim, 0.001);
}
// 题目样例相似度验证
TEST_METHOD(TestCosine_SampleCase)
{
std::unordered_map<std::wstring, int> origFreq = {
{L"今天",2}, {L"天是",1}, {L"是星",1}, {L"星期",1},
{L"期天",1}, {L"天天",1}, {L"天气",1}, {L"气晴",1},
{L"晴今",1}, {L"天晚",1}, {L"晚上",1}, {L"上我",1},
{L"我要",1}, {L"要去",1}, {L"去看",1}, {L"看电",1}, {L"电影",1}
};
std::unordered_map<std::wstring, int> plagFreq = {
{L"今天",1}, {L"天是",1}, {L"是周",1}, {L"周天",1},
{L"天天",1}, {L"天气",1}, {L"气晴",1}, {L"晴朗",1},
{L"朗我",1}, {L"我晚",1}, {L"晚上",1}, {L"上要",1},
{L"要去",1}, {L"去看",1}, {L"看电",1}, {L"电影",1}
};
double sim = SimilarityCalculator::calcCosineSimilarity(origFreq, plagFreq);
Assert::IsTrue(sim > 0.6 && sim < 0.7);
}
};
}
3. 测试数据构造思路
- 等价类划分:正常中文文本、全标点文本、混合字符文本、单字文本
- 边界值测试:完全相同文本(相似度 1.0)、完全无重叠文本(相似度 0.0)、长度为 1 的文本
- 业务场景测试:题目给出的样例文本,验证结果符合直观认知
4. 测试覆盖率
通过 Visual Studio 2017 代码覆盖率工具检测,核心计算模块代码覆盖率达 93%,其中相似度计算模块 100% 覆盖,文本处理模块覆盖所有分支。
覆盖率截图可在 VS 测试资源管理器中通过「分析代码覆盖率」功能生成,发布到博客中
五、计算模块部分异常处理说明
本项目自定义 4 类业务异常,均继承自std::exception,覆盖程序运行的所有典型错误场景,确保错误信息明确、可定位。
表格
| 异常类 | 设计目标 | 触发场景 | 单元测试样例 |
|---|---|---|---|
ArgumentCountException |
校验命令行参数数量,防止参数缺失 / 过多导致程序异常运行 | 命令行参数数量不等于 4(程序名 + 3 个文件路径) | 启动程序时不传入任何参数,捕获异常并验证错误信息包含「参数数量错误」 |
FileOpenException |
统一捕获文件 IO 错误,明确提示出错的文件路径 | 原文 / 抄袭版文件不存在、输出路径无写入权限 | 传入不存在的路径C:\test\no_file.txt作为原文,程序抛出该异常,信息包含「无法打开文件」 |
EmptyFileException |
识别空文件场景,避免后续无意义计算 | 文件读取后内容为空,或清洗后无有效中文字符 | 传入一个空 txt 文件,读取时抛出该异常,信息包含「文件内容为空」 |
EmptyFeatureException |
防止特征为空导致的除零错误或计算异常 | 文本长度不足 2 且无有效特征,无法生成 Bigram | 传入仅含 1 个汉字的文本,计算前校验抛出该异常,信息包含「文本特征为空」 |
异常单元测试样例
TEST_CLASS(ExceptionTest)
{
public:
// 测试文件打开异常
TEST_METHOD(TestFileOpenException)
{
bool hasCaught = false;
try
{
FileUtils::readFile("C:\\invalid\\not_exist.txt");
}
catch (const FileOpenException& e)
{
hasCaught = true;
std::string msg = e.what();
Assert::IsTrue(msg.find("无法打开") != std::string::npos);
}
Assert::IsTrue(hasCaught);
}
// 测试参数数量异常
TEST_METHOD(TestArgCountException)
{
bool hasCaught = false;
try
{
throw ArgumentCountException("参数数量错误");
}
catch (const ArgumentCountException& e)
{
hasCaught = true;
}
Assert::IsTrue(hasCaught);
}
};


浙公网安备 33010602011771号