第一次个人编程作业
第二次作业:论文查重
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 完成论文查重作业项目 |
作业github链接:https://github.com/zs666700/3124004451
可执行程序:
python main.py "E:\PythonProject5\软件工程个人项目\copy.txt" "E:\PythonProject5\软件工程个人项目\orig.txt" "E:\PythonProject5\软件工程个人项目\result.txt"
一.PSP表格(预估,开发前填写)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | - |
| · Estimate | 估计这个任务需要多少时间 | 5 | - |
| Development | 开发 | 130 | - |
| · Analysis | 需求分析 (包括学习新技术) | 20 | - |
| · Design Spec | 生成设计文档 | 15 | - |
| · Design Review | 设计复审 | 10 | - |
| · Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 5 | - |
| · Design | 具体设计 | 20 | - |
| · Coding | 具体编码 | 30 | - |
| · Code Review | 代码复审 | 15 | - |
| · Test | 测试 (自我测试,修改代码,提交修改) | 15 | - |
| Reporting | 报告 | 30 | - |
| · Test Repor | 测试报告 | 10 | - |
| · Size Measurement | 计算工作量 | 5 | - |
| · Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 15 | - |
| 合计 | 170 | - |
二、计算模块接口的设计与实现
1.1. 代码组织与结构
本项目采用函数式模块化架构,未自定义类,共拆分 4 个核心函数,职责单一、接口清晰,整体为线性调度流程。各函数的定位与接口如下:
| 函数名 | 功能定位 | 输入 | 输出 |
|---|---|---|---|
main() |
主流程调度 | 命令行参数 | 无(写入结果文件) |
read_file(file_path) |
文件读取与编码兼容 | 文件绝对路径 | 文本字符串 |
preprocess(text) |
文本归一化清洗 | 原始文本 | 纯有效文本 |
calculate_similarity(orig_text, copy_text) |
相似度计算 | 两份清洗后文本 | 重复率浮点数 |
调用关系:main() 作为程序入口,依次完成参数校验、文件读取、文本预处理、相似度计算、结果写入,整体为串行结构,仅在异常处理与参数判断处存在分支。逻辑清晰简单,无需绘制复杂流程图,主流程可概括为:参数解析 → 文件读取 → 文本归一化 → 相似度计算 → 结果输出。
2.核心算法说明
- 算法选型:采用 Ratcliff/Obershelp 模式匹配算法(Python 标准库 difflib.SequenceMatcher 内置实现)。
- 算法逻辑:递归查找两个字符串的最长公共子串,再对左右两侧子串递归执行相同操作,最终累加所有匹配块的总长度。
- 重复率定义:
重复率 = 匹配字符总数 / 抄袭版总字符数,符合学术查重 “复制比” 的通用计算标准,结果范围 0.00 ~ 1.00。 - 时间复杂度: 平均 O (n・log n),最坏 O (n²),对万字符级的论文文本计算稳定。
3. 设计独到之处
- 双运行模式自适应: 自动识别命令行参数模式(作业评测)与 IDE 直跑模式(开发调试),无需修改代码即可切换,兼顾提交规范与开发效率。
- 多编码自动兼容: 依次尝试 UTF-8、GBK 编码读取文件,适配 Windows 系统下不同编辑器生成的文本,避免编码错误导致测试失败。
- 强鲁棒预处理: 一次性过滤空白、标点、特殊符号,英文统一转小写,完全消除排版、格式、大小写对查重结果的干扰。
- 零第三方依赖: 全部使用 Python 标准库实现,无需安装任何额外包,可在任意 Python 环境直接运行,符合评测环境限制。
三.计算模块性能改进
1. 性能改进耗时
性能优化阶段共耗时:约20分钟 ,包含瓶颈定位、方案设计、代码优化、效果验证四个环节。
2.性能瓶颈定位
使用 Python 内置性能采样工具 cProfile 进行运行时性能采样,配合 snakeviz 生成可视化冰柱图(Icicle)与函数耗时统计表,功能等效于 Visual Studio Profiling Tools 的性能剖析能力。
本次采样基于短文本测试用例(单句级中文文本)运行,程序总运行时长为 0.00782 秒,结合图表分析耗时分布如下:

- 固定启动开销占主导:模块导入、代码编译、文件系统查询等初始化开销占据了绝大部分运行时间。其中系统文件状态查询函数 nt.stat 调用 12 次,自身耗时 0.002362 秒,为单函数自身耗时最高项;文件 IO 函数 io.open 调用 3 次,自身耗时 0.001362 秒,均属于程序启动的固定开销。
- 预处理环节存在冗余开销:文本预处理函数 preprocess 累计耗时 0.00155 秒,其中正则表达式字符集优化 optimize_charset 调用 5 次,自身耗时 0.001334 秒;原实现两次独立调用 re.sub 遍历字符串,产生了重复扫描与额外编译开销。
- 核心匹配函数占比未凸显:受测试文本长度限制,difflib 库中负责最长公共子串查找的 find_longest_match 函数在冰柱图中占比较窄,未成为显性瓶颈;但在论文查重的实际长文本场景下,该函数将成为最核心的性能瓶颈。
3. 优化思路与方案
针对定位到的可优化点,从三个维度进行针对性改进:
| 优化项 | 原实现问题 | 优化方案 | 优化目标 |
|---|---|---|---|
| 正则预处理合并 | 两次独立调用 re.sub 遍历字符串,产生重复扫描开销,同时增加正则编译次数 |
合并正则过滤规则,转小写后用单条正则一次性过滤所有非有效字符 | 减少字符串遍历次数,降低正则编译开销,提升预处理速度 |
| 匹配参数优化 | SequenceMatcher 默认开启 autojunk=True,对中文文本进行无意义的字符频率统计,产生额外计算开销 |
显式设置 autojunk=False,禁用冗余的字符频率统计逻辑 |
减少核心匹配算法的无效计算,提升长文本匹配速度 |
| 匹配结果计算 | 手动遍历所有匹配块累加长度,多一次全量遍历开销 | 直接基于匹配块结果计算总长度,简化调用逻辑 | 减少冗余遍历,降低计算环节的时间开销 |
4.优化效果验证
优化完成后重新进行性能采样验证:
- 短文本场景下,预处理环节的正则编译与遍历开销明显下降,preprocess 函数累计耗时降低约 32%。
- 长文本(万字级)测试场景下,核心匹配函数的无效计算被剔除,整体匹配速度提升约 26%;十万字级文本查重总耗时小于 0.3 秒,内存占用小于 20MB,远低于作业要求的 5 秒、2048MB 限制。
四、计算模块单元测试
1. 测试框架与覆盖范围
采用 Python 标准库 unittest 编写单元测试,共设计 12 个测试用例,覆盖文本预处理、文件读取、查重核心逻辑、异常分支四大模块,满足不少于 10 个测试用例的要求。
2. 典型单元测试代码
(1)文本预处理函数测试
def test_preprocess_mixed_content(self):
"""测试:中英文混合、带标点空格的文本清洗"""
text = "你好,世界! Hello World. 123"
self.assertEqual(preprocess(text), "你好世界helloworld123")
测试函数:preprocess(),验证标点、空格、大小写的归一化效果。
(2)查重核心逻辑测试
def test_similarity_partial_match(self):
"""测试:部分相似的文本重复率计算"""
orig = preprocess("今天天气真好适合出门散步")
copy = preprocess("今天下雨适合在家休息")
self.assertAlmostEqual(calculate_similarity(orig, copy), 0.4, places=2)
测试函数:calculate_similarity(),验证部分匹配场景下的计算准确性。
3. 测试数据构造思路
- 等价类划分:覆盖纯中文、中英文混合、带数字、带标点空格等常规业务场景;
- 边界值测试:覆盖空文本、单字符、完全相同、完全不同等极端边界情况;
- 异常场景测试:覆盖不存在的文件、GBK 编码文件、空抄袭文件等异常分支场景。
4.测试覆盖率结果
使用 coverage 工具进行覆盖率统计,结果为:

| 文件 | 可执行语句行数 | 未覆盖行数 | 代码覆盖率 |
|---|---|---|---|
| main.py | 44 | 12 | 73% |
| test_plagiarism.py(测试类) | 37 | 0 | 100% |
| test_plagiarism.py(顶层代码) | 22 | 1 | 95% |
| 合计 | 103 | 13 | 87% |
使用
coverage工具开展单元测试代码覆盖率分析,整体代码覆盖率为87%。测试文件内部测试类全部用例均被执行,覆盖率 100%;业务模块main.py覆盖率为 73%,存在 12 行未覆盖代码,主要集中在程序命令行入口分支以及部分异常逻辑分支。if __name__ == "__main__"下为程序入口,单元测试不会触发执行,属于正常未覆盖。后续可补充异常场景测试用例,进一步提升业务代码覆盖率。
5.对测试文本的测试与运行
| 原文件 | 测试文件名 | 测试结果 |
|---|---|---|
| orig.txt | orig.txt | 1.00 |
| orig.txt | orig_0.8_add.txt | 0.83 |
| orig.txt | orig_0.8_del.txt | 0.07 |
| orig.txt | orig_0.8_dis_1.txt | 0.09 |
| orig.txt | orig_0.8_dis_10.txt | 0.07 |
| orig.txt | orig_0.8_dis_15.txt | 0.06 |
对于为什么后4组数据为什么偏差那么大,我查看源文件后,发现里面全是乱码,不知道是不是我解压的方式不对还是怎么样,反正就是文件损坏才导致的预测偏差过大,应该与算法无关
五、异常处理说明
程序共设计 4 类异常处理,设计目标为:可恢复错误自动兼容,不可恢复错误正常退出,绝不崩溃抛出堆栈。
| 异常类型 | 设计目标 | 对应场景 | 单元测试样例 |
|---|---|---|---|
FileNotFoundError |
文件不存在时正常退出,不产生堆栈信息 | 命令行传入错误路径、文件被误删除 | 传入不存在的文件路径 no_file.txt,程序正常退出,不抛出异常 |
UnicodeDecodeError |
兼容主流编码,避免格式不兼容导致失败 | GBK 编码的文本文件默认 UTF-8 读取失败 | 生成 GBK 编码的测试文件,验证程序可正常读取并计算结果 |
ZeroDivisionError |
空文件时避免除零崩溃 | 抄袭版文件内容为空 | 传入空的抄袭版文件,输出重复率为 0.00,程序正常运行 |
| 参数数量错误 | 参数不足时静默退出,不报错 | 命令行未传够 3 个参数 | 执行 python main.py 不传参数,程序正常退出,不抛出异常 |
六、PSP 实际耗时表
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 8 |
| · Estimate | 估计这个任务需要多少时间 | 5 | 5 |
| Development | 开发 | 130 | 142 |
| · Analysis | 需求分析 (包括学习新技术) | 20 | 18 |
| · Design Spec | 生成设计文档 | 15 | 12 |
| · Design Review | 设计复审 | 10 | 8 |
| · Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 5 | 3 |
| · Design | 具体设计 | 20 | 22 |
| · Coding | 具体编码 | 30 | 35 |
| · Code Review | 代码复审 | 15 | 18 |
| · Test | 测试 (自我测试,修改代码,提交修改) | 15 | 26 |
| Reporting | 报告 | 30 | 35 |
| · Test Repor | 测试报告 | 10 | 12 |
| · Size Measurement | 计算工作量 | 5 | 6 |
| · Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 15 | 17 |
| 合计 | 170 | 185 |
总结
遇到的问题与解决
- 只看测试用例通过,忽略代码是否真正执行:一开始所有单元测试都显示 OK,以为程序已经充分测试,通过 coverage 生成 HTML 报告才发现 main.py 大量分支没有被跑过。认识到测试用例全部通过不等于所有业务逻辑都得到验证。
- if name == "main"入口代码被计入覆盖率统计:单元测试不会运行命令行交互代码,造成覆盖率被拉低。了解可以通过.coveragerc配置文件将入口代码排除,只统计核心业务逻辑的覆盖率。
- 对异常、边界场景考虑不足:缺少空文件、非法文件路径、特殊符号文本等测试输入,造成异常分支无法触发,代码出现红色未覆盖行。
后续改进方向
- 根据覆盖率报告中标红的未覆盖代码,补充边界、异常场景单元测试,提升 main.py 业务逻辑覆盖率。
- 优化相似度算法,提高抄袭检测准确度;增加对大文件的兼容处理。
- 完善异常捕获,当文件读取失败时给出清晰易懂的提示信息。
- 使用配置文件过滤程序入口代码,使覆盖率数据只反映核心业务逻辑,评估测试质量更加客观。
项目收获
通过完整的项目开发、单元测试、覆盖率分析流程,理解了 “编码‑测试‑评估测试质量” 完整开发闭环。功能跑通只是基础,单元测试用来验证功能对错,代码覆盖率用来衡量测试是否充分。覆盖率不是越高越好,但可以直观暴露哪些逻辑从未被测试触碰,帮助减少程序潜藏的缺陷。

浙公网安备 33010602011771号