第一次个人编程作业:论文查重

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 个类 和 若干工具函数。

  1. 类设计
    | 类名 | 职责 | 主要成员函数 |
    | ---- | ---- | ---- |
    | FileManager | 文件读写与编码处理 | readFile(path)、writeAnswer(path, score)|
    | TextProcessor | 文本预处理与特征提取 | extractFeatures(text)|
    | SimilarityCalculator | 相似度计算与流程调度 |c alculate(origPath, copyPath)|
  2. 函数接口说明
    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)
调度整个计算流程:读取文件 → 特征提取 → 构建词频向量 → 计算余弦相似度 → 返回结果。

  1. 辅助函数
    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)
根据两个频次向量计算余弦相似度。

  1. 类与函数关系
    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,重新生成。

  1. 打开性能探查器
    VS 菜单:调试 → 性能探查器(快捷键 Alt + F2)。

  2. 选择分析目标
    勾选 “CPU 使用率”(最常用)。

如果还想看内存,可同时勾选 “.NET 对象分配” 或 “内存使用率”,但 C++ 项目主要看 CPU。

点击 “开始”。

  1. 传入命令行参数
    因为程序需要三个文件路径,性能探查器启动时会以调试方式运行。
    在启动前,确保 项目属性 → 调试 → 命令参数 已经填好:

text
C:\tests\orig.txt C:\tests\orig_add.txt C:\tests\ans.txt
这样探查器启动后会自动读取这三个路径。

  1. 等待程序运行结束
    程序运行完毕后,VS 会自动生成性能报告。
    在报告中:

点击 “函数” 视图。

按 “总 CPU 时间” 或 “独占 CPU 时间” 降序排序。

排在第一位的函数就是消耗最大的函数。

点击该函数,可以看到调用树和具体耗时占比。
屏幕截图 2026-09-16 233854
改进思路
初始版本中,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 的引用。

项目结构:
屏幕截图 2026-09-16 234128
造测试数据的思路
采用 等价类划分 + 边界值分析 + 异常场景 三种思路:

正常等价类:使用题目样例“今天是星期天……”与“今天是周天……”,验证部分相似场景。

边界值:

完全相同文本 → 期望相似度 1.0;

完全不同文本 → 期望相似度 0.0;

空字符串 → 期望相似度 0.0;

单字符文本、纯标点文本 → 验证不崩溃。

异常场景:

文件不存在 → 期望抛出异常;

答案文件路径不可写 → 验证异常处理;

编码为 GB18030 的文件 → 验证编码自适应读取。

格式验证:对 writeAnswer 传入 0.8567,检查文件内容是否为 0.86,确保输出两位小数。

通过上述数据组合,计算模块核心函数的行覆盖率可达到 90% 以上,分支覆盖率可达到 85% 以上。

五、计算模块部分异常处理说明
异常处理设计目标
计算模块负责读取文件、提取文本特征、计算相似度并写入答案。为了让程序在面对非法输入、文件错误、环境异常时仍能稳定运行,而不是直接崩溃或异常退出,计算模块设计了统一的异常处理机制。设计目标如下:

不崩溃:任何可预见的错误都应被捕获,程序以友好提示或默认值结束。

可定位:不同错误对应不同异常类型,便于快速定位问题。

可恢复:部分异常(如空输入)捕获后写入 0.00,保证答案文件仍然生成。

不泄露资源:文件句柄、内存等资源在异常发生时能正确释放。

符合评测要求:不因异常导致 5 秒超时、内存超限或异常退出

posted @ 2026-09-17 01:15  谢毕护  阅读(2)  评论(0)    收藏  举报