软件工程第二次作业——第一次个人编程作业

这个作业属于哪个课程 <班级的链接>
这个作业要求在哪里 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)类与函数设计

  1. 主程序模块 (main.py)

函数:main()
职责:解析命令行参数,调用查重算法,处理异常,输出结果
关系:调用 checker.py 中的查重函数和 utils.py 中的文件操作函数

  1. 查重算法模块 (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 调用

  1. 工具函数模块 (utils.py)

函数:
read_file(file_path: str) -> str
职责:读取文件内容,处理文件不存在等异常
关系:被 check_plagiarism 调用

write_result(output_path: str, similarity: float)
职责:将结果写入输出文件
关系:被 main.py 调用

flowchart TD A[main.py: main函数] --> B[解析命令行参数] B --> C{参数验证} C -->|无效| D[输出错误信息并退出] C -->|有效| E[调用 checker.check_plagiarism] subgraph checker.check_plagiarism E --> F[utils.read_file 读取原文] E --> G[utils.read_file 读取抄袭文] F --> H[checker.preprocess_text 预处理原文] G --> I[checker.preprocess_text 预处理抄袭文] H --> J[checker.calculate_cosine_similarity] I --> J J --> K[返回相似度结果] end K --> L[utils.write_result 写入输出文件] L --> M[程序结束]

二、算法关键与独到之处

(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)测试函数与测试数据构造思路

  1. 测试函数说明
    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)单元测试覆盖率
481fd069f85902523983532e749d4296

五、异常处理设计与单元测试

(1)异常处理设计概述
在论文查重系统的开发过程中,设计了多种异常处理机制,以确保系统在面对各种异常情况时能够正常处理,而不是直接崩溃。每种异常都有其特定的设计目标和处理策略。

(2)异常类型及设计目标

  1. 文件不存在异常 (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)

错误场景:
用户输入了错误的文件路径
文件被移动或删除
文件路径拼写错误

  1. 空文件异常处理
    设计目标:
    处理空文件或几乎为空文件的情况
    防止算法在处理空文本时出现除零错误或其他计算错误
    返回合理的默认值(相似度为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")

错误场景:
用户提供的文件内容为空
文件只包含空白字符
文件只包含停用词,处理后变为空

  1. 编码异常 (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等)
文件包含无法解码的二进制内容
文件编码与系统默认编码不匹配

  1. 权限异常 (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)

错误场景:
文件被设置为只读权限
当前用户没有文件读取权限
文件被其他进程锁定

  1. 参数错误异常 (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

错误场景:
命令行参数数量不正确
文件路径格式错误
输出文件路径不可写

  1. 算法计算异常 (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块捕获可能出现的异常
在出现异常时返回合理的默认值而不是崩溃
提供清晰的错误信息,帮助用户解决问题

posted @ 2025-09-22 12:54  王嘉慧  阅读(35)  评论(0)    收藏  举报