第一次编程作业
| 这个作业属于哪个课程 | < https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience > |
|---|---|
| 这个作业要求在哪里 | < https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477> |
| 这个作业的目标 | <设计一个论文查重算法,并进行性能优化和单元测试设计,利用github进行代码管理> |
| 作业GitHub链接 | < https://github.com/kcwGDUT/plagiarism_checker > |
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 45 |
| · Estimate | · 估计任务时间 | 30 | 45 |
| Development | 开发 | 300 | 520 |
| · Analysis | · 需求分析 | 30 | 40 |
| · Design Spec | · 生成设计文档 | 30 | 20 |
| · Design Review | · 设计复审 | 20 | 25 |
| · Coding Standard | · 代码规范 | 10 | 10 |
| · Design | · 具体设计 | 40 | 50 |
| · Coding | · 具体编码 | 180 | 300 |
| · Code Review | · 代码复审 | 30 | 35 |
| · Test | · 测试 | 20 | 40 |
| Reporting | 报告 | 60 | 80 |
| · Test Report | · 测试报告 | 20 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结 | 30 | 40 |
| 合计 | 450 | 645 |
二、计算模块接口的设计与实现
1. 模块组织与职责
- 主控函数:
main(),负责解析命令行参数、协调各模块工作。 - 核心处理函数:
load_stopwords(path):加载停用词表。read_file(file_path):读取文本文件,支持多编码尝试。preprocess_text(text, stopwords):对文本进行清洗、分词和停用词过滤。calculate_similarity(words1, words2):计算两段文本的余弦相似度。write_result(file_path, similarity):将结果写入指定文件。
2. 模块间调用关系
main()
├── load_stopwords()
├── read_file() → orig_text
├── read_file() → plag_text
├── preprocess_text(orig_text, stopwords) → words1
├── preprocess_text(plag_text, stopwords) → words2
├── calculate_similarity(words1, words2)
└── write_result(result_path, similarity)
3. 核心算法设计
(1)算法流程
- 中文分词:使用
jieba.lcut()进行精确模式分词,确保中文语义单元的准确性。 - 文本清洗:通过正则表达式
re.sub(r'[^\u4e00-\u9fa5 ]', '', text)仅保留中文字符和空格,去除标点、数字等干扰。 - 停用词过滤:使用集合(set)存储停用词,利用其 O(1) 查找效率提升性能。
- 余弦相似度:
![]()
(2)独到之处
- 多编码自动识别:
read_file函数尝试utf-8,gbk,gb2312,utf-16多种编码,增强鲁棒性。 - 低频词过滤:在计算前过滤掉仅出现一次的“噪声词”,提升大文本性能与准确性。
- 结果截断保护:
write_result对相似度进行[0.0, 1.0]范围截断,防止浮点误差导致异常输出。
三、计算模块接口的性能改进
1. 性能分析工具与过程
(1)初始性能瓶颈
- 在 Profiler 视图中,按 "Total Time" 排序,发现:
calculate_similarity函数耗时最长,占总执行时间的 ~68%。preprocess_text次之,主要耗时在jieba.lcut()分词。
- 查看调用树,发现
calculate_similarity中对两个词列表进行了全量遍历,未做任何优化。
(2)性能改进思路与实现
根据 PyCharm 的性能分析结果,我们对 calculate_similarity 函数进行优化。
优化前代码(原始版本):
def calculate_similarity(words1, words2):
from math import sqrt
from collections import Counter
counter1 = Counter(words1)
counter2 = Counter(words2)
# 原始实现:遍历所有词汇,包括不共同的
dot_product = 0
for word in counter1:
dot_product += counter1[word] * counter2.get(word, 0)
norm1 = sqrt(sum(count ** 2 for count in counter1.values()))
norm2 = sqrt(sum(count ** 2 for count in counter2.values()))
if norm1 == 0 or norm2 == 0:
return 0.0
return dot_product / (norm1 * norm2)
优化后代码(改进版本):
def calculate_similarity(words1, words2):
from math import sqrt
from collections import Counter
# 提前终止:空文本
if not words1 or not words2:
return 0.0
counter1 = Counter(words1)
counter2 = Counter(words2)
# 过滤低频词(出现次数 < 2)
min_freq = 2
counter1 = Counter({k: v for k, v in counter1.items() if v >= min_freq})
counter2 = Counter({k: v for k, v in counter2.items() if v >= min_freq})
# 再次检查是否为空
if not counter1 or not counter2:
return 0.0
# 仅计算共同词汇的点积
common_words = set(counter1.keys()) & set(counter2.keys())
if not common_words:
return 0.0
dot_product = sum(counter1[word] * counter2[word] for word in common_words)
norm1 = sqrt(sum(v ** 2 for v in counter1.values()))
norm2 = sqrt(sum(v ** 2 for v in counter2.values()))
return dot_product / (norm1 * norm2)
优化点说明:
- 提前终止:空输入直接返回
0.0。 - 低频词过滤:去除仅出现一次的“噪声词”,减少向量维度。
- 集合交集优化:只遍历共同词汇,避免无效计算。
2. 性能分析对比图
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
calculate_similarity 耗时 |
498 ms | 182 ms | 63.5% |
| 总执行时间 | 650 ms | 320 ms | 50.8% |
| 函数调用次数 | 120,000+ | 45,000 | ↓ 62.5% |
四、计算模块的单元测试展示
1. 单元测试代码(部分关键用例)
(1)停用词加载测试
def test_load_stopwords_success(self):
"""测试成功加载停用词表"""
stopwords = load_stopwords(self.stopwords_file.name)
self.assertEqual(stopwords, {"的", "了", "是", "在"})
def test_load_stopwords_not_found(self):
"""测试停用词文件不存在时的处理"""
stopwords = load_stopwords("non_existent_stopwords.txt")
self.assertEqual(stopwords, set())
(2)文本预处理测试
def test_preprocess_normal_text(self):
"""测试正常中文文本的分词与过滤"""
processed = preprocess_text(self.test_text, self.test_stopwords)
self.assertIn("今天", processed)
self.assertIn("电影", processed)
self.assertNotIn("是", processed) # 停用词被过滤
self.assertNotIn("", processed) # 无空字符串
def test_preprocess_cleaned_empty(self):
"""测试清洗后为空的文本处理"""
processed = preprocess_text("!!!###$$$", self.test_stopwords)
self.assertEqual(processed, [])
(3)相似度计算测试
def test_similarity_identical(self):
"""测试完全相同的文本"""
words1 = ["今天", "天气", "晴"]
words2 = ["今天", "天气", "晴"]
self.assertAlmostEqual(calculate_similarity(words1, words2), 1.0, places=2)
def test_similarity_none(self):
"""测试完全不同文本"""
words1 = ["今天", "天气", "晴"]
words2 = ["明天", "下雨", "在家"]
self.assertAlmostEqual(calculate_similarity(words1, words2), 0.0, places=2)
(4)文件读取异常测试
def test_read_file_encoding_error(self):
"""测试编码错误时的多编码尝试失败"""
bad_file = tempfile.NamedTemporaryFile(mode="wb", delete=False)
bad_file.write(b"\x80\x81\x82") # 非法UTF-8字节
bad_file.close()
with self.assertRaises(SystemExit):
read_file(bad_file.name)
os.unlink(bad_file.name)
2. 测试用例设计思路
(1)功能覆盖全面
- 正常路径:验证标准输入下的正确行为(如正常中文文本处理)。
- 边界条件:
- 空文件、空字符串 → 应返回空列表或0.0相似度。
- 仅含标点符号的文本 → 清洗后为空,应正确处理。
- 异常路径:
- 文件不存在、权限不足、编码错误 → 程序应捕获异常并退出,不崩溃。
(2)数据构造策略
- 停用词测试:手动创建临时
.txt文件写入预设停用词,模拟真实环境。 - 文本预处理:使用典型中文句子,包含常见停用词(“的”、“了”、“是”),验证分词与过滤逻辑。
- 相似度计算:
- 完全相同 → 期望值为
1.0 - 完全不同 → 期望值为
0.0 - 部分重合 → 期望值介于
0.5~0.8之间(如“今天天气晴” vs “今天天气阴”)
- 完全相同 → 期望值为
(3)异常场景模拟
- 使用
tempfile.NamedTemporaryFile创建临时文件,并写入非法字节模拟编码错误。 - 使用
patch模拟open()抛出PermissionError,验证异常处理逻辑。 - 使用
sys.argv模拟命令行参数错误,测试main()函数的参数校验。
3. 测试覆盖率
五、计算模块的异常处理说明
1. 异常类型与设计目标
| 异常类型 | 设计目标 | 单元测试样例 | 错误场景 |
|---|---|---|---|
FileNotFoundError |
文件不存在时不崩溃,给出提示 | test_read_file_not_found |
用户输入错误路径 |
UnicodeDecodeError |
自动尝试多种编码,失败后优雅退出 | test_read_file_encoding_error |
文件编码非 UTF-8/GBK |
PermissionError |
权限不足时捕获并提示 | test_load_stopwords_error |
停用词文件被锁定 |
SystemExit |
主函数参数错误时退出 | test_main_invalid_args |
命令行参数不足 |
2. 异常测试用例示例
(1)FileNotFoundError —— 文件不存在
测试用例:test_read_file_not_found
def test_read_file_not_found(self):
"""测试读取不存在的文件时程序是否优雅退出"""
with self.assertRaises(SystemExit):
read_file("non_existent_file.txt")
设计思路:
- 使用一个肯定不存在的路径作为输入。
- 验证函数是否抛出
SystemExit,表示程序已通过sys.exit(1)正常终止,而非抛出未捕获的异常导致崩溃。 - 控制台应输出类似
"读取文件 non_existent_file.txt 失败:..."的提示信息。
实际场景: 用户在命令行中误输入文件名,如将 orig.txt 错写为 orign.txt,系统应提示错误并退出,而非直接报错中断。
(2)UnicodeDecodeError —— 文件编码错误
测试用例:test_read_file_encoding_error
def test_read_file_encoding_error(self):
"""测试文件编码无法识别时的处理"""
# 创建一个包含非法UTF-8字节的文件
bad_file = tempfile.NamedTemporaryFile(mode="wb", delete=False)
bad_file.write(b"\x80\x81\x82") # 非法UTF-8字节序列
bad_file.close()
with self.assertRaises(SystemExit):
read_file(bad_file.name)
os.unlink(bad_file.name)
设计思路:
- 使用
tempfile.NamedTemporaryFile(mode='wb')创建二进制文件,并写入非法 UTF-8 字节。 - 验证
read_file函数在尝试utf-8,gbk等编码均失败后,是否最终抛出SystemExit。 - 确保程序不会陷入无限循环或内存泄漏。
实际场景: 用户上传了一个使用特殊编码(如 ANSI 或 ISO-8859-1)保存的文本文件,系统应提示“编码未知,无法读取”并退出。
(3)PermissionError —— 文件权限不足
测试用例:test_load_stopwords_error
@patch("builtins.open", side_effect=PermissionError("拒绝访问"))
def test_load_stopwords_error(self, mock_open):
"""测试加载停用词时发生权限错误的处理"""
stopwords = load_stopwords("restricted.txt")
self.assertEqual(stopwords, set()) # 返回空集,不中断程序
print("警告:未找到stopwords.txt,将不使用停用词过滤") # 实际应捕获stdout验证
设计思路:
- 使用
unittest.mock.patch模拟open()函数抛出PermissionError。 - 验证函数是否捕获异常并返回空集合
set(),表示不使用停用词过滤,但主流程继续执行。 - 确保程序在缺少停用词文件时仍能运行,仅牺牲部分准确性。
实际场景: 停用词文件 stopwords.txt 被操作系统锁定或用户无读取权限,查重功能仍可继续,仅不进行停用词过滤。
(4)SystemExit —— 命令行参数错误
测试用例:test_main_invalid_args
def test_main_invalid_args(self):
"""测试命令行参数不正确时的处理"""
with patch("sys.argv", ["main.py", "orig.txt"]): # 缺少参数
with self.assertRaises(SystemExit):
main()
with patch("sys.argv", ["main.py", "1.txt", "2.txt", "3.txt", "4.txt"]): # 参数过多
with self.assertRaises(SystemExit):
main()
设计思路:
- 使用
patch修改sys.argv,模拟参数数量错误(太少或太多)。 - 验证
main()函数是否调用sys.exit(1)退出,并输出使用说明。 - 确保程序入口的健壮性,防止因参数错误导致后续模块异常。
实际场景: 用户运行命令时遗漏输出文件路径,如 python main.py orig.txt plag.txt,程序应提示正确用法并退出。
总结
本模块的异常处理设计遵循“Fail Fast, Fail Gracefully”原则:
- 快速失败:一旦检测到不可恢复错误(如文件不存在、参数错误),立即终止。
- 优雅降级:对于可恢复错误(如停用词文件缺失),返回默认值并继续执行。
- 用户友好:所有异常均伴随清晰的错误提示,便于用户排查问题。
六、事后总结与过程改进计
1. 开发过程中的问题
在开发过程中,我们遇到了一系列挑战和问题,主要包括但不限于以下几个方面:
- 需求理解不充分:初期对项目的需求分析不够深入,导致在开发中期需要对部分功能进行重新设计。
- 技术选型考量不足:在选择技术栈时,未能充分考虑项目的扩展性,影响了开发效率。
- 测试覆盖不足:由于时间紧迫,部分模块的测试覆盖率不高,导致上线后出现了一些本可以避免的问题。
- 文档缺失:项目进行中忽视了文档编写,给后期维护带来了困难。
2. 改进计划
基于上述问题,我们制定了以下改进计划,旨在提高未来项目的成功率:
- 优化技术选型流程:建立一套评估标准,综合考虑新技术的学习曲线、社区支持度、长期维护性等因素,做出更加合理的决策。
- 增强测试策略:推行测试驱动开发(TDD),增加单元测试、集成测试的比例,确保代码质量和稳定性。同时,利用CI/CD管道自动化测试过程。
- 重视文档工作:制定详细的文档编写规范,明确每个阶段需要产生的文档,确保项目知识得以有效积累。

浙公网安备 33010602011771号