软件工程第二次作业——第一次个人编程作业
这个作业属于哪个课程 | <班级的链接> |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13468 |
这个作业的目标 | 完成一个从需求分析、技术设计、编码实现、测试验证到文档报告的完整开发周期,最终交付一个功能正确、结构清晰、经过充分测试且便于协作的软件项目 |
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 660 | 685 |
Development | 开发 | 480 | 520 |
· Analysis | · 需求分析(包括学习新技术) | 120 | 140 |
· Design Spec | · 生成设计文档 | 60 | 70 |
· Design Review | · 设计复审 | 30 | 25 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | 35 |
· Design | · 具体设计 | 90 | 100 |
· Coding | · 具体编码 | 180 | 200 |
· Code Review | · 代码复审 | 45 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 130 |
Reporting | 报告 | 120 | 110 |
· Test Report | · 测试报告 | 60 | 50 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 30 | 30 |
合计 | 660 | 685 |
一、代码组织与结构设计
(1)模块划分:
主程序模块 (main.py):负责命令行参数解析和程序流程控制
查重算法模块 (checker.py):核心计算逻辑的实现
工具函数模块 (utils.py):提供文件操作等辅助功能
(2)类与函数设计
- 主程序模块 (main.py)
函数:main()
职责:解析命令行参数,调用查重算法,处理异常,输出结果
关系:调用 checker.py 中的查重函数和 utils.py 中的文件操作函数
- 查重算法模块 (checker.py)
函数:
preprocess_text(text: str) -> str
职责:文本预处理,包括分词、去除停用词和标点符号
关系:被 check_plagiarism 调用
calculate_cosine_similarity(text1: str, text2: str) -> float
职责:计算两个文本的余弦相似度
关系:被 check_plagiarism 调用
check_plagiarism(original_path: str, plagiarized_path: str) -> float
职责:主查重函数,协调整个查重流程
关系:调用预处理和相似度计算函数,被 main.py 调用
- 工具函数模块 (utils.py)
函数:
read_file(file_path: str) -> str
职责:读取文件内容,处理文件不存在等异常
关系:被 check_plagiarism 调用
write_result(output_path: str, similarity: float)
职责:将结果写入输出文件
关系:被 main.py 调用
二、算法关键与独到之处
(1)算法关键
1.文本预处理
使用jieba进行中文分词,将连续文本转换为词语序列
去除停用词(如"的"、"了"、"在"等常见但无实际语义的词)
过滤标点符号和特殊字符,保留有意义的词汇
2.特征提取与向量化
采用 TF-IDF(词频-逆文档频率)算法将文本转换为数值向量
TF-IDF 能够衡量词语在文档中的重要程度,既考虑词频也考虑词语区分度
3.相似度计算
使用余弦相似度衡量两个向量的方向差异
余弦相似度关注向量的方向而非大小,适合文本相似度计算
公式:cosine_similarity = (A·B) / (||A|| * ||B||)
(2)独到之处
与传统基于字符串匹配的方法不同,本系统在语义层面计算相似度
能够识别同义词和语义相近的表达(如"星期天"和"周天")
针对中文特点定制停用词表,提高算法对实质内容的关注度
减少常见虚词对相似度计算的干扰
能够处理空文件、短文本、特殊字符等边界情况
对异常输入有良好的容错能力
模块化设计使得算法组件易于替换和升级
(3)综合性能考量
在准确性和计算效率之间取得平衡
适合处理中等长度的学术论文文本
三、实现过程中的考虑
(1)接口设计原则
1.保持接口简洁明了,降低使用复杂度
2.输入输出明确,错误处理完善
3.函数职责单一,便于测试和维护
(2)异常处理机制
1.对文件不存在、编码错误等常见异常进行捕获和处理
2.提供有意义的错误信息,方便用户排查问题
(3)性能优化
使用高效的向量化操作,避免不必要的循环
对长文本进行适当处理,防止内存溢出
四、单元测试代码展示
(1)单元测试代码,展示了不同测试场景的实现:
class TestPlagiarismChecker(unittest.TestCase):
def setUp(self):
"""设置测试环境,创建测试文件目录"""
self.test_dir = os.path.join(os.path.dirname(__file__), "test_files")
os.makedirs(self.test_dir, exist_ok=True)
def test_identical_documents(self):
"""测试完全相同文档的相似度应为1.0"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_identical.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_identical.txt")
output_path = os.path.join(self.test_dir, "output_identical.txt")
# 创建完全相同的测试文件
with open(original_path, 'w', encoding='utf-8') as f:
f.write("自然语言处理是人工智能领域中的一个重要方向。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("自然语言处理是人工智能领域中的一个重要方向。")
# 执行测试
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果
self.assertAlmostEqual(similarity, 1.0, places=2,
msg="完全相同文档的相似度应为1.0")
def test_similar_documents(self):
"""测试相似但不完全相同的文档"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_similar.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_similar.txt")
output_path = os.path.join(self.test_dir, "output_similar.txt")
# 创建相似但不完全相同的测试文件
with open(original_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴,今天晚上我要去看电影。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("今天是周天,天气晴朗,我晚上要去看电影。")
# 执行测试
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果 - 相似度应在合理范围内
self.assertGreater(similarity, 0.7,
msg="相似文档的相似度应大于0.7")
self.assertLess(similarity, 0.9,
msg="相似但不完全相同的文档相似度应小于0.9")
def test_different_documents(self):
"""测试完全不同的文档相似度应接近0"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_different.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_different.txt")
output_path = os.path.join(self.test_dir, "output_different.txt")
# 创建完全不同的测试文件
with open(original_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴,今天晚上我要去看电影。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("明天是星期一,天气阴,我准备在家看书学习。")
# 执行测试
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果
self.assertLess(similarity, 0.3,
msg="完全不同文档的相似度应小于0.3")
def test_empty_document(self):
"""测试空文档的处理"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_empty.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_empty.txt")
output_path = os.path.join(self.test_dir, "output_empty.txt")
# 创建一个正常文档和一个空文档
with open(original_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴,今天晚上我要去看电影。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("") # 空文件
# 执行测试
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果
self.assertEqual(similarity, 0.0,
msg="空文档的相似度应为0.0")
def test_nonexistent_file(self):
"""测试文件不存在时的异常处理"""
# 使用不存在的文件路径
original_path = os.path.join(self.test_dir, "nonexistent_original.txt")
plagiarized_path = os.path.join(self.test_dir, "nonexistent_plagiarized.txt")
# 验证会抛出FileNotFoundError异常
with self.assertRaises(FileNotFoundError):
check_plagiarism(original_path, plagiarized_path)
if name == 'main':
unittest.main()
(2)测试函数与测试数据构造思路
- 测试函数说明
test_identical_documents
测试函数:check_plagiarism
测试目的:验证完全相同文档的相似度计算结果应为1.0
测试方法:创建两个内容完全相同的文件,计算它们的相似度
2.test_similar_documents
测试函数:check_plagiarism
测试目的:验证相似但不完全相同的文档的相似度在合理范围内
测试方法:创建两个内容相似但不完全相同的文件,检查相似度是否在预期范围内
3.test_different_documents
测试函数:check_plagiarism
测试目的:验证完全不同文档的相似度应接近0
测试方法:创建两个内容完全不同的文件,检查相似度是否接近0
4.test_empty_document
测试函数:check_plagiarism
测试目的:验证空文档的处理是否正确
测试方法:创建一个正常文档和一个空文档,检查相似度是否为0
5.test_nonexistent_file
测试函数:check_plagiarism
测试目的:验证文件不存在时的异常处理
测试方法:使用不存在的文件路径,检查是否会抛出预期的异常
(3)测试数据构造思路
测试数据的构造遵循以下原则:
全面性:覆盖各种可能的输入情况,包括正常情况和边界情况
代表性:选择具有代表性的测试用例,能够有效验证算法的正确性
可重复性:测试数据应该是确定的,每次运行都能得到相同的结果
多样性:包括中文文本、特殊字符、长文本、短文本等各种情况
(4)单元测试覆盖率
五、异常处理设计与单元测试
(1)异常处理设计概述
在论文查重系统的开发过程中,设计了多种异常处理机制,以确保系统在面对各种异常情况时能够正常处理,而不是直接崩溃。每种异常都有其特定的设计目标和处理策略。
(2)异常类型及设计目标
- 文件不存在异常 (FileNotFoundError)
设计目标:
防止程序在尝试读取不存在的文件时崩溃
提供清晰的错误信息,帮助用户定位问题
确保程序能够优雅地终止或跳过错误文件
单元测试样例:
def test_nonexistent_file(self):
"""测试文件不存在时的异常处理"""
# 使用不存在的文件路径
original_path = os.path.join(self.test_dir, "nonexistent_original.txt")
plagiarized_path = os.path.join(self.test_dir, "nonexistent_plagiarized.txt")
# 验证会抛出FileNotFoundError异常
with self.assertRaises(FileNotFoundError):
check_plagiarism(original_path, plagiarized_path)
错误场景:
用户输入了错误的文件路径
文件被移动或删除
文件路径拼写错误
- 空文件异常处理
设计目标:
处理空文件或几乎为空文件的情况
防止算法在处理空文本时出现除零错误或其他计算错误
返回合理的默认值(相似度为0.0)
单元测试样例:
def test_empty_document(self):
"""测试空文档的处理"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_empty.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_empty.txt")
output_path = os.path.join(self.test_dir, "output_empty.txt")
# 创建一个正常文档和一个空文档
with open(original_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴,今天晚上我要去看电影。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("") # 空文件
# 执行测试
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果
self.assertEqual(similarity, 0.0,
msg="空文档的相似度应为0.0")
错误场景:
用户提供的文件内容为空
文件只包含空白字符
文件只包含停用词,处理后变为空
- 编码异常 (UnicodeDecodeError)
设计目标:
处理非UTF-8编码的文件
提供清晰的错误信息,指导用户转换文件编码
防止程序因编码问题而崩溃
单元测试样例:
def test_encoding_error(self):
"""测试非UTF-8编码文件的处理"""
# 准备测试数据 - 创建一个GBK编码的文件
original_path = os.path.join(self.test_dir, "original_gbk.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_utf8.txt")
# 创建GBK编码的文件
with open(original_path, 'w', encoding='gbk') as f:
f.write("今天是星期天,天气晴。")
# 创建UTF-8编码的文件
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴。")
# 验证会抛出UnicodeDecodeError异常
with self.assertRaises(UnicodeDecodeError):
check_plagiarism(original_path, plagiarized_path)
错误场景:
文件使用非UTF-8编码(如GBK、GB2312等)
文件包含无法解码的二进制内容
文件编码与系统默认编码不匹配
- 权限异常 (PermissionError)
设计目标:
处理文件权限不足的情况
提供清晰的错误信息,指导用户修改文件权限
防止程序因权限问题而崩溃
单元测试样例:
def test_permission_error(self):
"""测试文件权限不足的情况"""
# 准备测试数据
original_path = os.path.join(self.test_dir, "original_no_permission.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_no_permission.txt")
# 创建文件并设置只读权限
with open(original_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴。")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("今天是星期天,天气晴。")
# 修改文件权限为只读(在Unix-like系统上)
if hasattr(os, 'chmod'):
os.chmod(original_path, 0o000)
os.chmod(plagiarized_path, 0o000)
# 验证会抛出PermissionError异常
with self.assertRaises(PermissionError):
check_plagiarism(original_path, plagiarized_path)
# 恢复文件权限以便清理
if hasattr(os, 'chmod'):
os.chmod(original_path, 0o644)
os.chmod(plagiarized_path, 0o644)
错误场景:
文件被设置为只读权限
当前用户没有文件读取权限
文件被其他进程锁定
- 参数错误异常 (ValueError)
设计目标:
处理命令行参数错误的情况
提供使用说明,帮助用户正确使用程序
防止程序因参数错误而执行错误操作
单元测试样例:
def test_argument_error(self):
"""测试命令行参数错误的情况"""
# 保存原始参数
original_argv = sys.argv
# 测试参数不足的情况
sys.argv = ['main.py', 'original.txt']
with self.assertRaises(SystemExit) as cm:
main()
self.assertEqual(cm.exception.code, 1)
# 测试参数过多的情况
sys.argv = ['main.py', 'original.txt', 'plagiarized.txt', 'output.txt', 'extra.txt']
with self.assertRaises(SystemExit) as cm:
main()
self.assertEqual(cm.exception.code, 1)
# 恢复原始参数
sys.argv = original_argv
错误场景:
命令行参数数量不正确
文件路径格式错误
输出文件路径不可写
- 算法计算异常 (ValueError/ZeroDivisionError)
设计目标:
处理算法计算过程中可能出现的异常
确保算法在面对异常输入时能够 gracefully 处理
返回合理的默认值或提供有意义的错误信息
单元测试样例:
def test_algorithm_error(self):
"""测试算法计算异常的情况"""
# 准备测试数据 - 两个完全不同的短文本
original_path = os.path.join(self.test_dir, "original_short.txt")
plagiarized_path = os.path.join(self.test_dir, "plagiarized_short.txt")
output_path = os.path.join(self.test_dir, "output_short.txt")
# 创建非常短的文本(可能无法提取有效特征)
with open(original_path, 'w', encoding='utf-8') as f:
f.write("A")
with open(plagiarized_path, 'w', encoding='utf-8') as f:
f.write("B")
# 执行测试 - 应该不会抛出异常,而是返回一个合理的值
similarity = check_plagiarism(original_path, plagiarized_path)
write_result(output_path, similarity)
# 验证结果是一个浮点数且在合理范围内
self.assertIsInstance(similarity, float)
self.assertGreaterEqual(similarity, 0.0)
self.assertLessEqual(similarity, 1.0)
错误场景:
文本过短,无法提取有效特征
所有词汇都被过滤为停用词
向量模长为零导致除零错误
(3)异常处理策略总结
在操作前检查文件是否存在、是否有权限等
使用try-except块捕获可能出现的异常
在出现异常时返回合理的默认值而不是崩溃
提供清晰的错误信息,帮助用户解决问题