论文查重算法
论文查重算法——个人项目作业
学号: 3124004212
姓名: 李智杰
Github仓库截图:

一、项目需求
本次个人项目要求设计并实现一个论文查重程序。
程序接收三个命令行参数:
python main.py <原文文件绝对路径> <抄袭版论文文件绝对路径> <答案文件绝对路径>
其中:
- 第一个参数为原文文件;
- 第二个参数为经过增、删、改后的论文文件;
- 第三个参数为查重结果输出文件。
程序读取两个文本文件,计算两篇文本的相似度,并将最终结果以浮点数形式写入答案文件,结果保留两位小数。
例如:
0.80
答案文件中只保存查重结果,不添加"相似度""重复率"等其他文字,以保证符合自动评测要求。
二、PSP 表格
在正式开发前,我按照 PSP(Personal Software Process)对整个项目的开发过程进行了时间规划。
| PSP2.1 | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 35 |
| Estimate | 估计任务需要多少时间 | 20 | 25 |
| Development | 开发 | 820 | 1150 |
| Analysis | 需求分析 | 70 | 85 |
| Design Spec | 生成设计文档 | 90 | 100 |
| Design Review | 设计复审 | 40 | 45 |
| Coding Standard | 代码规范 | 20 | 30 |
| Design | 具体设计 | 130 | 160 |
| Coding | 具体编码 | 220 | 290 |
| Code Review | 代码复审 | 70 | 90 |
| Test | 测试、修改代码、提交修改 | 180 | 350 |
| Reporting | 报告 | 100 | 150 |
| Test Report | 测试报告 | 50 | 75 |
| Size Measurement | 计算工作量 | 20 | 25 |
| Postmortem & Process Improvement Plan | 事后总结并提出改进计划 | 30 | 50 |
| 合计 | 970 | 1360 |
本次项目中,实际耗时与最初预计存在一定差异。特别是在 Visual Studio Python 性能分析环境配置、性能测试以及优化过程中花费的时间比原先预想更多。通过 PSP 表格可以更加直观地发现时间估计与实际开发之间的差距。
三、程序设计与实现
3.1 项目结构
最终项目的主要结构如下:
3124004212/
│
├── main.py
├── plagiarism_checker.py
├── requirements.txt
├── requirements-dev.txt
├── run_tests.bat
├── run_coverage.bat
│
└── tests/
└── test_plagiarism_checker.py
各文件职责如下:
| 文件 | 主要作用 |
|---|---|
| main.py | 程序入口,接收三个命令行参数 |
| plagiarism_checker.py | 文本预处理、相似度计算及文件读写 |
| requirements.txt | Python 项目运行依赖说明 |
| tests/test_plagiarism_checker.py | 单元测试 |
| run_tests.bat | 一键运行单元测试 |
| run_coverage.bat | 一键运行测试并生成分支覆盖率报告 |
程序将"命令行处理"和"论文查重算法"分开,使代码结构更加清晰,也方便对核心算法进行单独测试。
3.2 程序执行流程
整体处理流程如下:
开始
↓
读取命令行参数
↓
检查参数数量
↓
读取原文和抄袭版文本
↓
文本规范化处理
↓
生成文本特征
↓
计算文本相似度
↓
得到最终重复率
↓
保留两位小数
↓
写入答案文件
↓
结束
main.py 主要负责参数检查和程序入口控制,核心的查重逻辑由 plagiarism_checker.py 完成。
这样设计以后,程序入口只负责调度,而文本处理、文件处理和相似度计算都由独立函数负责,提高了代码的可读性和可测试性。
3.3 文本预处理
原始论文中可能存在空格、换行、标点以及全角字符等差异。如果直接进行字符串比较,这些无关差异会影响最终相似度。
因此,在正式计算前首先对文本进行规范化处理,主要包括:
去除无意义空白字符
↓
处理标点等干扰字符
↓
统一全角、半角字符
↓
得到规范化文本
例如:
今天是星期天,天气晴。
和:
今天是星期天 天气晴
虽然标点形式不同,但其正文表达基本相同,因此预处理后应尽可能降低标点对查重结果的影响。
3.4 N-gram 文本特征
仅仅比较单个字符不能很好地反映上下文关系,因此程序进一步利用 N-gram 提取相邻字符特征。
例如文本:
今天天气很好
当使用二元字符特征时,可以得到类似:
今天
天天
天气
气很
很好
然后统计这些特征出现的频率,将文本转换为可以进行数学计算的特征向量。
这种方法不依赖额外的中文分词库,程序部署更加简单,同时对于论文经过一定程度的增、删、改之后仍然能够保持一定的识别能力。
3.5 余弦相似度
得到两个文本的特征向量后,使用余弦相似度衡量两个向量的接近程度。
其基本公式为:
[
similarity(A,B)=\frac{A \cdot B}{|A||B|}
]
余弦相似度越接近 1,说明两个文本越相似;越接近 0,说明差异越大。
最终程序将计算结果处理为 0~1 范围内的相似度,并保留两位小数写入答案文件。
四、程序接口设计
程序中主要函数按照功能可以划分为以下几类。
文本处理
normalize_text()
_ngram_counter()
负责文本规范化及 N-gram 特征统计。
相似度计算
_cosine_similarity()
calculate_similarity()
负责计算特征向量之间的相似度,并生成最终论文相似度。
文件处理
read_text_file()
write_result()
check_files()
分别负责读取论文、写入结果以及完成一次完整的文件查重流程。
程序入口
main.py
负责接收:
原文路径
抄袭版路径
答案文件路径
并调用查重模块完成计算。
这种模块化设计降低了函数之间的耦合,同时方便单元测试针对不同功能分别进行验证。
五、性能分析与优化
5.1 性能分析
在完成第一版程序以后,我使用 Visual Studio 的 Python 性能分析工具对程序进行了性能检测。
为了使性能分析工具能够采集到足够的数据,我使用较大的测试文本运行程序,并观察各个函数的运行时间。
优化前性能分析截图:

从优化前的检测分析报告可以看到,总执行时间为 17.367 秒,热路径主要集中在 main.main → plagiarism_checker.check_files → plagiarism_checker.calculate_similarity → plagiarism_checker.normalize_text / plagiarism_checker.ngram_counter 这一调用链上,其中 normalize_text 占用了约 68.89% 的非独占时间,是明显的性能热点。
从"执行单个工作最多的函数"列表来看,<genexpr> 生成器表达式、_collections._count_elements、str.join、str.isalnum 等底层字符串操作的独占耗时占比很高,说明文本规范化和 N-gram 统计过程中存在较多可以精简的重复字符串处理。
因此,后续优化主要围绕这些频繁执行的文本处理操作展开。
5.2 优化思路
根据性能分析结果,我重新检查了查重算法中的数据处理过程,主要目标是减少不必要的重复计算和文本处理开销。
优化时遵循以下原则:
- 尽可能减少重复的字符串处理;
- 避免产生没有必要的中间数据;
- 对文本只进行必要的遍历;
- 保持原有查重结果和程序接口不变;
- 在优化性能的同时保证代码仍然具有良好的可读性。
完成修改以后,再使用相同的测试环境和测试数据进行性能分析。
优化后性能分析截图:

优化后的检测分析报告显示,总执行时间下降为 13.178 秒。调用链依然是 main.main → check_files → calculate_similarity,但 normalize_text 的非独占时间占比从 68.89% 降至 60.12%,说明该函数内部的字符串处理开销得到了有效削减;同时独占时间最高的函数变为 <listcomp>、_count_elements、str.isalnum 等,unicodedata.normalize 也进入了前五,反映出优化后统一全角/半角字符的处理更加集中和高效。
5.3 优化结果
两次实际性能分析的总体耗时如下:
| 版本 | 总耗时 |
|---|---|
| 优化前 | 17.367 s |
| 优化后 | 13.178 s |
性能提升比例约为:24.1%
因此,优化后程序的总体运行时间下降约 24.1%。
这说明此次针对程序热点进行的优化产生了较明显的效果,同时程序的输入输出格式以及查重功能保持不变。
六、单元测试
为了验证程序在不同情况下是否能够正确工作,我使用 Python 自带的 unittest 框架设计了 15 个单元测试。
测试不仅针对正常输入,还覆盖了边界情况、异常输入以及完整的文件输入输出流程。
本次主要测试内容如下:
| 测试 | 测试目的 |
|---|---|
| test_01_normalize_removes_spaces_and_punctuation | 验证空格和标点规范化 |
| test_02_normalize_unifies_full_width_characters | 验证全角字符转换 |
| test_03_ngram_counter_counts_bigrams | 验证二元 N-gram 统计 |
| test_04_ngram_counter_rejects_invalid_n | 验证非法 N 值处理 |
| test_05_identical_text_returns_one | 验证完全相同文本 |
| test_06_empty_text_returns_zero | 验证空文本 |
| test_07_completely_different_text_is_low | 验证完全不同文本 |
| test_08_assignment_example_is_similar | 验证题目示例 |
| test_09_added_content_stays_similar | 验证增加部分内容后的文本 |
| test_10_cosine_similarity_empty_vector | 验证空向量处理 |
| test_11_write_result_has_two_decimal_places | 验证答案保留两位小数 |
| test_12_missing_file_raises_error | 验证文件不存在异常 |
| test_13_check_files_end_to_end | 验证完整文件查重流程 |
| test_14_main_rejects_wrong_argument_count | 验证命令行参数数量错误 |
| test_15_main_creates_answer_file | 验证最终答案文件能够生成 |
运行测试:
python -m unittest discover -s tests -v
实际运行结果为:
Ran 15 tests
OK
说明 15 个测试全部通过。
单元测试全部通过截图:

七、测试覆盖率
为了进一步检查单元测试是否充分覆盖程序中的不同执行路径,我使用 coverage 对程序进行了分支覆盖率分析。
执行方式:
python -m coverage run --branch -m unittest discover -s tests -v
python -m coverage report -m
python -m coverage html
实际测试结果如下:
| 文件 | 覆盖率 |
|---|---|
| main.py | 77% |
| plagiarism_checker.py | 86% |
| tests/test_plagiarism_checker.py | 95% |
| 总体 | 89% |
本次测试共统计:
- 164 条语句
- 13 条未覆盖语句
- 32 个分支
- 总体覆盖率 89%
从结果可以看出,核心算法 plagiarism_checker.py 的覆盖率达到 86%,整个项目总体覆盖率达到 89%。
未覆盖的部分主要属于部分异常路径或较少出现的边界分支。对于本次作业规模而言,目前测试已经能够覆盖主要算法、正常输入、错误输入以及完整文件输入输出流程。
测试覆盖率报告截图:

八、异常处理
论文查重程序需要处理文件、参数以及文本数据,因此程序中设计了相应的异常处理机制。
8.1 命令行参数数量错误
程序规定必须输入三个参数:
- 原文
- 抄袭版论文
- 答案文件
如果参数数量不正确,程序会提示正确的使用方法,例如:
用法:python main.py <原文文件绝对路径> <抄袭版论文文件绝对路径> <答案文件绝对路径>
对应单元测试:test_14_main_rejects_wrong_argument_count
8.2 输入文件不存在
如果用户输入的原文路径或抄袭版路径不存在,程序不能继续读取文件。
因此程序会捕获文件读取异常,并给出错误信息,而不是直接异常退出。
对应单元测试:test_12_missing_file_raises_error
8.3 非法 N-gram 参数
生成 N-gram 特征时,参数必须满足算法要求。
如果传入非法参数,程序会主动拒绝该参数,以防产生无效结果。
对应单元测试:test_04_ngram_counter_rejects_invalid_n
8.4 空文本与空向量
论文文件可能为空,因此程序专门对空文本以及空特征向量进行了处理,避免出现除零等问题。
对应测试:test_06_empty_text_returns_zero、test_10_cosine_similarity_empty_vector
通过这些异常和边界处理,程序能够在输入不符合预期时给出合理结果或提示,提高程序的健壮性。
九、总结
本次论文查重项目从需求分析、算法设计、编码实现,到性能分析优化、单元测试与覆盖率检验,完整走了一遍个人软件开发流程。通过 Visual Studio 性能分析工具定位到 normalize_text 等文本处理函数是主要性能热点后,针对性地精简了字符串处理逻辑,使程序总运行时间从 17.367 秒下降到 13.178 秒,提升约 24.1%,同时保持了原有的查重准确性和接口不变。15 个单元测试全部通过,总体分支覆盖率达到 89%,覆盖了正常、边界、异常等多种输入情况,为程序的正确性和健壮性提供了较好的保障。
浙公网安备 33010602011771号