论文查重作业
作业 GitHub 链接:https://github.com/it-cc/3124004197
论文查重项目:从文本规范化到余弦相似度
一、题目分析
本次作业要求从命令行接收原文、抄袭版和答案文件三个路径,读取两个文件,计算重复率,并将结果以保留两位小数的浮点数写入答案文件。程序不能依赖网络,也不能读写题目之外的文件。
我选择 Python 3 和标准库实现,入口文件为 main.py。程序的核心计算放在 plagiarism_checker.py 中,命令行参数解析和文件 I/O 放在入口模块中,便于分别测试算法和外部接口。
二、PSP 表
以下时间单位为分钟。预估值在编码前填写,实际值在完成实现和测试后填写。
| Personal Software Process Stages | 阶段 | 预估耗时 | 实际耗时 |
|---|---|---|---|
| Planning | 计划 | 10 | 8 |
| Estimate | 估计这个任务需要多少时间 | 10 | 8 |
| Development | 开发 | 185 | 210 |
| Analysis | 需求分析(包括学习新技术) | 30 | 35 |
| Design Spec | 生成设计文档 | 15 | 18 |
| Design Review | 设计复审 | 10 | 12 |
| Coding Standard | 代码规范 | 10 | 8 |
| Design | 具体设计 | 25 | 28 |
| Coding | 具体编码 | 65 | 72 |
| Code Review | 代码复审 | 15 | 17 |
| Test | 测试、修改代码、提交修改 | 15 | 20 |
| Reporting | 报告 | 70 | 75 |
| Test Report | 测试报告 | 25 | 28 |
| Size Measurement | 计算工作量 | 10 | 8 |
| Postmortem & Process Improvement Plan | 事后总结和改进计划 | 35 | 39 |
| 合计 | 265 | 293 |
实际时间包含样例文件检查、单元测试和性能分析。
三、模块接口设计
项目由两个生产模块组成:
main.py:定义build_parser()和main(argv),负责接收三个命令行参数、调用计算模块、写答案文件和统一处理异常。plagiarism_checker.py:定义normalize_text()、tokenize()、term_frequencies()、cosine_similarity()、similarity_from_text()、calculate_similarity()和format_similarity()。
calculate_similarity() 是文件级接口,适合真实命令行调用;similarity_from_text() 是字符串级接口,适合单元测试;cosine_similarity() 接收两个稀疏频次向量,负责纯数学计算。
四、算法关键点
1. 文本规范化
输入先经过 Unicode NFKC 规范化和 casefold(),所以全角字符、英文大小写的差异不会被当成实质差异。程序使用 UTF-8 with BOM 方式读取,普通 UTF-8 文件也能正常读取。
2. 中文二元片段
中文没有使用外部分词库,而是将连续中文生成重叠二元片段。例如“今天是”会生成“今天”“天是”。修改一个字时,通常只影响相邻的少量片段,仍能保留相似文本的局部证据。英文单词、数字和带连字符的英文词作为一个 token。
3. 余弦相似度
对于两个词元频次向量 A 和 B,计算:
similarity = sum(A[i] * B[i]) /
(sqrt(sum(A[i]^2)) * sqrt(sum(B[i]^2)))
相同文本的向量方向完全相同,结果为 1.00;没有共同词元时结果为 0.00。任意一个文件没有有效词元时定义结果为 0.00,避免除零异常。
对于总输入长度 n,正则扫描、词频统计和向量内积均为线性或近似线性操作,时间复杂度为 O(n + t),额外空间复杂度为 O(t),其中 t 是词元总数。程序不会建立字符两两比较的 n*n 矩阵,因此适合题目的较长文本。
五、性能分析和改进
我使用 Python cProfile 对 benchmark.py 进行分析。基准数据由两段约 30 万字的中文文本组成,连续计算 20 次。运行命令为:
python benchmark.py > profile.txt
当前实测总耗时约为 1.40 秒,核心函数的累计耗时如下,完整结果在 profile.txt:
| 函数 | 调用次数 | 累计耗时 |
|---|---|---|
similarity_from_text |
20 | 1.378 s |
term_frequencies |
40 | 1.377 s |
tokenize |
40 | 1.186 s |
_tokens_from_cjk_run |
4,900,000 | 0.499 s |
分析表明主要开销在中文二元片段生成和正则处理。实现中的改进包括:使用 Counter 保存稀疏频次、只对两个向量的共同 key 做内积、预编译正则表达式和一次扫描生成二元片段。对于真实题目输入,单次运行远小于 5 秒,内存使用也只与词元数量线性相关。博客发布时可将 profile.txt 的前 12 行截图作为性能分析图。
六、单元测试
测试代码位于 tests/test_plagiarism_checker.py,使用 pytest 自动运行,共 16 个用例。测试采用白盒和黑盒相结合的方式:白盒覆盖规范化、分词、频次统计、余弦计算和格式化分支;黑盒验证完整命令行流程。
主要测试数据设计如下:
- 相同文本、完全不相同文本和空文本,覆盖相似度上下界及除零分支。
- 中文改字、增加句子、标点和空白变化,验证二元片段的容错性。
- 英文大小写、数字和重复词,验证不同 token 类型和频次统计。
- 临时目录中的三个文件,验证命令行读取、答案写入和两位小数格式。
- 缺失输入文件,验证错误信息、返回码以及不产生错误答案文件。
运行结果:
16 passed in 0.10s
TOTAL coverage: 100%
main.py: 100%
生产代码的分支已经由测试覆盖,包括类型保护和理论上的零范数保护。
对题目目录中的文件也进行了命令行验收:
| 输入文件 | 输出重复率 |
|---|---|
orig_0.8_add.txt |
0.90 |
七、异常处理
程序将异常集中在 main() 中处理,失败时向标准错误输出中文错误信息,并返回 1,不写入不可信的答案。
FileNotFoundError和其他OSError:输入路径不存在、没有权限或答案路径不可写。测试test_command_line_reports_missing_input_without_crashing对应输入文件缺失场景。UnicodeError:输入不是可解码的 UTF-8 文件。这样程序会明确失败,而不是静默计算错误结果。TypeError:核心接口收到非字符串文本。测试中的类型保护保证模块接口契约明确。ValueError:格式化阶段遇到 NaN、无穷大或不在0~1范围内的结果。test_invalid_output_value_is_rejected覆盖该场景。- 空文件:不是异常,而是定义为
0.00,由cosine_similarity()的空向量分支处理,测试test_empty_documents_have_zero_similarity覆盖该场景。
八、总结和改进计划
本版本已经满足命令行输入输出、两位小数、单元测试和本地样例验收要求。当前算法属于词元统计相似度,能够处理字符级增删改,但不能理解同义词,例如“星期天”和“周天”不会被当作完全相同的词。后续可以在不改变命令行接口的前提下增加可选中文分词或同义词归一化,并继续用回归测试保证结果稳定。
浙公网安备 33010602011771号