第一次个人编程作业

这个作业属于哪个课程 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)相似度计算:余弦相似度算法

基于词袋模型计算两篇文本特征向量的余弦相似度,结果即为重复率。

  • 步骤:
    1. 分别统计两篇文本所有 Bigram 特征的出现频次,生成词频映射
    2. 以两个向量的点积除以向量模长的乘积,得到余弦值
    3. 余弦值范围为 [0,1],值越大表示文本重复度越高
  • 公式:
    image

其中Ai、Bi分别为两个文本中第i个特征的词频。

3. 设计独到之处

  1. 零第三方依赖:全部功能基于 C++ 标准库实现,无需配置分词库、数值计算库,直接编译即可运行,适配课程作业提交要求。
  2. 分层异常设计:在参数校验、文件 IO、文本处理、相似度计算各层均设置异常抛出点,错误类型明确,便于定位问题。
  3. 性能与准确率平衡:Bigram + 余弦相似度的组合,时间复杂度为 O (n+m),远优于 LCS 等 O (nm) 算法,同时对中文文本的查重准确率满足课程作业要求。
  4. 编码适配:针对 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);
    }
};
posted @ 2026-09-15 22:59  邓文炜  阅读(4)  评论(0)    收藏  举报