第一次个人编程作业
github 链接:https://github.com/MWQ111/paper-check
论文查重程序 —— 软件工程作业
姓名:马文青 学号:3124004255 日期:2026-09-15
一、PSP 时间记录表
预估耗时
| 阶段 | 预估耗时(min) |
|---|---|
| Planning · Estimate | 20 |
| Development · Analysis | 20 |
| Development · Design Spec | 30 |
| Development · Design Review | 15 |
| Development · Coding Standard | 10 |
| Development · Design | 30 |
| Development · Coding | 60 |
| Development · Code Review | 15 |
| Development · Test | 40 |
| Reporting · Test Report | 20 |
| Reporting · Size Measurement | 10 |
| Reporting · Postmortem | 30 |
| 合计 | 380 |
实际耗时
| 阶段 | 实际耗时(min) |
|---|---|
| Planning · Estimate | 18 |
| Development · Analysis | 28 |
| Development · Design Spec | 32 |
| Development · Design Review | 18 |
| Development · Coding Standard | 12 |
| Development · Design | 45 |
| Development · Coding | 100 |
| Development · Code Review | 25 |
| Development · Test | 45 |
| Reporting · Test Report | 28 |
| Reporting · Size Measurement | 12 |
| Reporting · Postmortem | 22 |
| 合计 | 385 |
完整 PSP 记录见仓库 PSP.md
二、算法设计与实现
2.1 整体流程
程序处理流程为:读取 → 清洗 → 分词 → 构建 TF-IDF 向量 → 余弦相似度 → 输出。
读原文 ─┐
├─→ 清洗 ─→ 分词 ─→ TF-IDF ─→ 余弦相似度 ─→ 输出
读抄袭 ─┘
2.2 代码组织
程序共 8 个核心函数,按职责分为三层:
- IO 层:
read_file(含 UTF-8 → GBK 回退)、write_answer(两位小数格式化) - 预处理层:
clean_text(正则清洗 + 小写化)、tokenize(生成器惰性产出) - 算法层:
build_tfidf_vector、cosine_similarity、calculate_similarity - 入口:
main(命令行参数解析 + 异常处理)
2.3 关键算法
分词策略:
| 语言 | 策略 |
|---|---|
| 英文 | 按整词切分,正则 [a-z]+ |
| 数字 | 按数字串切分,正则 [0-9]+ |
| 中文 | 按单字切分 |
| 中文补充 | 相邻两字组成 2-gram |
TF-IDF:
- TF:词频归一化,
词出现次数 / 文档词元总数 - IDF:
ln((N + 1) / (df + 1)) + 1,其中 N = 2(双文档),加 1 平滑避免零权重
余弦相似度:
similarity = (A · B) / (|A| × |B|)
2.4 独到之处
① 主动发现并处理了需求矛盾
作业要求同时满足「去除标点、空白」与「英文按单词切分」。若真删除空白,Hello World 会合并为 helloworld,英文分词完全失效。实测反例:
- 修复前:
['helloworldiamhere'] - 修复后:
['hello', 'world', 'i', 'am', 'here']
因此保留空白作为英文单词边界 —— 中文分词基于正则提取,空白会被自然忽略,不受影响。此决策已在代码 docstring 和 README 中说明。
② 生成器惰性求值
tokenize 用 yield 产出词元,避免构造中间列表,长文本下显著降低内存占用。
③ 双文档 IDF
两篇文档共同计算 IDF,而非单文档 TF,区分能力更强。
④ UTF-8 → GBK 自动回退
兼容 Windows 下常见的 GBK 编码文本。
三、性能改进
3.1 分析工具
使用 Python cProfile + snakeviz 可视化性能。
3.2 性能分析图

3.3 结论
- 样例数据(22 万字符)处理耗时 0.13 秒,远低于 5 秒限制
- cProfile 显示业务代码
calculate_similarity仅占 0.17ms,说明瓶颈已不在算法层 - 主要开销在 Python 解释器启动(编译字节码),属解释型语言固有成本
3.4 优化点
- 用生成器避免中间列表
cosine_similarity遍历较短向量,减少查表次数- 用
Counter替代手写循环统计词频
四、单元测试
4.1 测试概况
共 39 个测试用例,分 9 个测试类:
| 测试类 | 用例数 | 覆盖内容 |
|---|---|---|
| TestCleanText | 4 | 标点去除、小写化、空白保留、空串 |
| TestTokenize | 6 | 中文单字+2gram、英文整词、数字、生成器、空文本 |
| TestCalculateSimilarity | 12 | 相同/不同/空/部分相似、对称性、取值范围 |
| TestCosineSimilarity | 5 | 空向量、相同向量、正交、已知解析解 |
| TestBuildTfidfVector | 2 | 空词元、IDF 加权 |
| TestReadFile | 3 | UTF-8、GBK 回退、文件不存在 |
| TestWriteAnswer | 3 | 两位小数、进位、边界 0/1 |
| TestMainCommandLine | 3 | 参数错误、文件不存在、端到端 |
| TestPerformance | 1 | 长文本 5 秒内完成 |
4.2 测试运行结果
Ran 39 tests in 0.147s
OK
4.3 覆盖率

- 整体覆盖率:96%
- 核心代码
main.py:90%
4.4 未覆盖行说明
未覆盖的 8 行均属合理情况:
cosine_similarity中norm == 0的兜底返回(TF-IDF 权重恒为正,数学上不可达)PermissionError与OSError两个异常分支(需构造无权限文件或磁盘错误,单元测试难以稳定复现)if __name__ == '__main__'入口(模块以 import 加载时不执行)
这些分支属防御性代码与运行入口,不影响功能验证。
五、异常处理
程序针对 4 类异常做了处理,每类都有对应测试:
| 异常 | 触发场景 | 处理 | 测试用例 |
|---|---|---|---|
FileNotFoundError |
输入文件不存在 | 打印错误、返回退出码 1 | test_main_with_missing_file_returns_error |
PermissionError |
文件无读/写权限 | 打印错误、返回退出码 1 | 未覆盖(需改文件 ACL) |
UnicodeDecodeError |
UTF-8 解码失败 | 回退 GBK 重试 | test_read_file_falls_back_to_gbk |
| 参数数量错误 | 命令行参数 ≠ 3 | 打印用法、返回退出码 2 | test_main_with_wrong_argument_count |
异常测试样例
def test_main_with_missing_file_returns_error(self):
"""输入文件不存在时应返回退出码 1,而不是抛出异常。"""
missing = os.path.join(TEST_DATA_DIR, '不存在的文件.txt')
with tempfile.TemporaryDirectory() as temp_dir:
answer = os.path.join(temp_dir, 'ans.txt')
captured = io.StringIO()
with redirect_stdout(captured):
exit_code = main.main([missing, missing, answer])
self.assertEqual(exit_code, 1)
self.assertFalse(os.path.exists(answer))
self.assertIn('文件不存在', captured.getvalue())
异常场景说明
- 文件不存在:用户传错路径,程序不崩,打印友好提示并返回 1
- 权限不足:文件被占用或无权限,同样优雅退出
- 编码错误:Windows 下文件可能是 GBK,程序自动回退
- 参数错误:用户忘了传参数,打印用法提示并返回 2
六、项目总结
技术难点
清洗与英文分词的矛盾 —— 需求同时要求「去除空白」和「英文按词切分」,两者不可同时满足。我保留空白作为单词边界,并通过实测验证了删空格的错误后果。
中文分词的准确度 —— 无第三方库(如 jieba)可用,采用单字 + 2-gram 的折中方案。
编码兼容 —— UTF-8 → GBK 自动回退,兼容 Windows 常见文本文件。
收获
- 实践了 TF-IDF + 余弦相似度这一经典文本相似度算法
- 学会用
coverage量化测试质量,用cProfile/snakeviz定位性能瓶颈 - 理解了「防御性代码」与「不可达分支」对覆盖率统计的影响
- 全程使用 Git 管理,8 次提交记录开发过程