第一次个人编程作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702
这个作业的目标 实现一个论文查重程序,熟悉一个项目开发流程

作业GitHub链接:https://github.com/666F1E88/3224004347


一、PSP预估表


PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10 20
-Estimate 估计这个任务需要多少时间 10 20
Development 开发 240 300
-Analysis 需求分析 (包括学习新技术) 30 45
-Design Spec 生成设计文档 20 20
-Design Review 设计复审 15 15
-Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 15
-Design 具体设计 25 30
-Coding 具体编码 80 100
-Code Review 代码复审 20 25
-Test 测试(自我测试,修改代码,提交修改) 40 50
Reporting 报告 60 70
-Test Repor 测试报告 20 20
-Size Measurement 计算工作量 15 15
-Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 25 35
合计 310 390

二、项目概述


本项目实现了一个基于字符级最长公共子序列的论文查重程序,用Python编写。程序通过命令行参数接收三个文件路径:原文文件、抄袭版论文文件、答案输出文件,计算两段文本的相似度,并将结果保留两位小数写入答案文件。


2.1 运行方式

python main.py orig.txt orig_0.8_add.txt ans.txt

2.2 项目文件结构

paper_similarity_check/
  main.py              # 核心程序
  test_main.py         # 单元测试
  requirements.txt     # 依赖清单
  README.md            # 项目说明
  orig.txt             # 原文
  orig_0.8_add.txt     # 抄袭版——增加噪声
  orig_0.8_del.txt     # 抄袭版——删除内容
  orig_0.8_dis_1.txt   # 抄袭版——打乱1
  orig_0.8_dis_10.txt  # 抄袭版——打乱10
  orig_0.8_dis_15.txt  # 抄袭版——打乱15

三、计算模块接口的设计与实现过程


3.1 代码组织


项目核心代码集中在main.py,包含四个函数:

函数名 功能
read_file(path) 读取文件内容
preprocess(text) 去除标点和空白字符
calculate_similarity(orig, orig_add) 计算两段文本的相似度
main() 解析命令行参数

3.2 函数之间关系

main()
-> read_file(orig_path)       读取原文
-> read_file(orig_add_path)   读取抄袭版
-> calculate_similarity()
  -> preprocess()             预处理
  -> LCSseq.similarity()      计算LCS
-> 写入答案文件

3.3 算法关键


  1. 核心思路:将两段文本视为字符序列,计算最长公共子序列的长度,然后用LCS长度 / 较长序列长度作为相似度。

  1. 选择LCS原因
  • 论文抄袭通常是在原文基础上增删改,LCS能有效捕捉这些变化。
  • 相比于余弦相似度,LCS对词序敏感,区分度更高。
  • 相比编辑距离,LCS对插入和删除的处理更公平,不会因为原文或抄袭版长度差异过大而失真。

  1. 独到之处
  • 使用rapidfuzz库的LCSseq.similarity,底层是C++实现,性能远超Python的difflib.SequenceMatcher
  • 预编译正则表达式,避免重复编译。
  • 对空文本、纯标点文本做保护,返回0。

3.4 关键函数流程


graph LR; 从命令行获取文件路径 --> 读取原文和抄袭版内容; 读取原文和抄袭版内容 --> 对文本分别预处理; 对文本分别预处理 --> 若有空则返回0.0; 若有空则返回0.0 --> 计算LCS长度; 计算LCS长度 --> 除以较长序列长度得到相似度; 除以较长序列长度得到相似度 --> 保留两位小数写入答案文件;

3.5 功能验证


用提供的5个抄袭版文件测试程序,运行命令如下:

python main.py orig.txt orig_0.8_add.txt ans.txt
python main.py orig.txt orig_0.8_del.txt ans.txt
python main.py orig.txt orig_0.8_dis_1.txt ans.txt
python main.py orig.txt orig_0.8_dis_10.txt ans.txt
python main.py orig.txt orig_0.8_dis_15.txt ans.txt

2a58239b-a018-4b6f-8029-9dcaf483d87a


四、计算模块接口部分的性能改进


4.1 性能分析工具与性能分析图


  • 使用Python自带的cProfile进行性能分析:
python -m cProfile -s tottime main.py orig.txt orig_0.8_add.txt ans.txt

  • 改进前输出:

fcbb3fbc-0b2f-44fa-b2e0-d70be53a7bbd


4.2 消耗最大的函数


改进前,difflib.find_longest_match耗时0.363秒,占总时间0.522秒的70%.该函数是SequenceMatcher内部实现LCS算法的核心,复杂度O(N²),文本越长耗时越大。

4.3 改进思路与效果


  • 改进思路:改用rapidfuzz库——它用C++实现了相同的LCS算法,API兼容,结果一致,但速度快数十倍。

  • 改进后输出

f499279a-1bc8-44b3-b91a-fcc6dcdf11c7

  • 改进效果对比
指标 改进前 改进后 提升
总耗时 0.522秒 0.065秒 约8倍
LCS核心耗时 0.363秒 0.016秒 约22倍

  • 精度验证: 改进后重新运行5个测试文件,相似度变化在±0.07以内,说明改进未损失精度。14个单元测试全部通过。
文件 优化前 优化后 变化
orig_0.8_add 0.82 0.83 +0.01
orig_0.8_del 0.79 0.80 +0.01
orig_0.8_dis_1 0.97 0.97 0
orig_0.8_dis_10 0.81 0.84 +0.03
orig_0.8_dis_15 0.60 0.67 +0.07

  • 性能改进花费时间: 约50分钟(分析cProfile输出10分钟,调研rapidfuzz用15分钟,替换代码10分钟,重新测试与对比15分钟)。

五、计算模块部分单元测试展示


5.1 单元测试框架与测试函数


使用Python标准库unittest。测试文件test_main.py包含三个测试类,共14个测试用例:

  • TestReadFile:测试read_file函数
  • TestPreprocess:测试preprocess函数
  • TestCalculateSimilarity:测试calculate_similarity函数

5.2 测试用例设计


共设计14个测试用例,覆盖三个核心函数的正常、边界和异常情况。全部用例列表如下:

编号 测试函数 测试场景 构造思路
1 test_read_multiline_file 读取多行中英文数字文件 覆盖正常读取路径
2 test_read_empty_file 读取空文件 边界情况
3 test_read_missing_file 文件不存在 异常情况
4 test_mixed_punctuation 中英文标点混合 覆盖标点去除
5 test_fullwidth_and_halfwidth 全角半角标点 边界情况
6 test_newline_tab_space 换行、制表符、空格 覆盖空白去除
7 test_only_numbers_and_letters 纯字母数字 验证不影响正常字符
8 test_empty_input 空字符串输入 边界情况
9 test_identical_long_text 完全相同文本 相似度应为1.0
10 test_completely_unrelated 完全不相关文本 相似度应低于0.3
11 test_one_is_substring 一方是子串 相似度应大于0.5
12 test_repeated_characters 重复字符 相似度应介于0.4~1.0
13 test_one_side_empty 一方为空 相似度应为0.0
14 test_both_sides_empty 双方都为空 相似度应为0.0

下面选取其中3个有代表性的测试函数,展示测试代码并说明设计思路。

(1). 测试read_file:读取多行中英文数字文件

def test_read_multiline_file(self):
    """
    读取含多行、中文、英文、数字的文件
    """
    content = "第一行 abc\n第二行 123\n第三行 end"
    with tempfile.NamedTemporaryFile(mode='w', delete=False,
                                      suffix='.txt', encoding='utf-8') as f:
        f.write(content)
        path = f.name

    result = read_file(path)
    os.unlink(path)

    self.assertEqual(result, content)
  • 测试函数:read_file
  • 构造思路:用tempfile创建一个临时文件,内容包含中文、英文、数字和换行,来模拟真实的文本文件。读取后删除临时文件。
  • 预期结果:读取到的内容与写入的内容完全一致,验证正常读取路径。

(2). 测试preprocess:中英文标点混合去除

def test_mixed_punctuation(self):
    """
    中英文标点混合去除
    """
    self.assertEqual(preprocess("Hello,世界!(test)?"), "Hello世界test")
  • 测试函数:preprocess
  • 构造思路:输入字符串中混合了中文逗号、感叹号、英文括号和问号,覆盖不同标点类型。
  • 预期结果:所有标点被去除,只保留字母和汉字,输出"Hello世界test"。

(3). 测试calculate_similarity:一方为空

def test_one_side_empty(self):
    """
    一方为空,相似度为0
    """
    self.assertEqual(calculate_similarity("你好世界", ""), 0.0)
    self.assertEqual(calculate_similarity("", "你好世界"), 0.0)
  • 测试函数:calculate_similarity
  • 构造思路:分别测试原文为空、抄袭版为空两种情况,覆盖空输入的边界条件。
  • 预期结果:两种情况下相似度都返回0.0,验证空输入保护逻辑。

5.3 测试覆盖率截图


  • 使用coverage工具:
python -m coverage run -m unittest test_main.py
python -m coverage report
python -m coverage html

  • 覆盖率报告截图

62ba06691cad388139e0161467705716

main.py未覆盖部分主要集中在main()函数中,因为单元测试针对的是各功能函数,而不是整个命令行入口。

5.4 测试评价


8bed2f48-664e-4f02-891c-dd7296ade64c

  1. 本次单元测试共14个用例,覆盖了三个核心函数的正常、边界、异常情况,运行结果全部通过(Ran 14 tests in 0.012s OK),覆盖率报告显示总覆盖率为84%,其中test_main.py覆盖率为98%,main.py覆盖率为62%
  • 正常情况:读取正常文件、相同文本、部分匹配文本。
  • 边界情况:空字符串、只有标点、一方为空、双方为空。
  • 异常情况:文件不存在时抛出FileNotFoundError。

  1. 从白盒测试角度,read_file的正常路径和异常路径均被覆盖;preprocess覆盖了标点去除、空白去除、空输入;calculate_similarity覆盖了空输入保护、LCS计算、返回0和1的极端情况。

  2. 不足:未覆盖编码错误和写入失败,后续可补充。

六、计算模块部分异常处理说明


异常类型 触发场景 设计目标 处理方式 对应测试
FileNotFoundError 文件路径不存在 提示用户路径错误 抛出异常,由调用方处理 test_read_missing_file
空文本 原文或抄袭版为空 避免除零错误 返回相似度0.0 test_one_side_empty
双方为空 两段文本都为空 避免无意义计算 返回相似度0.0 test_both_sides_empty
纯标点文本 文本只有标点 预处理后为空 返回相似度0.0 test_repeated_characters

每种异常都设计了对应的单元测试样例,确保程序在各种情况下不会崩溃。

七、过程总结


实际耗时见第一部分PSP表格:

  • 偏差分析:实际耗时比预估多出了约80分钟,主要原因是:

1.对GitHub相关操作不熟练,前期环境配置花费时间较多。
2.算法从词频余弦相似度调整成字符级LCS。
3.性能改进阶段调研rapidfuzz库并验证精度。


  • 改进计划

1.下次提前熟悉开发工具,减少环境配置时间。
2.算法选型前先做小规模实验,避免中途换方案。
3.性能分析提前做,避免后期临时优化。


八、附录


  • requirements.txt:
rapidfuzz

  • 运行命令:
python main.py <原文文件> <抄袭版文件> <答案文件>
posted @ 2026-09-14 23:59  666F1E88  阅读(17)  评论(0)    收藏  举报