第一次个人编程作业
GitHub 项目地址:linyanzhi591-afk/linyanzhi591-afk
第一次个人编程作业:论文查重
一、项目说明
本次作业使用 Python 3 实现一个论文查重程序。程序从命令行接收原文文件、抄袭版文件和答案文件的绝对路径,读取前两份文本,计算重复率,并将结果写入答案文件。
项目目录为 3124004184/,入口文件为 main.py。
运行方式:
python main.py [原文文件绝对路径] [抄袭版文件绝对路径] [答案文件绝对路径]
例如:
python main.py C:\tests\orig.txt C:\tests\orig_add.txt C:\tests\ans.txt
答案文件输出 0 到 1 之间的浮点数,精确到小数点后两位,例如:
0.71
Python 项目仅使用标准库,requirements.txt 用于说明不需要额外的运行依赖。
二、需求分析
程序需要满足以下要求:
- 从命令行读取三个文件的绝对路径。
- 读取原文和抄袭版文本。
- 对增删改后的文本计算重复率。
- 将结果写入指定答案文件。
- 输出格式为保留两位小数的浮点数。
- 输入文件不存在、参数错误或答案文件无法写入时,程序不能异常退出。
- 程序不连接网络,不读写指定文件之外的其他业务文件。
三、算法设计
3.1 总体思路
程序将文本相似度转化为词频向量之间的余弦相似度。处理过程如下:
读取原文和抄袭版
↓
过滤标点、空白和无关字符
↓
中文按单字切分,英文单词和数字按 token 切分
↓
统计每个 token 的出现次数
↓
构造两个词频向量
↓
计算余弦相似度
↓
格式化为两位小数并写入答案文件
3.2 文本切分
中文文本没有天然空格,程序将每个中文字符作为一个 token。例如:
今天是晴天
会切分为:
今、天、是、晴、天
英文单词和数字作为完整 token,例如:
Python 3.12
会切分为:
python、3.12
英文统一转换为小写,使 Python 和 python 被视为相同内容。标点和空白不会参与相似度计算。
3.3 余弦相似度
设原文词频向量为 A,抄袭版词频向量为 B,重复率计算公式为:
similarity = A · B / (|A| × |B|)
其中:
A · B是两个向量的点积;|A|和|B|是两个向量的模长;- 两份文本完全相同时结果为
1.00; - 没有共同 token 或任意一份文本为空时结果为
0.00。
这种方法能够处理文本中的增加、删除和部分修改,并且不依赖联网服务或第三方分词接口。
3.4 复杂度分析
设两份文本的 token 数量分别为 n 和 m,不同 token 数量为 k。
- 时间复杂度:
O(n + m + k) - 空间复杂度:
O(k)
程序只进行有限次文本扫描和词频统计,适合本题的文件规模和 5 秒运行限制。
四、模块和接口设计
程序采用函数式模块划分,没有引入不必要的类层次。
| 函数 | 作用 |
|---|---|
tokenize(text) |
将文本转换为 token 列表 |
vector_similarity(left, right) |
根据两个 token 序列计算余弦相似度 |
plagiarism_rate(original_text, copied_text) |
对外提供重复率计算接口 |
read_text(path_text) |
读取 UTF-8 文本文件 |
write_result(path_text, rate) |
将两位小数结果写入答案文件 |
main(arguments) |
处理命令行参数并组织完整流程 |
其中 plagiarism_rate 是计算模块的主要接口,文件读写和命令行处理与计算逻辑分离,便于单元测试。
五、代码规范与质量检查
代码遵循以下规范:
- 使用有意义的英文函数名和变量名;
- 函数使用类型标注;
- 关键函数包含文档字符串;
- 输入输出异常通过自定义异常类型表达;
- 主程序入口使用
if __name__ == "__main__"; - 不使用网络请求、系统命令或与题目无关的文件读写。
已完成的本地检查:
python -m unittest discover -s tests -v
python -m compileall -q .
13 个单元测试全部通过,Python 文件编译检查通过。
如需补充课程要求的代码质量工具截图,可运行:
python -m pip install pylint
pylint main.py

六、性能分析与改进
6.1 性能分析
使用 Python 标准库 cProfile 对大规模重复文本进行本地性能检查,主要热点集中在:
tokenize:正则表达式扫描文本;vector_similarity:建立词频统计并计算向量;plagiarism_rate:组织两个步骤。
性能分析结果符合算法预期,没有发现重复扫描原始文本或不必要的嵌套遍历。
6.2 性能改进
初始设计中,程序可能为每个 token 反复查找其出现次数。实现时改为使用 collections.Counter 一次性完成词频统计,并使用集合交集只计算两个文本共有的 token:
common_tokens = left_counter.keys() & right_counter.keys()
这样减少了无效查找,使计算复杂度保持在线性范围内,同时避免保存完整的稠密向量。

七、单元测试
测试文件为 3124004184/tests/test_main.py,共 13 个测试用例,超过题目要求的 10 个。
测试覆盖内容如下:
| 测试场景 | 验证内容 |
|---|---|
| 中文、英文和数字切分 | 验证基本 token 规则 |
| 两份文本完全相同 | 结果应为 1.00 |
| 两份文本完全不同 | 结果应为 0.00 |
| 一份文本为空 | 验证空输入处理 |
| 两份文本都为空 | 验证除零保护 |
| 重复 token 频率不同 | 验证词频会影响结果 |
| 英文大小写不同 | 验证大小写归一化 |
| 标点不同 | 验证标点不会影响结果 |
| 数字 token | 验证数字处理 |
| 输入文件不存在 | 验证输入异常 |
| 答案文件格式 | 验证输出两位小数 |
| 命令行参数数量错误 | 验证参数异常 |
| 相似度边界 | 验证结果在 [0, 1] 范围内 |
运行测试:
python -m unittest discover -s tests -v
测试结果:
Ran 13 tests in 0.025s
OK
测试数据采用白盒测试和边界值测试相结合的方式,分别覆盖正常流程、空输入、完全相同、完全不同、非法路径和非法参数等分支。

八、异常处理
8.1 参数数量错误
当命令行参数不是 3 个时,程序输出使用说明并返回非零状态码,避免使用不存在的参数导致异常。
对应测试:test_main_rejects_wrong_argument_count
8.2 输入文件不存在或无法读取
read_text 在读取前检查文件是否存在,并捕获文件系统异常和编码异常,统一转换为 InputFileError。
对应测试:test_missing_input_raises
8.3 答案文件无法写入
write_result 捕获答案文件创建和写入过程中的 OSError,转换为 OutputFileError,由主函数统一输出错误信息。
8.4 空文本
当任意一份文本没有有效 token 时,函数返回 0.0,避免余弦相似度计算中的除零异常。
对应测试:test_empty_text_is_zero、test_both_empty_texts_are_zero
九、GitHub 版本管理
本地项目按照功能划分进行了两次提交:
| 提交信息 | 内容 |
|---|---|
完成论文查重计算模块 |
完成 main.py 和 requirements.txt |
补充测试文档和性能记录 |
补充单元测试、README、PSP 和性能说明 |
最终代码已上传到 GitHub 仓库的 3124004184/ 文件夹中。
十、PSP 表格
| 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| 需求分析 | 30 | 15 |
| 设计文档 | 30 | 20 |
| 设计复审 | 15 | 10 |
| 代码规范 | 10 | 5 |
| 具体设计 | 30 | 20 |
| 具体编码 | 60 | 45 |
| 代码复审 | 20 | 10 |
| 测试与修改 | 60 | 25 |
| 测试报告 | 30 | 10 |
| 工作量度量 | 10 | 5 |
| 事后总结与过程改进 | 20 | 5 |
| 合计 | 315 | 170 |
十一、总结
本项目完成了论文原文和抄袭版文件的命令行读取、文本预处理、词频统计、余弦相似度计算和结果文件输出。通过模块化函数设计,计算逻辑可以脱离文件系统单独测试;通过 Counter 和集合交集降低了重复查找开销;通过 13 个测试用例覆盖了主要正常分支和异常分支。
提交前还需要将实际的代码质量分析、性能分析和测试覆盖率截图补充到对应位置,并使用课堂正式样例验证 18 个测试点。

浙公网安备 33010602011771号