第一次个人编程作业

https://github.com/your-username/your-repo

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

一、PSP 表格

PSP2.1 阶段 任务内容 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 25
· Estimate 估计任务需要多少时间 30 25
Development 开发 300 360
· Analysis 需求分析(包括学习新技术) 60 80
· Design Spec 生成设计文档 40 50
· Design Review 设计复审 20 15
· Coding Standard 代码规范 15 10
· Design 具体设计 50 60
· Coding 具体编码 80 120
· Code Review 代码复审 20 15
· Test 测试(自测、改bug) 15 10
Test Report 测试报告 20 20
Size Measurement 计算工作量 10 5
Postmortem 事后总结 20 15
合计 380 420

二、计算模块接口的设计与实现过程

2.1 整体架构设计

本项目采用面向对象的设计思想,将论文查重系统拆分为两个核心类:

  • TextProcessor(文本处理器):负责文件读取、文本预处理、n-gram 生成
  • PlagiarismCalculator(查重计算器):负责相似度计算和重复率评估

整体调用流程如下:

main.cpp (入口)
    ↓
PlagiarismCalculator::calculatePlagiarismRate()
    ├── TextProcessor::readFile()      ← 读取原文文件
    ├── TextProcessor::readFile()      ← 读取抄袭版文件
    ├── TextProcessor::preprocess()    ← 预处理原文
    ├── TextProcessor::preprocess()    ← 预处理抄袭版
    ├── TextProcessor::generateNGrams() ← 生成原文3-gram
    ├── TextProcessor::generateNGrams() ← 生成抄袭版3-gram
    └── calculateJaccardSimilarity()    ← 计算Jaccard相似度
    ↓
writeResult() → 输出到答案文件

2.2 核心算法:3-gram + Jaccard 相似度

算法选择思路:

论文查重的核心是衡量两段文本的相似程度。常见的文本相似度算法有:

  • 余弦相似度(Cosine Similarity)
  • Jaccard 相似度
  • SimHash
  • 编辑距离(Levenshtein Distance)

考虑到中文文本的特点和性能要求,我选择了 3-gram + Jaccard 相似度 方案,原因如下:

  1. 3-gram(三元语法):将连续的三个字符作为一个"指纹"单元,能有效捕捉中文文本的局部语义特征。相比单字比较,3-gram 对语序变化和局部修改的敏感度更高。

  2. Jaccard 相似度:公式为 J(A,B) = |A∩B| / |A∪B|,即两个集合交集大小除以并集大小。这个指标直观地反映了两段文本的"重叠程度",适合查重场景。

2.3 关键函数说明

1. 文本预处理 preprocess()

预处理阶段去除所有无意义的字符,只保留:

  • 基本汉字(U+4E00 ~ U+9FFF)
  • 英文字母(统一转小写)
  • 数字

这样可以消除标点、空格、换行等对相似度计算的干扰,让查重结果更准确地反映文本内容的重复程度。

2. 生成 n-gram generateNGrams()

将预处理后的文本按 UTF-8 字符边界切分,然后用滑动窗口生成所有长度为 n 的子串集合。默认 n=3。

例如:"今天天气好" → {今天天, 今天气, 天天气, 天气好}

3. 计算 Jaccard 相似度 calculateJaccardSimilarity()

遍历较小的集合,统计与另一个集合的交集数量,然后计算 Jaccard 系数。优化点是遍历较小的集合,减少查找次数。

2.4 独到之处

  1. UTF-8 字符安全处理:正确处理中文 UTF-8 多字节字符,按字符边界切分 n-gram,避免出现半个汉字的情况。

  2. 精确的汉字范围过滤:只保留基本汉字区(U+4E00~U+9FFF),有效过滤中文标点、全角符号等干扰字符。

  3. 效率优化:计算交集时遍历较小的集合,将时间复杂度从 O(|A|+|B|) 优化为 O(min(|A|,|B|))。


三、计算模块接口的性能改进

3.1 性能分析思路

初始版本的算法直接遍历两个集合的所有元素计算交集,时间复杂度为 O(|A| × |B|)。对于长文本(比如几万字的论文),3-gram 集合大小可能达到数万个,双重遍历的效率较低。

3.2 改进方案

改进前:双重循环遍历两个集合

for (auto& a : setA) {
    for (auto& b : setB) {
        if (a == b) intersection++;
    }
}

时间复杂度:O(|A| × |B|)

改进后:利用哈希集合的 O(1) 查找,遍历较小集合

for (const auto& gram : smallerSet) {
    if (largerSet.count(gram)) intersection++;
}

时间复杂度:O(min(|A|, |B|))

3.3 性能提升效果

以一篇约 5000 字的论文为例:

  • 3-gram 集合大小约 5000 个
  • 改进前:约 25,000,000 次比较操作
  • 改进后:约 5,000 次哈希查找操作

性能提升约 5000 倍,完全满足 5 秒内出结果的要求。

3.4 内存占用分析

使用 unordered_set 存储 n-gram,对于 5000 个 3-gram(每个约 9 字节),内存占用约几十 KB,远低于 2048MB 的限制。


四、计算模块单元测试展示

4.1 测试框架设计

本次单元测试共设计了 12 个测试用例(超过要求的 10 个),覆盖以下场景:

编号 测试用例 测试函数 预期结果
1 完全相同文本 testIdenticalText 重复率 100%
2 完全不同文本 testCompletelyDifferentText 重复率接近 0%
3 部分抄袭文本 testPartialPlagiarism 中等相似度
4 两个空文本 testBothEmpty 重复率 100%
5 一个空一个非空 testOneEmpty 重复率 0%
6 标点不同内容相同 testDifferentPunctuation 相似度 >90%
7 英文大小写不敏感 testCaseInsensitive 预处理后相同
8 短文本不崩溃 testShortText 正常运行不报错
9 纯标点文本 testOnlyPunctuation 预处理后为空
10 长文本部分重叠 testLongTextPartialOverlap 中高相似度
11 文件不存在 testFileNotFound 抛出异常
12 空白字符去除 testWhitespaceRemoval 无空白字符残留

4.2 测试代码片段

// 测试用例1:完全相同的文本,重复率应为100%
void testIdenticalText() {
    writeTestFile("test_orig_1.txt", "今天天气很好,我们去公园玩吧。");
    writeTestFile("test_plag_1.txt", "今天天气很好,我们去公园玩吧。");

    double rate = PlagiarismCalculator::calculatePlagiarismRate("test_orig_1.txt", "test_plag_1.txt");
    TEST_ASSERT(std::abs(rate - 100.0) < 0.01, "测试1: 完全相同文本→100%");
}

// 测试用例11:文件不存在,应抛出异常
void testFileNotFound() {
    bool exceptionThrown = false;
    try {
        PlagiarismCalculator::calculatePlagiarismRate("nonexistent.txt", "nonexistent.txt");
    } catch (const FileOpenException& e) {
        exceptionThrown = true;
    }
    TEST_ASSERT(exceptionThrown, "测试11: 文件不存在抛出异常");
}

4.3 测试结果

===== 论文查重程序单元测试 =====
[PASS] 测试1: 完全相同文本→100%
[PASS] 测试2: 完全不同文本→接近0%
[PASS] 测试3: 部分抄袭→中等相似度
[PASS] 测试4: 两个空文本→100%
[PASS] 测试5: 一空一非空→0%
[PASS] 测试6: 标点不同但内容相同→>90%
[PASS] 测试7: 英文大小写不敏感
[PASS] 测试8: 短文本不崩溃
[PASS] 测试9: 纯标点预处理后为空
[PASS] 测试10: 长文本部分重叠→中高相似度
[PASS] 测试11: 文件不存在抛出异常
[PASS] 测试12: 空白字符被去除

===== 测试结果汇总 =====
通过: 12 个
失败: 0 个
总计: 12 个

测试覆盖率分析:12 个测试用例覆盖了正常流程、边界条件、异常场景三大类,基本覆盖了核心算法的所有分支路径。白盒测试角度,包括了:

  • 预处理函数的所有分支(汉字保留、ASCII保留、其他过滤)
  • n-gram 生成的边界(短文本返回空集合)
  • Jaccard 计算的三种情况(都空、一空一非空、都非空)
  • 文件读取的异常情况

五、计算模块异常处理说明

5.1 异常设计目标

本程序设计了完善的异常处理机制,确保程序在遇到错误输入时不会崩溃,而是给出明确的错误提示。主要异常类型包括:

5.2 异常类型与场景

1. FileOpenException(文件打开异常)

  • 设计目标:当指定的原文或抄袭版文件不存在、无法读取时,抛出此异常,告知用户文件路径有误或文件不可访问。
  • 错误场景:用户输入了错误的文件路径,或者文件被其他程序锁定无法读取。
  • 单元测试样例
    void testFileNotFound() {
        bool exceptionThrown = false;
        try {
            PlagiarismCalculator::calculatePlagiarismRate("nonexistent.txt", "nonexistent.txt");
        } catch (const FileOpenException& e) {
            exceptionThrown = true;
        }
        TEST_ASSERT(exceptionThrown, "文件不存在抛出异常");
    }
    

2. 参数数量异常(命令行参数错误)

  • 设计目标:当用户没有提供正确数量的命令行参数时,提示正确的用法说明。
  • 错误场景:用户只传了两个参数,或者参数顺序错误。
  • 处理方式:输出用法说明并返回错误码 1。

3. 输出文件写入异常

  • 设计目标:当答案文件路径无效(如目录不存在、权限不足)时,抛出异常。
  • 错误场景:用户指定的输出路径在不存在的目录下。
  • 处理方式:捕获 std::exception,输出错误信息并返回错误码 3。

4. 未知异常兜底

  • 设计目标:捕获所有未预料到的异常,确保程序不会直接崩溃退出。
  • 错误场景:内存不足、 unexpected runtime error 等。
  • 处理方式:捕获 ...,输出"未知异常发生"并返回错误码 4。

5.3 错误码设计

错误码 含义
0 正常退出
1 命令行参数数量错误
2 文件打开错误
3 其他运行时异常
4 未知异常

这种设计让调用者(如评测脚本)可以通过返回值快速判断错误类型,便于自动化测试和问题排查。


六、总结

本次个人编程作业实现了一个基于 3-gram + Jaccard 相似度的论文查重系统。通过 PSP 流程管理,我对整个开发过程有了更清晰的认识:

  1. 预估与实际的差距:实际编码时间比预估多了 40 分钟,主要花在了 UTF-8 字符处理的调试上,这也提醒我在今后的开发中要充分考虑编码问题。

  2. 单元测试的价值:12 个测试用例帮助我发现了中文标点被误识别为汉字的问题,如果没有测试,这个 bug 可能会导致查重结果不准。

  3. 性能意识:通过优化交集计算方式,将时间复杂度从 O(n²) 降到了 O(n),虽然对于小规模数据差别不大,但这是一个良好的编程习惯。

posted @ 2026-09-15 21:06  cjx9128  阅读(7)  评论(0)    收藏  举报