第一次个人编程作业

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_vectorcosine_similaritycalculate_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 中说明。

② 生成器惰性求值

tokenizeyield 产出词元,避免构造中间列表,长文本下显著降低内存占用。

③ 双文档 IDF

两篇文档共同计算 IDF,而非单文档 TF,区分能力更强。

④ UTF-8 → GBK 自动回退

兼容 Windows 下常见的 GBK 编码文本。


三、性能改进

3.1 分析工具

使用 Python cProfile + snakeviz 可视化性能。

3.2 性能分析图

屏幕截图 2026-09-15 142053

3.3 结论

  • 样例数据(22 万字符)处理耗时 0.13 秒,远低于 5 秒限制
  • cProfile 显示业务代码 calculate_similarity 仅占 0.17ms,说明瓶颈已不在算法层
  • 主要开销在 Python 解释器启动(编译字节码),属解释型语言固有成本

3.4 优化点

  1. 用生成器避免中间列表
  2. cosine_similarity 遍历较短向量,减少查表次数
  3. 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 覆盖率

屏幕截图 2026-09-15 141936

  • 整体覆盖率:96%
  • 核心代码 main.py90%

4.4 未覆盖行说明

未覆盖的 8 行均属合理情况:

  1. cosine_similaritynorm == 0 的兜底返回(TF-IDF 权重恒为正,数学上不可达)
  2. PermissionErrorOSError 两个异常分支(需构造无权限文件或磁盘错误,单元测试难以稳定复现)
  3. 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 次提交记录开发过程

仓库地址

https://github.com/MWQ111/paper-check

posted @ 2026-09-15 15:35  mwq111  阅读(18)  评论(0)    收藏  举报