| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15693 |
| 这个作业的目标 | 完成个人编程任务 |
GitHub项目地址:https://github.com/Stella-0203/Software-Engineering-course-project
一、PSP预估与实际耗时
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 15 |
| · Estimate | 估计这个任务需要多少时间 | 20 | 15 |
| Development | 开发 | 40 | 40 |
| · Analysis | 需求分析 (包括学习新技术) | 50 | 60 |
| · Design Spec | 生成设计文档 | 30 | 20 |
| · Design Review | 设计复审 | 15 | 15 |
| · Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
| · Design | 具体设计 | 40 | 60 |
| · Coding | 具体编码 | 100 | 50 |
| · Code Review | 代码复审 | 30 | 20 |
| · Test | 测试(自我测试,修改代码,提交修改) | 100 | 120 |
| Reporting | 报告 | 40 | 40 |
| · Test Repor | 测试报告 | 30 | 30 |
| · Size Measurement | 计算工作量 | 15 | 15 |
| · Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 20 |
| ·合计 | 570 | 530 |
二、计算模块设计与实现
2.1模块与函数
本项目采用 Python 实现,计算模块主要位于 src/plagiarism_checker.py 中。整体采用函数化设计,没有额外引入类结构,将论文重复率计算过程拆分为文本预处理、n-gram 统计、TF-IDF 向量构建、余弦相似度计算等几个相互衔接的功能。
主要函数及其职责如下:
| 函数 | 主要功能 |
|---|---|
| normalize_text() | 去除文本中的空白字符,完成基础预处理 |
| build_ngram_counts() | 提取文本中的 2-gram 和 3-gram,并统计其出现次数 |
| build_tfidf_vector() | 根据 n-gram 词频和文档频率计算 TF-IDF,并构建文本向量 |
| cosine_similarity() | 计算两个 TF-IDF 向量之间的余弦相似度 |
| calculate_repetition_rate() | 组织完整计算流程,对外提供重复率计算接口 |
其中,calculate_repetition_rate() 作为计算模块的主要对外接口,接收原文和抄袭文本两个字符串,并依次调用其他函数完成重复率计算。函数之间的调用关系如下:
calculate_repetition_rate()
│
├── build_ngram_counts()
│ └── normalize_text()
│
├── build_tfidf_vector()
│
└── cosine_similarity()
这种组织方式将文本处理、特征提取和相似度计算分离,便于后续分别进行单元测试和性能分析。
2.2核心算法
程序首先对原文和待检测文本进行预处理,去除空格、换行等空白字符,在保留正文其他字符的基础上减少格式差异对计算结果的影响。随后分别提取文本中的 2-gram 和 3-gram。例如,对于连续文本 abcdef,可以得到:
2-gram:ab、bc、cd、de、ef
3-gram:abc、bcd、cde、def
程序统计每个 n-gram 的出现次数,并以此作为文本的基本特征。
在此基础上,使用 TF-IDF 对各个 n-gram 进行加权。TF 用于表示某个 n-gram 在当前文本中的出现频率,IDF 则根据其在两个文本中的出现情况调整权重,从而降低公共高频片段对结果的影响。最终分别得到原文和待检测文本的 TF-IDF 向量。
最后计算两个向量之间的余弦相似度:
[Similarity(A,B)=\frac{A\cdot B}{|A||B|}]
余弦相似度越接近 1,说明两个文本的 n-gram 分布越接近;越接近 0,则说明两者差异越大。本项目将该相似度作为最终的论文重复率,并按照题目要求保留两位小数输出。
整体计算流程可以概括为:
原文 / 抄袭文本
↓
去除空白字符
↓
提取 2-gram、3-gram
↓
统计 n-gram 词频
↓
构建 TF-IDF 向量
↓
计算余弦相似度
↓
重复率
2.3算法选择与特点
在确定最终方案前,对不同文本相似度计算方法进行了实际测试。尝试过 LCS、不同 n-gram 参数以及结合局部匹配的方案。
LCS 对连续文本中的增删改具有较直观的匹配效果,但完整 LCS 的计算开销较大,在测试数据上的运行时间约为 8~10 秒,无法满足题目要求的 5 秒运行时间限制。调整 n-gram 粒度后发现,1/2-gram 对文本局部修改较为宽松,而 3/4-gram 又容易受到文本修改的影响。部分基于固定分块的局部匹配方案还存在文本插入或删除后位置发生偏移的问题。
综合比较计算效果、运行效率和实现复杂度后,最终选择 2-gram 与 3-gram 结合 TF-IDF 和余弦相似度的方案。
该方案的主要特点是通过同时使用两种粒度的 n-gram 提取局部文本特征,在一定程度上兼顾了对文本修改的容忍性和特征区分能力;同时采用 TF-IDF 对特征进行加权,并通过余弦相似度完成两个文本整体特征的比较。
| 方案 | add | del | dis_1 | dis_10 | dis_15 | 运行情况 |
|---|---|---|---|---|---|---|
| LCS | 0.84 | 0.82 | 0.97 | 0.84 | 0.68 | 约 8~10s |
| 1/2-gram | 0.99 | … | … | … | … | 快 |
| 3/4-gram | 0.46 | … | … | … | … | 快 |
| 2/3-gram + TF-IDF | 0.81 | 0.83 | 0.95 | 0.84 | 0.61 | 满足要求 |
三、性能分析与优化
3.1性能测试与瓶颈
第一版程序实现后,首先使用 Python 内置的 cProfile 对程序进行性能分析。测试过程中选取实际论文文本作为输入,对主要函数的累计耗时进行统计。
优化前的主要性能数据如下:
| 函数 | 累计耗时 |
|---|---|
| calculate_repetition_rate() | 0.090 s |
| build_ngram_counts() | 0.039 s |
| build_tfidf_vector() | 0.020 s |
| cosine_similarity() | 0.014 s |
从分析结果可以看出,build_ngram_counts() 是计算模块中耗时较为明显的函数之一。进一步检查其实现发现,原程序分别对文本进行一次 2-gram 遍历和一次 3-gram 遍历:
文本
├── 第一次遍历 → 2-gram
└── 第二次遍历 → 3-gram
两次遍历的主体逻辑基本相同,因此存在重复扫描文本的问题。虽然对于当前测试数据而言程序运行时间已经满足 5 秒限制,但该部分仍然存在可以直接优化的空间。
3.2优化思路与结果
针对上述问题,将 2-gram 和 3-gram 的提取过程合并到同一个循环中。在每次遍历文本位置时,同时生成当前位置对应的 2-gram 和 3-gram,从而避免对文本进行两次完整扫描。
文本
↓
单次遍历
├── 生成 2-gram
└── 生成 3-gram
优化后重新进行性能分析,主要函数的累计耗时如下:
| 函数 | 优化前 | 优化后 |
|---|---|---|
| calculate_repetition_rate() | 0.090 s | 0.068 s |
| build_ngram_counts() | 0.039 s | 0.029 s |
| build_tfidf_vector() | 0.020 s | 0.017 s |
| cosine_similarity() | 0.014 s | 0.010 s |
其中,build_ngram_counts() 的累计耗时由 0.039 s 降至 0.029 s,降低约 25.6%;整个 calculate_repetition_rate() 的累计耗时由 0.090 s 降至 0.068 s。同时,对优化后的程序重新进行功能测试,5 组测试数据的结果均与优化前保持一致:
| 测试数据 | 优化后结果 |
|---|---|
| orig_0.8_add.txt | 0.81 |
| orig_0.8_del.txt | 0.83 |
| orig_0.8_dis_1.txt | 0.95 |
| orig_0.8_dis_10.txt | 0.84 |
| orig_0.8_dis_15.txt | 0.61 |
此外,使用 PowerShell 对完整命令行程序进行多次实际运行测试,优化前稳定运行时间约为 0.13~0.14 s,优化后约为 0.12~0.13 s。由于完整运行时间还包括 Python 启动和文件读写等开销,因此实际程序运行时间的下降幅度小于核心计算函数的下降幅度,但仍能够观察到一定的性能改善。
本次优化没有改变重复率计算方法,仅减少了 n-gram 提取过程中的重复遍历,在保持计算结果一致的情况下降低了计算开销。
四、单元测试
4.1测试函数与测试数据
为验证论文查重计算模块中各函数的正确性,使用 pytest 编写单元测试,对 src/plagiarism_checker.py 中的主要函数分别进行测试。测试采用白盒测试思路,根据函数的具体实现和内部判断分支设计测试用例,而不仅仅验证最终的重复率结果。
本次测试覆盖的主要函数及测试内容如下:
| 测试函数 | 主要测试内容 |
|---|---|
| normalize_text() | 空白字符处理、标点保留 |
| build_ngram_counts() | 空文本、短文本、正常 n-gram、重复 n-gram、空白处理 |
| build_tfidf_vector() | 空输入、单个 n-gram、多 n-gram 及 TF-IDF 计算 |
| cosine_similarity() | 空向量、相同向量、正交向量、零范数、部分重合向量 |
| calculate_repetition_rate() | 双空文本、单空文本、完全相同文本、不同文本及结果范围 |
测试代码统一存放在:tests/test_plagiarism_checker.py
4.2测试数据构造
测试数据根据各函数的功能特点分别构造。
对于文本预处理和 n-gram 提取,主要使用短文本进行精确验证。例如通过 "abcd" 验证 2-gram 和 3-gram 的具体生成结果,通过 "aaaa" 验证重复 n-gram 的计数是否正确,同时使用包含空格的文本验证预处理过程。
对于 TF-IDF 和余弦相似度计算,采用人工构造的词频和向量数据,使测试结果能够根据公式直接确定。例如使用完全相同的向量验证余弦相似度应为 1,使用没有共同维度的向量验证结果应为 0,并针对空向量和零范数情况验证程序的边界处理。
对于整体重复率计算,则分别构造空文本、完全相同文本、完全不同文本以及普通文本等情况,从而验证不同输入条件下程序的处理是否符合预期。
这种测试数据构造方式能够将具体函数的输入与预期结果对应起来,便于定位测试失败时的具体模块。
4.3测试结果与覆盖率
在项目虚拟环境中运行:pytest-v
共收集到20个测试用例,全部通过:20 passed in 0.13s
随后使用pytest-cov进行代码覆盖率测试:pytest--cov=src--cov--report=term-missing
测试结果如下:
| 文件/模块 | Stmts | Miss | Cover |
|---|---|---|---|
| src_init_.py | 0 | 0 | 100% |
| src\plagiarism_checker.py | 50 | 0 | 100% |
| TOTAL | 50 | 0 | 100% |
20 passed in 0.17s
测试结果表明,plagiarism_checker.py 共包含 50 条可执行语句,本次测试覆盖 50/50 条语句,代码覆盖率达到 100%,且没有未覆盖的代码行。

五、异常处理
5.1 异常处理设计
程序在命令行参数解析、文件读写以及重复率计算过程中,对可能出现的异常和特殊输入进行了处理,以避免程序因非法输入或文件操作失败而直接异常退出。
具体设计如下:
| 异常或特殊场景 | 处理方式 | 具体设计 |
|---|---|---|
| 命令行参数数量错误 | 检查 sys.argv 长度,输出提示并返回 1 |
在程序入口处检查命令行参数数量,只有参数数量为 4 时才继续执行,避免因参数缺失或过多导致后续路径解析错误 |
| 输入文件不存在或无法读取 | 捕获 OSError,输出文件读取失败信息并返回 1 |
将两个输入文件的读取操作放入 try-except 中,对文件不存在、权限不足等操作系统级异常进行统一处理 |
| 输入文件编码错误 | 捕获 UnicodeError,输出文件读取失败信息并返回 1 |
使用 UTF-8 读取文本,当文件编码不符合要求时捕获编码异常并终止当前计算 |
| 输出文件无法写入 | 捕获 OSError,输出文件写入失败信息并返回 1 |
将结果文件写入操作单独进行异常捕获,避免输出路径无效、权限不足等问题导致程序异常退出 |
| 原文和抄袭文本均为空 | 返回 1.0 |
对两个输入文本同时为空的情况进行特殊定义,避免进入 n-gram 和向量计算流程 |
| 仅一篇文本为空 | 返回 0.0 |
当其中一篇文本为空时直接返回 0,避免空文本参与向量计算 |
| TF-IDF 输入为空 | 返回空向量 {} |
在构建 TF-IDF 向量前检查词频统计结果,空输入直接返回空向量 |
| 余弦相似度输入为空或零范数 | 返回 0.0 |
在计算点积和范数前检查向量是否为空,并在计算结果前判断范数是否为 0,避免除零错误 |
其中,文件操作相关异常采用 try-except 进行显式处理;算法内部则通过输入判断处理空数据和零范数等边界情况。这样既保证了程序对异常输入具有较好的容错能力,也避免使用过于宽泛的异常捕获而隐藏真正的程序错误。
5.2异常与边界场景测试
针对上述异常处理设计,使用 pytest 对程序的异常分支和算法边界情况进行测试。测试通过 monkeypatch 模拟文件读取、文件写入等异常操作,使测试可以在不依赖实际文件系统异常的情况下验证异常处理逻辑。
本次新增了 4 个针对 main() 的异常测试:
| 测试函数 | 测试场景 | 预期结果 |
|---|---|---|
| test_main_wrong_argument_count() | 命令行参数数量错误 | 返回 1 |
| test_main_file_read_os_error() | 输入文件读取产生 OSError | 捕获异常并返回 1 |
| test_main_file_read_unicode_error() | 输入文件读取产生 UnicodeError | 捕获异常并返回 1 |
| test_main_output_write_os_error() | 输出文件写入产生 OSError | 捕获异常并返回 1 |
此前的 20 个计算模块测试同时覆盖了空文本、空向量、零范数等边界情况,因此本次异常处理测试与原有单元测试共同构成了完整的异常和边界场景验证。
最终共执行 24 个测试用例,全部通过:24 passed in 0.09s
其中,计算模块的覆盖率测试结果如下:
| 文件/模块 | Stmts | Miss | Cover |
|---|---|---|---|
| src_init_.py | 0 | 0 | 100% |
| src\plagiarism_checker.py | 50 | 0 | 100% |
| TOTAL | 50 | 0 | 100% |
覆盖率测试结果为 100%,plagiarism_checker.py 中共 50 条可执行语句,全部被测试覆盖,没有未覆盖代码。

浙公网安备 33010602011771号