软件工程第二次作业
| 这个作业属于哪个课程 | 我所在的班级 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 尝试完成一份“软件工程作业”,包含测试等完成流程 |
作业 GitHub 链接:https://github.com/morganalter/paper-check-homework/tree/main/3124004485
一、PSP表格
| PSP2.1 阶段 | 中文说明 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 20 |
| · Estimate | 估计任务总时间 | 20 | 20 |
| Development | 开发 | 240 | 280 |
| · Analysis | 需求分析 + 学习相关技术 | 30 | 40 |
| · Design Spec | 梳理设计思路 | 20 | 25 |
| · Design Review | 设计自查 | 10 | 25 |
| · Coding Standard | 制定代码规范 | 10 | 15 |
| · Design | 模块与函数设计 | 30 | 30 |
| · Coding | 编写代码 | 80 | 85 |
| · Code Review | 代码检查 | 20 | 25 |
| · Test | 自测 + 修改 bug | 40 | 40 |
| Reporting | 报告与总结 | 90 | 80 |
| · Test Repor | 整理测试报告 | 30 | 30 |
| · Size Measurement | 统计工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结 + 改进计划 | 50 | 50 |
| 合计 | 350 | 425 |
二、计算模块接口的设计与实现过程
2.1 模块组织与接口设计
程序采用模块化分层设计,共包含 5 个功能函数与 1 个主入口函数。
| 函数 | 输入 | 输出 | 核心职责 |
|---|---|---|---|
| clean_text(text) | 原始文本字符串 | 清洗后的纯文本字符串 | 去除标点、空格、换行符等无关字符,统一文本格式 |
| seg_words(text) | 清洗后的文本 | 分词结果列表 | 调用 jieba 精确模式完成中文分词,过滤空字符 |
| calc_cosine_similarity(text1, text2) | 两篇原始文本 | 相似度浮点数值 | 词频统计、向量构建、余弦相似度计算 |
| read_file(file_path) | 文件路径字符串 | 文件内容字符串 | UTF-8 编码文件读取与异常捕获 |
| write_result(file_path, score) | 输出路径、相似度值 | 无 | 格式化输出结果到指定文件 |
| main() | 命令行参数 | 无 | 参数校验、流程调度、全局异常处理 |
调用关系为线性流程:main() → read_file() 读取两篇文本 → calc_cosine_similarity() 内部依次调用 clean_text()、seg_words() 完成预处理与分词 → 计算相似度 → write_result() 写入结果。
2.2 核心算法原理
程序基于词袋模型 + 余弦相似度算法实现文本查重,核心思路如下:
- 文本预处理:通过正则表达式去除非文本字符,消除格式差异对结果的干扰。
- 中文分词:将连续的中文文本拆分为独立词语,作为向量化的基本单元。
- 词频向量化:统计每篇文本中每个词语的出现次数,构建词频向量。
- 余弦相似度计算:计算两个词频向量夹角的余弦值,数值越接近 1 代表文本重合度越高。
2.3 设计独到之处
文本清洗、分词、相似度计算、文件读写完全独立,可单独测试、替换与优化,扩展性强。
三、计算模块接口部分的性能改进
3.1 改进思路
核心思路为:先通过性能分析工具定位瓶颈,再针对自研代码的可优化点进行定向改进。
优化前先通过 cProfile 采集全函数耗时数据,定位到两类可优化点:一是重复执行的编译类操作,二是 Python 层的显式循环。据此实施四项针对性优化:正则表达式预编译、分词词典预加载、Counter 内置词频统计、列表推导式替代 for 循环。
3.2 性能分析结果
使用 Python 标准性能分析工具 cProfile 采集数据,结合 snakeviz 生成可视化调用树图,如下所示:


上图为优化前版本性能分析图,程序总耗时 0.5748 秒,其中消耗最大的函数为 marshal.load,累计耗时 0.443 秒,占总耗时约 77%,属于第三方中文分词库的固有初始化开销,自研业务代码部分累计耗时最长为calc_cosine_similarity。


上图为优化后版本性能分析图
优化后程序总耗时 0.553 秒,自研业务代码自身耗时降低约 30%,核心瓶颈仍为 jieba 词典加载,属于第三方库固定开销,自研业务代码部分优化后累计耗时最长为main。
3.3 优化效果对比
| 优化点 | 优化前实现 | 优化后实现 | 性能提升 |
|---|---|---|---|
| 正则编译 | 每次调用实时编译 | 模块顶层预编译 | 文本清洗模块耗时降低 49% |
| 词典加载 | 首次分词时触发加载 | 程序启动主动预加载 | 业务执行阶段无突发卡顿 |
| 词频统计 | 手写字典 + for 循环 | collections.Counter 内置实现 | 词频统计模块耗时降低 41% |
| 分词过滤 | for 循环 + append | 列表推导式 | 分词过滤环节耗时降低 25% |
整体来看,自研业务代码自身性能提升约 30%,优化措施均达到预期效果。优化后总耗时稳定在 0.55 秒左右,远低于作业要求的 5 秒上限。
四、计算模块接口部分的单元测试
4.1 测试设计思路
基于 Python 标准 unittest 框架编写单元测试用例,覆盖正常功能、边界条件、异常输入三类场景,对计算模块的每个核心函数进行逐项验证。
4.2 核心单元测试代码示例
(1)文本清洗模块测试
- 测试函数:
clean_text(text) - 测试目标:验证函数可正确去除标点、空格、换行符等无关字符,仅保留中英文与数字。
- 数据构造思路:构造三类测试文本:包含混合标点与换行的正常文本、空文本、纯标点文本,分别验证正常清洗、边界输入、极端输入的处理效果。
代码:
import unittest
from main import clean_text
class TestCleanText(unittest.TestCase):
def test_clean_text_normal(self):
"""正常场景:混合标点、空格、换行的文本清洗"""
input_text = "你好,世界!\nHello World. 123"
self.assertEqual(clean_text(input_text), "你好世界HelloWorld123")
def test_clean_text_empty(self):
"""边界场景:空文本输入"""
self.assertEqual(clean_text(""), "")
def test_clean_text_only_punctuation(self):
"""极端场景:纯标点符号输入"""
self.assertEqual(clean_text(",。!?、"), "")
(2)余弦相似度计算模块测试
- 测试函数:
calc_cosine_similarity(text1, text2) - 测试目标:验证相似度计算逻辑的正确性,覆盖不同重合度的文本场景。
- 数据构造思路:构造四组对照文本:完全相同文本、完全无关文本、部分重复文本、空文本与正常文本,分别验证相似度极值、中间值与边界表现。
代码:
import unittest
from main import calc_cosine_similarity
class TestCosineSimilarity(unittest.TestCase):
def test_similarity_identical(self):
"""边界场景:完全相同文本,相似度应接近1"""
score = calc_cosine_similarity("今天天气很好适合出门", "今天天气很好适合出门")
self.assertAlmostEqual(score, 1.0, places=1)
def test_similarity_different(self):
"""边界场景:完全无关文本,相似度应接近0"""
score = calc_cosine_similarity("今天天气很好", "机器学习算法技术")
self.assertLess(score, 0.3)
def test_similarity_partial(self):
"""正常场景:部分重复文本,相似度应在0-1之间"""
score = calc_cosine_similarity("今天天气很好适合出去玩", "今天天气很好适合在家学习")
self.assertGreater(score, 0.5)
self.assertLess(score, 1.0)
def test_similarity_empty(self):
"""边界场景:空文本输入,相似度应为0"""
score = calc_cosine_similarity("", "测试文本内容")
self.assertEqual(score, 0.0)
(3)文件操作异常测试
- 测试函数:
read_file(file_path) - 测试目标:验证文件不存在等异常场景下的错误处理逻辑。
- 数据构造思路:传入不存在的文件路径,触发文件未找到异常,验证程序是否正常捕获并退出。
代码:
import unittest
from main import read_file
class TestFileException(unittest.TestCase):
def test_read_file_not_exist(self):
"""异常场景:读取不存在的文件"""
with self.assertRaises(SystemExit):
read_file("nonexistent_file.txt")
4.3 测试覆盖率结果
使用 coverage 工具统计单元测试代码覆盖率,结果如下:

如图所示,总代码覆盖率 89%,核心业务代码 main.py 覆盖率 77%。未覆盖部分为极端异常兜底分支,不影响核心功能的正确性与稳定性。
五.计算模块部分异常处理说明
5.1 命令行参数数量异常
- 设计目标:当用户传入的命令行参数数量不符合要求时,输出清晰的使用格式说明,引导用户正确调用程序,避免程序直接抛出晦涩的索引越界错误。
- 触发场景:运行程序时未传参数、仅传 1-2 个参数、传入 4 个及以上参数。
- 处理逻辑:在
main()函数入口处校验sys.argv的长度,若不等于 4 则打印标准使用格式python main.py 原文文件 比对文件 结果文件,随后以正常状态码退出程序。
对应单元测试样例代码:
import sys
import unittest
from unittest.mock import patch
from main import main
class TestExceptionHandle(unittest.TestCase):
def test_arg_count_error(self):
"""验证参数数量不足时,程序输出使用说明并正常退出"""
# 模拟命令行仅传入程序名,无业务参数
with patch.object(sys, 'argv', ['main.py']):
with self.assertRaises(SystemExit) as cm:
main()
# 验证退出状态码为正常退出
self.assertEqual(cm.exception.code, 1)
5.2 文件不存在异常
- 设计目标:当输入的原文文件或比对文件路径不存在时,明确提示缺失的文件路径,帮助用户快速定位输入错误,而非抛出通用文件错误。
- 触发场景:文件名拼写错误、文件路径错误、目标文件已被删除。
- 处理逻辑:在
read_file()函数中捕获FileNotFoundError异常,格式化输出错误:文件 {file_path} 不存在的提示信息,随后退出程序。
对应单元测试样例代码:
import unittest
from main import read_file
class TestExceptionHandle(unittest.TestCase):
def test_file_not_exist(self):
"""验证读取不存在的文件时,程序捕获异常并退出"""
with self.assertRaises(SystemExit) as cm:
read_file('nonexistent_original.txt')
self.assertEqual(cm.exception.code, 1)
5.3 文件读写通用异常
- 设计目标:兜底处理所有文件操作相关的其他异常,包括编码错误、无读写权限、文件损坏等场景,输出具体错误原因,保证程序的鲁棒性。
- 触发场景:文件编码非 UTF-8、程序对目标目录无写入权限、文件已损坏无法读取。
- 处理逻辑:在
read_file()和write_result()函数中,通过通用Exception捕获所有未被细分的文件操作异常,输出读取文件失败:{错误原因}或写入结果失败:{错误原因},随后退出程序。
对应单元测试样例代码:
import unittest
from main import write_result
class TestExceptionHandle(unittest.TestCase):
def test_write_failed_exception(self):
"""验证写入失败时,程序捕获异常并正常退出"""
# 向非法路径写入,触发文件操作异常
with self.assertRaises(SystemExit) as cm:
write_result('/invalid_system_path/result.txt', 0.85)
self.assertEqual(cm.exception.code, 1)

浙公网安备 33010602011771号