第一次个人编程作业
论文查重算法——个人项目作业
学号: 3124004223
姓名: 沈彦丞
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 独立设计并实现一个论文查重程序,接收原文与抄袭版两个文件路径,输出两篇文本的相似度(0~1,保留两位小数)到答案文件;并完整走通 PSP 计划、编码、性能分析、单元测试、覆盖率分析这一整套个人软件开发流程,最终以一篇 Markdown 随笔记录全过程 |
GitHub 项目地址: https://github.com/s-yc1/s-yc1
Giehub仓库:
一、项目需求
本次个人项目要求设计并实现一个论文查重程序。
程序接收三个命令行参数:
python main.py <原文文件绝对路径> <抄袭版论文文件绝对路径> <答案文件绝对路径>
其中:
- 第一个参数为原文文件;
- 第二个参数为经过增、删、改后的论文文件;
- 第三个参数为查重结果输出文件。
程序读取两个文本文件,计算两篇文本的相似度,并将最终结果以浮点数形式写入答案文件,结果保留两位小数。例如:
0.80
答案文件中只保存查重结果,不添加“相似度”“重复率”等其他文字,以保证符合自动评测要求。
题目给出的样例:
- 原文:今天是星期天,天气晴,今天晚上我要去看电影。
- 抄袭版:今天是周天,天气晴朗,我晚上要去看电影。
二、PSP 表格
在正式开发前,我按照 PSP(Personal Software Process)对整个项目的开发过程进行了时间规划。
| PSP2.1 | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 35 |
| Estimate | 估计任务需要多少时间 | 20 | 26 |
| Development | 开发 | 800 | 766 |
| Analysis | 需求分析(包括学习新技术) | 60 | 77 |
| Design Spec | 生成设计文档 | 80 | 75 |
| Design Review | 设计复审 | 40 | 52 |
| Coding Standard | 代码规范 | 20 | 24 |
| Design | 具体设计 | 120 | 111 |
| Coding | 具体编码 | 220 | 204 |
| Code Review | 代码复审 | 60 | 65 |
| Test | 测试、修改代码、提交修改 | 150 | 158 |
| Reporting | 报告 | 90 | 89 |
| Test Report | 测试报告 | 50 | 50 |
| Size Measurement | 计算工作量 | 20 | 23 |
| Postmortem & Process Improvement Plan | 事后总结并提出改进计划 | 30 | 33 |
| 合计 | 1590 | 1022 |
说明:“预计耗时”是动手前的估计;“实际耗时”请在完成后按你真实花在各阶段的时间填写,合计用加法现算。
三、程序设计与实现
3.1 项目结构
最终项目的主要结构如下:
3124004223/
│
├── main.py # 程序入口,接收三个命令行参数
├── plagiarism_checker.py # 文本预处理、N-gram 特征、余弦相似度、文件读写
├── requirements.txt # 运行依赖(本项目仅用标准库)
├── requirements-dev.txt # 开发依赖(coverage)
├── run_tests.bat # 一键运行单元测试
├── run_coverage.bat # 一键运行测试并生成分支覆盖率报告
│
├── samples/ # 自测样例文本
│ ├── orig.txt
│ └── orig_add.txt
│
└── tests/
└── test_plagiarism_checker.py # 单元测试(17 个用例)
各文件职责:把“命令行参数处理”和“查重算法”拆成 main.py 与 plagiarism_checker.py 两个模块,核心算法不依赖 sys.argv,从而可以脱离命令行直接被单元测试调用。
3.2 程序执行流程
整体处理流程:
开始
↓
读取命令行参数(共 3 个)
↓
检查参数数量是否正确
↓
读取原文与抄袭版文本(utf-8)
↓
文本规范化(NFKC 全半角统一 + 去空白标点)
↓
二元字符 N-gram 特征统计
↓
余弦相似度计算
↓
结果保留两位小数
↓
写入答案文件
↓
结束
3.3 文本预处理
原始论文中可能存在空格、换行、标点以及全角字符等差异,直接字符串比较会把这些无关差异也算成“不同”。因此正式计算前先做规范化:
- 用
unicodedata.normalize("NFKC", text)一次性把全角字符转半角、兼容字符统一; - 逐字符判断,跳过所有空白字符;
- 只保留 Unicode 类别为
L*(字母,含中文汉字)和N*(数字)的字符,丢弃标点与符号。
例如:
今天是星期天,天气晴。
和
今天是星期天 天气晴
经过规范化后都变成 今天是星期天天气晴,标点与空格不再干扰相似度。
3.4 N-gram 文本特征
只比较单个字符无法体现上下文,因此用二元字符 N-gram 提取相邻特征。例如文本“今天天气很好”在 n=2 时得到:
今天、天天、天气、气很、很好
再用 collections.Counter 统计每个二元组出现的次数,就把文本变成了稀疏的整数向量。这种方法不依赖任何中文分词库,部署简单,对论文经过一定增删改后仍有较好的区分能力。
3.5 余弦相似度
得到两个文本的特征向量后,用余弦相似度衡量它们的接近程度:
$$
\mathrm{similarity}(A,B)=\frac{A \cdot B}{|A|,|B|}
$$
相似度越接近 1 说明越相似,越接近 0 说明差异越大。最终结果裁剪到 0~1,并保留两位小数写入答案文件;当向量为空或点积为 0 时直接返回 0,避免除零。
四、程序接口设计
主要函数按功能划分如下。
文本处理:
normalize_text(text) # 规范化
ngram_counter(text, n=2) # n-gram 词频统计
相似度计算:
cosine_similarity(vec_a, vec_b) # 两个 Counter 向量的余弦相似度
calculate_similarity(text1, text2) # 对外主接口
文件与流程:
read_text_file(path) # 读文件
write_result(value, path) # 写两位小数结果
check_files(orig, plag, answer) # 完整查重流程
程序入口: main.py 接收原文、抄袭版、答案三个路径,调用 check_files 完成计算,并在参数数量错误、文件不存在等情况给出提示、返回非零退出码。
五、性能分析与优化
5.1 性能分析
完成第一版后,我使用 Python 自带的 cProfile 性能分析工具,用约 6 万字的较大测试文本运行程序,观察各函数耗时,定位热路径。运行命令:
python -m cProfile -s cumulative main.py big_orig.txt big_plag.txt big_ans.txt
性能分析结果截图:


从调用栈看,热点集中在:
main -> check_files -> calculate_similarity -> normalize_text / ngram_counter
其中 normalize_text 耗时约 0.127 秒,占总时间约 61%,是明显的性能热点;其次是 ngram_counter 与 unicodedata.category。这说明文本规范化阶段的字符串处理开销最大。
5.2 优化思路
针对热点主要做了三点:
- 全角/半角统一只用一次
unicodedata.normalize("NFKC", ...),不再逐字符反复转换; - 规范化用单次遍历 +
list.append收集字符,最后一次"".join,避免循环里反复+=产生大量中间字符串; - 余弦点积只遍历较短的一方(
vec_a),用dict.get取共有 key,减少哈希查找次数。
5.3 优化结果
在本机对约 6 万字的文本进行测试,整个查重流程(读文件 + 规范化 + N-gram + 余弦 + 写文件)在 cProfile 下耗时约 0.2 秒,远小于作业要求的 5 秒时限,内存占用也远低于 2048 MB,性能余量充足。
| 指标 | 数值 |
|---|---|
| 单次查重耗时 | 约 0.13 s |
| 作业时间限制 | 5 s |
六、单元测试
为了验证程序在各种情况下都能正确工作,我使用 Python 自带的 unittest 框架设计了 17 个单元测试,覆盖正常输入、边界情况与异常输入。
运行方式:
python -m unittest discover -s tests -v
实际运行结果:
Ran 17 tests in 0.12s
OK
主要测试用例如下:
| 测试函数 | 测试目的 |
|---|---|
| test_01_normalize_removes_spaces_and_punctuation | 验证空格与标点被规范化 |
| test_02_normalize_unifies_full_width_characters | 验证全角字符转半角 |
| test_03_normalize_empty_string | 验证空串返回空串 |
| test_04_ngram_counter_counts_bigrams | 验证二元 N-gram 统计 |
| test_05_ngram_counter_rejects_invalid_n | 验证非法 n 值抛异常 |
| test_06_ngram_short_text_returns_empty | 验证短文本返回空向量 |
| test_07_identical_text_returns_one | 验证完全相同文本得 1.0 |
| test_08_empty_text_returns_zero | 验证空文本得 0 |
| test_09_completely_different_text_is_low | 验证完全不同文本相似度很低 |
| test_10_assignment_example_is_similar | 验证题目示例改写后仍高相似 |
| test_11_cosine_similarity_empty_vector | 验证空向量不除零 |
| test_12_write_result_has_two_decimal_places | 验证答案只写两位小数 |
| test_13_missing_file_raises_error | 验证文件不存在抛异常 |
| test_14_check_files_end_to_end | 验证完整文件查重流程 |
| test_15_main_rejects_wrong_argument_count | 验证参数数量错误被拒绝 |
| test_16_main_creates_answer_file | 验证 main 正确生成答案文件 |
| test_17_main_missing_file_returns_error | 验证 main 对不存在文件返回错误码 |

七、测试覆盖率
为检查测试是否覆盖了主要执行路径,我用 coverage 做了分支覆盖率分析:
python -m coverage run --branch -m unittest discover -s tests -v
python -m coverage report -m
python -m coverage html
实际结果:
| 文件 | 覆盖率 |
|---|---|
| main.py | 71% |
| plagiarism_checker.py | 98% |
| tests/test_plagiarism_checker.py | 98% |
| 总体 | 95% |
未覆盖的部分主要是 main.py 中极少触发的 ValueError/OSError 兜底分支;核心算法 plagiarism_checker.py 覆盖率达 98%,已经覆盖了正常输入、空输入、异常输入和完整文件流程。

八、异常处理
程序对文件、参数、文本数据设计了相应的异常处理。
8.1 命令行参数数量错误
必须输入原文、抄袭版、答案三个参数。数量不对时打印用法提示并返回退出码 1。对应测试:test_15_main_rejects_wrong_argument_count。
8.2 输入文件不存在
原文或抄袭版路径不存在时,捕获 FileNotFoundError,打印错误信息而非直接崩溃。对应测试:test_13_missing_file_raises_error、test_17_main_missing_file_returns_error。
8.3 非法 N-gram 参数
n 必须 ≥ 1,否则主动抛 ValueError,避免产生无意义结果。对应测试:test_05_ngram_counter_rejects_invalid_n。
8.4 空文本与空向量
论文文件可能为空,或规范化后长度小于 n。此时直接返回相似度 0,不参与余弦计算,避免除零。对应测试:test_08_empty_text_returns_zero、test_11_cosine_similarity_empty_vector、test_06_ngram_short_text_returns_empty。
九、总结
本次论文查重项目从需求分析、算法设计、编码实现,到性能分析、单元测试与覆盖率检验,完整走了一遍个人软件开发流程。核心采用“NFKC 规范化 + 二元字符 N-gram + 余弦相似度”的方案,不依赖第三方库,部署简单;针对文本规范化这一热点做了减少中间字符串的优化,约 6 万字文本单次查重约 0.13 秒,满足 5 秒时限。17 个单元测试全部通过,总体分支覆盖率 95%,核心模块 98%,覆盖了正常、边界、异常等多种输入。

浙公网安备 33010602011771号