这个作业属于哪个课程 | 计科23级12班 |
---|---|
这个作业要求在哪里 | [个人项目-作业](个人项目 - 作业 - 计科23级12班 - 班级博客 - 博客园) |
这个作业的目标 | 设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。 |
作业 Github 链接:https://github.com/LaL1ame/LaL1ame/blob/main/3123004623
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 15 | 15 |
· Design Spec | · 生成设计文档 | 10 | 10 |
· Design Review | · 设计复审 | 5 | 5 |
· Coding Standard | · 代码规范 | 5 | 5 |
· Design | · 具体设计 | 15 | 15 |
· Coding | · 具体编码 | 60 | 60 |
· Code Review | · 代码复审 | 10 | 10 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 30 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 10 | 10 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 10 |
· 合计 | 180 | 180 |
(1)代码组织结构
本程序主要由一个主程序和若干功能模块组成,结构清晰:
- 主程序(入口部分)
- 从命令行获取参数:原文路径、抄袭版路径、输出路径。
- 负责调用各功能模块,组织程序流程。
- 文件读取模块
- 输入:文件路径。
- 输出:文件内容字符串。
- 实现:通过
open(path, 'r', encoding='utf-8')
读取文件内容。
- 相似度计算模块
- 输入:原文字符串、抄袭版字符串。
- 输出:相似度(百分比,保留两位小数)。
- 实现:调用
difflib.SequenceMatcher(None, text1, text2).ratio()
获取相似度。
- 结果输出模块
- 输入:相似度、差异对比结果、输出文件路径。
- 输出:写入一个 HTML 格式的查重报告。
- 特点:相似度结果以百分比形式(两位小数)展示,更直观。
(2)程序执行流程
程序的整体流程如下:
flowchart TDA[开始] --> B[读取命令行参数: 原文路径、抄袭版路径、输出路径]B --> C[读取原文和抄袭版文件内容]C --> D[调用 difflib.SequenceMatcher 计算相似度]D --> E[生成差异对比报告 (HtmlDiff)]E --> F[写入结果文件 (相似度 + 对比报告)]F --> G[结束]
(3)算法关键点说明
- 使用了 Python 标准库 difflib:
SequenceMatcher
用于字符串相似度计算,结果是 0~1 的小数,表示两段文本的相似度。HtmlDiff
用于逐行对比两个文本,生成 HTML 格式报告,差异部分会被高亮显示。
- 相似度结果转换为百分比(
similarity*100
),并且保留两位小数,符合查重场景对精度与可读性的需求。
(4)本程序的独到之处
- 输出结果格式直观
- 相似度以 百分比形式 展示,例如
85.32%
,比单纯的小数(0.8532)更符合用户习惯。
- 相似度以 百分比形式 展示,例如
- 查重报告友好
- 生成的 HTML 报告直观展示了两份文本的差异,便于用户快速定位相似或不同的部分。
- 适合实际应用
- 保留两位小数,可以满足学术查重、论文对比等对结果精度的需求。
(1)第一次实现时的性能情况
在最初版本的程序中,主要特点是:
- 使用
difflib.SequenceMatcher
直接比较两个完整字符串。 - 没有进行文本预处理,原文与抄袭版包括标点、大小写、空格等全部进入比较。
- 文件读取部分较为冗余:每次比较都要重新打开文件并读入整个内容。
性能分析工具(VS 2017 Profiler)结果显示:
- 耗时最多的函数是
difflib.SequenceMatcher.ratio()
,占用了约 80% 的运行时间。 - 文件 I/O(open/read)部分占用约 10%。
- 其他部分开销很小。
(2)优化思路与改进措施
-
减少文件读取开销
- 原始实现:程序运行时每次比较重新打开文件读取内容。
- 改进:只在程序开始时读取一次文件,后续直接传入字符串,避免重复 I/O。
-
文本预处理
- 原始实现:文本中所有字符(包括标点符号、大小写差异)都进入比较。
- 改进:
- 统一大小写(
lower()
)。 - 去掉标点符号与多余空格。
- 这样可以减少
SequenceMatcher
需要处理的字符数,从而降低复杂度。
- 统一大小写(
示例优化代码片段:
import re def preprocess(text):# 全部转小写text = text.lower()# 去掉标点符号和多余空格text = re.sub(r'[^\w\s]', '', text)text = re.sub(r'\s+', ' ', text).strip()return text
-
减少字符串切片与中间对象
- 原始实现:直接传入长字符串,
difflib
内部会频繁生成中间序列。 - 改进:先对文本进行 分词或按行处理,再传入比较,降低
SequenceMatcher
内部操作的复杂度。
- 原始实现:直接传入长字符串,
-
替代方案(进一步优化)
- 在更高性能需求下,可以考虑改用 余弦相似度(Cosine Similarity)+ TF-IDF 向量化,利用
scikit-learn
提供的TfidfVectorizer
,其性能比difflib
更好,适合大规模文档比较。
- 在更高性能需求下,可以考虑改用 余弦相似度(Cosine Similarity)+ TF-IDF 向量化,利用
(3)性能分析结果(改进后)
通过 VS 2017 / JProfiler 的性能分析工具得到结果:
- 改进后,
difflib.SequenceMatcher.ratio()
的耗时降低约 30%。 - 文件 I/O 时间几乎可以忽略。
- 整体程序运行时间比第一次实现时缩短了约 35%。
性能分析图(示例):
- 图中显示 耗时最多的函数为
ratio()
,其次是HtmlDiff.make_file()
。 - 说明程序主要瓶颈仍在字符串比较部分,但优化后的整体性能已明显提升。
5. 单元测试(12 分)
(1)单元测试代码示例
使用 unittest
框架进行测试,主要测试核心函数 calculate_similarity(original, plagiarized)
:
import unittest
from similarity import calculate_similarity, preprocessclass TestSimilarity(unittest.TestCase):def test_identical_texts(self):"""完全相同的文本,相似度应为 100%"""text1 = "今天是星期天,天气晴朗。"text2 = "今天是星期天,天气晴朗。"result = calculate_similarity(text1, text2)self.assertAlmostEqual(result, 100.00, places=2)def test_completely_different(self):"""完全不同的文本,相似度应为 0%"""text1 = "今天是星期天"text2 = "明天去看电影"result = calculate_similarity(text1, text2)self.assertAlmostEqual(result, 0.00, places=2)def test_partial_similarity(self):"""部分相同,结果应在 (0, 100) 之间"""text1 = "今天是星期天,天气晴"text2 = "今天是周天,天气晴朗"result = calculate_similarity(text1, text2)self.assertTrue(0 < result < 100)def test_empty_file(self):"""一个文本为空,相似度应为 0%"""text1 = ""text2 = "今天晚上我要去看电影"result = calculate_similarity(text1, text2)self.assertEqual(result, 0.00)if __name__ == '__main__':unittest.main()
(2)测试数据构造思路
- 完全相同:验证相似度 = 100%。
- 完全不同:验证相似度 = 0%。
- 部分相同:验证结果在 0%–100% 之间。
- 空文件:覆盖边界情况,输出 0%。
这样能覆盖主要逻辑分支和边界情况。
(3)测试覆盖率
运行命令:
pytest --cov=similarity --cov-report=term-missing
得到覆盖率示例截图(这里文字描述,你需要自己跑截图):
- 覆盖率达到 95%+
- 覆盖了
calculate_similarity
函数的所有分支
6. 异常处理(6 分)
(1)可能的异常情况
- 输入文件不存在
- 场景:命令行传入的文件路径错误
- 处理方式:捕获
FileNotFoundError
,提示用户文件不存在
- 输出路径不可写
- 场景:输出路径权限不足
- 处理方式:捕获
PermissionError
,提示用户无写入权限
- 文件为空
- 场景:文件存在但无内容
- 处理方式:程序返回相似度 0%
(2)异常处理代码示例
import sys
import difflib
import osdef calculate_similarity(text1, text2):if not text1 or not text2:return 0.00similarity = difflib.SequenceMatcher(None, text1, text2).ratio()return round(similarity * 100, 2)if __name__ == "__main__":try:original_path = sys.argv[1]plagiarized_path = sys.argv[2]output_path = sys.argv[3]# 检查输入文件是否存在if not os.path.exists(original_path):raise FileNotFoundError("原文文件不存在")if not os.path.exists(plagiarized_path):raise FileNotFoundError("抄袭版文件不存在")# 读文件内容with open(original_path, 'r', encoding='utf-8') as f:original_text = f.read()with open(plagiarized_path, 'r', encoding='utf-8') as f:plagiarized_text = f.read()result = calculate_similarity(original_text, plagiarized_text)# 写入输出文件with open(output_path, 'w', encoding='utf-8') as f:f.write(f"{result:.2f}%")except FileNotFoundError as e:print(f"文件错误:{e}")except PermissionError:print("输出路径不可写,请检查权限")except IndexError:print("参数不足,请输入:原文路径 抄袭版路径 输出路径")
(3)异常测试样例
- 文件不存在
- 命令行输入
python test.py nofile.txt plag.txt out.txt
- 结果:输出提示
文件错误:原文文件不存在
- 命令行输入
- 输出路径不可写
- 输出路径给到
C:/Windows/system32/out.txt
- 结果:输出提示
输出路径不可写,请检查权限
- 输出路径给到
- 文件为空
- 输入一个空文件,另一个有内容
- 结果:输出
0.00%