第一次个人编程作业
第一次编程作业:论文查重
GitHub链接:https://github.com/xing540/3124004216
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率 |
学号: 3124004216
开发语言: python3
一、PSP表格:
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 20 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 15 | 20 |
| · Analysis | · 需求分析 (包括学习新技术) | 40 | 45 |
| · Design Spec | · 生成设计文档 | 30 | 35 |
| · Design Review | · 设计复审 | 15 | 10 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 8 |
| · Design | · 具体设计 | 30 | 32 |
| · Coding | · 具体编码 | 60 | 70 |
| · Code Review | · 代码复审 | 20 | 18 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 40 | 50 |
| Reporting | 报告 | 15 | 15 |
| · Test Repor | · 测试报告 | 25 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 8 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | 20 |
| · 合计 | 335 | 358 |
二、计算模块接口的设计与实现过程:
1.函数划分:
get_ngram()提取特征、calc_similarity()计算相似度、main()处理命令行与文件IO
2.流程图:

3.算法关键说明:
本查重算法使用2‑gram + Jaccard相似度实现文本重复率计算。
1. 2‑gram特征:将连续两个汉字作为一个特征单元。例如“今天是星期天”会拆分为: 今天 、 天是 、 是 星 、 星期 、 期天 。
该方式可以捕捉文本局部连续片段,对少量增删改、同义词替换有一定鲁棒性,适合课程论文查重场景。
2. Jaccard相似度公式
J(A,B)=$\frac{|A\cap B|}{|A\cup B|}$
-
A:原文的二元特征集合;B:抄袭版的二元特征集合
-
交集代表两篇文本共同拥有的连续片段;并集代表全部不重复片段;比值就是文本相似度。
3. 边界处理:如果两篇文本清洗之后都没有有效中文,直接返回相似度0,避免除零错误。
4. 最终重复率 = 相似度 × 100,格式化保留两位小数输出到文件。
4.独特之处:
1. 算法与IO完全解耦
核心计算函数 calc_similarity 只处理字符串,不读写文件、不读取命令行。单元测试可以直接传入字符串进行测试,不需要创建真实文件,提高测试效率。
2. 预处理过滤干扰字符
把标点、数字、英文字母全部过滤,避免标点符号、无关符号干扰查重结果。例如样例中“星期天”和“周天”,会在二元特征中体现部分重合,符合题目示例预期。
3. 使用集合(set)存储特征,提升性能
使用Python集合存储2‑gram特征,集合的交集、并集底层是哈希实现,时间复杂度接近O(n)。相比列表双重循环对比,大幅降低计算开销,处理大文本不会超时。
4. 完备的边界条件处理
考虑多种极端输入:空文件、文件只有标点符号、一方为空文本、两方全部为空文本,全部做特殊处理,防止程序除零崩溃、异常退出,保证评测点不因为边界用例失败。
5. 无外部依赖,无网络请求
不使用第三方库,不访问网络,不读写除输入输出指定路径以外的文件,完全符合作业评测安全要求,不会触发作业0分判定。
三、计算模块接口部分的性能改进:
1.改进花费时间
- 性能分析与优化总耗时:约45分钟
- 读取性能分析报告:15分钟
- 定位性能瓶颈:15分钟
- 代码修改、对比测试:15分钟
2.原始版本存在的问题
原始代码逻辑:
1. clean_text() :正则过滤非中文字符
2. get_2gram() :遍历字符串,逐个生成二元组,存入列表,再转集合
3. calc_similarity() :调用清洗、特征提取,做Jaccard计算
原始版本瓶颈:
1. 最初版本把2‑gram先存 list ,再转 set ,会产生临时列表,大文本下内存、时间开销变大。
2. 字符串遍历过程中,重复创建中间对象。
3. 没有对空文本做提前短路判断,即使文本清洗后为空,依然会走完完整流程。
3.改进思路
改进1:直接生成集合,消除临时列表
旧写法:先生成list,再转set
优化后:直接往set添加元素,不使用中间列表,减少内存拷贝,省去列表转集合的开销。
改进2:增加提前短路判断
在 calc_similarity 内部,文本清洗完成后,如果清洗后的字符串长度小于2,直接返回相似度0,不再执行2‑gram提取,避免无效循环。
比如输入只有1个汉字、全是标点,直接返回0,跳过循环。
改进3:正则表达式复用
re.sub 正则模式编译一次,不要每次调用 clean_text 都重新编译正则,减少正则编译开销。
python
改进4:使用集合原生交并运算
Python内置 set 的 & (交集)、 | (并集)是C底层实现,远快于自己写循环统计交集数量,保留该设计。
优化后:时间复杂度仍为O(n),但是常数项大幅降低;大文本下内存占用下降,不会出现超时、内存超限。
4.性能分析(cProfile)
消耗最大的函数: get_2gram()
原因: 该函数需要遍历全部清洗后的中文文本,生成二元特征,文本越大,循环次数越多,占用时间最多。
其次是 clean_text() 正则文本清洗。优化后,消除中间列表,减少内存拷贝,运行速度提升约30%。
5.优化前后对比
项目 原始版本 优化后版本
大文本运行耗时 0.18s 0.12s
内存占用 较高(存在临时list) 更低,无多余临时对象
空文本处理 完整走完循环 直接短路返回0
正则编译 每次调用都编译 全局预编译一次
优化后,程序运行时间远小于5秒,内存远低于2048MB,满足评测要求。
四、计算模块部分单元测试展示:
测试覆盖率截图:

代表性用例:
两篇完全相同:

两篇完全不同:

删减部分文字:

部分修改,部分重复:

五、计算模块部分异常处理说明:
1. 命令行参数数量错误
设计目标:
sys.argv 存储命令行传入参数, sys.argv[0] 是程序名,真正输入需要3个路径,一共需要4个元素。如果传入参数数量不对,直接打印使用帮助,正常退出,防止访问下标时报 IndexError 索引越界崩溃。
异常场景:
执行程序时只传入2个路径,或者不传任何参数。
示例命令: python main.py D:\test\orig.txt D:\test\orig_add.txt (缺少输出文件路径)

2. 输入文件不存在
设计目标:
当传入的原文或者抄袭版文件路径不存在时,捕获 FileNotFoundError ,输出提示信息,正常退出,防止程序抛出未捕获异常导致异常退出扣分。
异常场景:
传入一个不存在的文件路径,例如 D:\test\no_exist.txt ,磁盘上没有这个文件。

3. 文件读取权限错误
设计目标:
文件存在,但是当前用户没有读取权限,捕获 PermissionError ,提示权限问题,程序正常退出,不直接崩溃。
异常场景:
文件被系统锁定、文件设置为只读,无法打开读取。

4. 输出文件写入失败
设计目标:
输出答案文件路径非法、文件夹不存在,或者没有写入权限,捕获写入异常,给出提示,程序正常退出,避免程序崩溃。
异常场景:
输出路径指向一个不存在的文件夹,或者磁盘只读,无法创建ans.txt。

异常处理整体设计总结
1. IO层面异常:针对命令行参数、文件读写,捕获明确的异常类型,打印提示信息,调用 sys.exit() 正常退出,杜绝未捕获异常造成程序异常退出,防止评测扣分。
2. 业务逻辑边界异常:针对空文本、单字符文本,做前置短路判断,直接返回0,规避除零、无效循环问题。
3. 程序不会访问网络、不会读写除输入输出指定路径以外的文件,不会触发作业0分判定。
4. 全部异常场景均编写对应的单元测试用例,保证边界场景下程序行为符合预期。

浙公网安备 33010602011771号