第一次个人编程作业

论文查重算法——个人项目作业

学号: 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仓库:df4b5bd02fa0bd923b9a140e803049ed

一、项目需求

本次个人项目要求设计并实现一个论文查重程序。

程序接收三个命令行参数:

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.pyplagiarism_checker.py 两个模块,核心算法不依赖 sys.argv,从而可以脱离命令行直接被单元测试调用。

3.2 程序执行流程

整体处理流程:

开始
 ↓
读取命令行参数(共 3 个)
 ↓
检查参数数量是否正确
 ↓
读取原文与抄袭版文本(utf-8)
 ↓
文本规范化(NFKC 全半角统一 + 去空白标点)
 ↓
二元字符 N-gram 特征统计
 ↓
余弦相似度计算
 ↓
结果保留两位小数
 ↓
写入答案文件
 ↓
结束

3.3 文本预处理

原始论文中可能存在空格、换行、标点以及全角字符等差异,直接字符串比较会把这些无关差异也算成“不同”。因此正式计算前先做规范化:

  1. unicodedata.normalize("NFKC", text) 一次性把全角字符转半角、兼容字符统一;
  2. 逐字符判断,跳过所有空白字符;
  3. 只保留 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

性能分析结果截图:

cfd119d94e15f89fa48da68393a7f5a2
c2745db479ad9f5addca813c3767923d

从调用栈看,热点集中在:

main -> check_files -> calculate_similarity -> normalize_text / ngram_counter

其中 normalize_text 耗时约 0.127 秒,占总时间约 61%,是明显的性能热点;其次是 ngram_counterunicodedata.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 对不存在文件返回错误码

1f872ac79e38f242dee7928fe755133f

七、测试覆盖率

为检查测试是否覆盖了主要执行路径,我用 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%,已经覆盖了正常输入、空输入、异常输入和完整文件流程。

13e9748a8e8ea3588efa5f34ba019502

八、异常处理

程序对文件、参数、文本数据设计了相应的异常处理。

8.1 命令行参数数量错误

必须输入原文、抄袭版、答案三个参数。数量不对时打印用法提示并返回退出码 1。对应测试:test_15_main_rejects_wrong_argument_count

8.2 输入文件不存在

原文或抄袭版路径不存在时,捕获 FileNotFoundError,打印错误信息而非直接崩溃。对应测试:test_13_missing_file_raises_errortest_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_zerotest_11_cosine_similarity_empty_vectortest_06_ngram_short_text_returns_empty

九、总结

本次论文查重项目从需求分析、算法设计、编码实现,到性能分析、单元测试与覆盖率检验,完整走了一遍个人软件开发流程。核心采用“NFKC 规范化 + 二元字符 N-gram + 余弦相似度”的方案,不依赖第三方库,部署简单;针对文本规范化这一热点做了减少中间字符串的优化,约 6 万字文本单次查重约 0.13 秒,满足 5 秒时限。17 个单元测试全部通过,总体分支覆盖率 95%,核心模块 98%,覆盖了正常、边界、异常等多种输入。

posted @ 2026-09-15 22:05  沈彦丞  阅读(4)  评论(0)    收藏  举报