第一次个人编程作业
| 这个作业属于哪个课程 | 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 算法关键
- 核心思路:将两段文本视为字符序列,计算最长公共子序列的长度,然后用LCS长度 / 较长序列长度作为相似度。
- 选择LCS原因:
- 论文抄袭通常是在原文基础上增删改,LCS能有效捕捉这些变化。
- 相比于余弦相似度,LCS对词序敏感,区分度更高。
- 相比编辑距离,LCS对插入和删除的处理更公平,不会因为原文或抄袭版长度差异过大而失真。
- 独到之处:
- 使用
rapidfuzz库的LCSseq.similarity,底层是C++实现,性能远超Python的difflib.SequenceMatcher。 - 预编译正则表达式,避免重复编译。
- 对空文本、纯标点文本做保护,返回0。
3.4 关键函数流程
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

四、计算模块接口部分的性能改进
4.1 性能分析工具与性能分析图
- 使用Python自带的
cProfile进行性能分析:
python -m cProfile -s tottime main.py orig.txt orig_0.8_add.txt ans.txt
- 改进前输出:

4.2 消耗最大的函数
改进前,difflib.find_longest_match耗时0.363秒,占总时间0.522秒的70%.该函数是SequenceMatcher内部实现LCS算法的核心,复杂度O(N²),文本越长耗时越大。
4.3 改进思路与效果
-
改进思路:改用
rapidfuzz库——它用C++实现了相同的LCS算法,API兼容,结果一致,但速度快数十倍。
-
改进后输出:

- 改进效果对比:
| 指标 | 改进前 | 改进后 | 提升 |
|---|---|---|---|
| 总耗时 | 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
- 覆盖率报告截图:

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

- 本次单元测试共14个用例,覆盖了三个核心函数的正常、边界、异常情况,运行结果全部通过(Ran 14 tests in 0.012s OK),覆盖率报告显示总覆盖率为84%,其中
test_main.py覆盖率为98%,main.py覆盖率为62%
- 正常情况:读取正常文件、相同文本、部分匹配文本。
- 边界情况:空字符串、只有标点、一方为空、双方为空。
- 异常情况:文件不存在时抛出FileNotFoundError。
-
从白盒测试角度,read_file的正常路径和异常路径均被覆盖;preprocess覆盖了标点去除、空白去除、空输入;calculate_similarity覆盖了空输入保护、LCS计算、返回0和1的极端情况。
-
不足:未覆盖编码错误和写入失败,后续可补充。
六、计算模块部分异常处理说明
| 异常类型 | 触发场景 | 设计目标 | 处理方式 | 对应测试 |
|---|---|---|---|---|
| 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 <原文文件> <抄袭版文件> <答案文件>

浙公网安备 33010602011771号