作业二:第一次个人编程作业
这个作业属于哪个课程 | 班级地址 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 熟悉个人开发工作 |
问题重述
- 设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率
- 要求输入输出采用文件输入输出,规范如下:
从命令行参数给出:论文原文的文件的绝对路径。
从命令行参数给出:抄袭版论文的文件的绝对路径。
从命令行参数给出:输出的答案文件的绝对路径。
整体实现
算法思路
先使用分词包将原文和目标抄袭文件进行分词,选择合适的算法将每个词语的使用次数对应到向量里 通过比较两个向量的相似度来得出两篇文章的重复率
使用的环境和安装的库
操作系统:win11家庭版
内核:python3.7.0
使用到的库:coverage、 jieba、 numpy、 psutil、 scikit_learn、 tabulate
模块接口的设计与实现过程
模块设计
整个 main.py 文件采用模块化设计,主要包含以下几个函数,没有使用面向对象的类设计。整体结构清晰,便于维护和扩展。以下是模块的组成和功能说明:
- 模块组成:
函数数量:4 个(read_text, preprocess_text, parse_args, main)。
功能划分:
read_text(file_path):处理文件读取。
preprocess_text(text):处理文本的预处理步骤。
parse_args():处理命令行参数的解析。
main():协调各个模块,组织整体逻辑。
接口设计
- 每个函数的接口设计如下:
read_text(file_path: str) -> str
输入:文件路径
输出:读取的文本内容
异常:未找到文件或读取错误时抛出异常
preprocess_text(text: str) -> str
输入:原始文本字符串
输出:处理后的文本字符串
异常:处理时发生的错误将抛出异常
parse_args() -> Namespace
输入:无
输出:命令行参数的命名空间对象
main() -> None
输入:无
输出:无(执行该函数会产生副作用,包括文件读写和控制台输出)
异常:文件未找到、文本处理错误等
主函数关系:
main() 是程序的入口,协调其他所有函数。
main() 首先调用 parse_args() 获取命令行参数,然后调用 read_text() 来读取原始文件和比较文件。
接下来,调用 preprocess_text() 对读取的文本进行处理。
最后,计算文本的相似度,输出结果并写入文件。
算法的关键与独到之处
-
文本预处理
在 preprocess_text() 函数中,使用正则表达式去除标点符号,结合jieba库进行分词。这种预处理方法能够有效地将原始文本转换为适合后续处理的格式。 -
TF-IDF 与余弦相似度计算
使用 TfidfVectorizer 计算TF-IDF值,能够有效处理文本的权重问题,减少常见词的影响,使得较为独特的词更加重要。
使用余弦相似度计算的优点是可以有效地衡量两个文本的相似性,尤其在处理高维稀疏数据(如文本)时效果良好。
异常处理:通过详细的异常捕获以保证程序的负责性和鲁棒性,用户在使用时可获得有用的错误信息,有助于调试。 -
灵活性与扩展性
该模块化设计使得将来可以方便地添加新功能(如支持更多的文本预处理技术或者新的相似度计算方法)而不影响现有的功能实现
项目结构
改进计算模块性能的思路
为了改善计算模块的性能,主要从以下几个方面进行了优化:
文本预处理优化:
- 在 preprocess_text() 中,使用高效的正则表达式来去除标点符号,与 jieba 的分词功能结合,尽量减少临时字符串的生成,优化文本处理速度。分析停用词的影响,考虑在预处理过程中动态加载或更新停用词列表,以减少在数据预处理中的重复检查操作。
- TF-IDF 计算优化:
使用 TfidfVectorizer() 的特性如 sublinear_tf=True,以实现更好的性能,因为它将使用对数频率而不仅仅是单纯的词频。 - 进行小规模的文本集测试,选取核心文本,从而减少计算量和资源消耗。
- 余弦相似度计算:
在 calculate_similarity() 方法中,确保调用 cosine_similarity() 时,仅计算需要的部分,避免计算不必要的矩阵。
性能分析图
各模块耗时占比
对cpu的使用率为
各模块占用cpu情况--火焰图
纵向(Y 轴)高低不平,表示的是函数调用栈的深度。每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。
横向(X 轴)表示该函数执行消耗的时间,横向上会按照字母顺序排序,而且如果是同样的调用会做合并(注意:如果一个函数在 X 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长,所以这里不是严格意义上的执行消耗的时间),所以一个横向宽度越大的函数调用,一般很可能是程序的瓶颈。
火焰图的颜色是随机分配的,并不是颜色越深就是越瓶颈。因为火焰图表示的是 CPU 的繁忙程度,所以一般都是暖色调。我们需要留意的就是那些比较宽大的火苗。只要有"平顶",就表示该函数可能存在性能问题。
质量分析
- 刚开始存在很多性能问题
- 第一次优化后
- 第二次优化处理后
单元测试
折叠代码块
import unittest from unittest.mock import patch, mock_open from main import read_text, preprocess_text, main, parse_args import argparse import jieba import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from tabulate import tabulate
class TestMainFunctions(unittest.TestCase):
def setUp(self): self.results = [] def tearDown(self): # 将结果写入 report.txt 并打印到控制台 with open('report.txt', 'w', encoding='utf-8') as f: for result in self.results: f.write('\t'.join(result) + '\n') # 写入文件,使用制表符分隔 print(f"测试情景: {result[0]}, 测试结果: {result[1]}, 异常原因: {result[2]}, 重复率: {result[3]}") def calculate_similarity(self, text1, text2): return cosine_similarity(self._tfidf_transform([text1, text2]))[0][1] def _tfidf_transform(self, texts): vectorizer = TfidfVectorizer() return vectorizer.fit_transform(texts).toarray() # 测试参数解析错误 def test_parse_args_error(self): with self.assertRaises(SystemExit): parse_args() self.results.append(["解析参数失败", "成功", "参数解析时应引发系统退出", ""]) print("测试参数解析错误: 成功") # 测试读取路径不存在 def test_read_text_file_not_found(self): with self.assertRaises(FileNotFoundError) as context: read_text('non_existent_file.txt') self.assertTrue("文件 non_existent_file.txt 未找到,请检查文件路径。" in str(context.exception)) self.results.append(["读取不存在的文件", "失败", "文件未找到", ""]) print("测试读取路径不存在: 失败, 原因:", str(context.exception)) # 测试预处理文本是否去除标点符号 def test_preprocess_text_remove_punctuation(self): result = preprocess_text("测试文本,应该去除标点。") self.assertEqual(result, "测试文本 应该去除标点", "标点符号未正确去除") self.results.append(["预处理文本去除标点", "成功", "", ""]) print("测试预处理文本去除标点: 成功") # 测试停用词是否正确去除 def test_preprocess_text_remove_stopwords(self): result = preprocess_text("这是一个测试文本,我们的目标是去除停用词。") self.assertEqual(result, "测试文本 目标 去除停用词", "停用词未正确去除") self.results.append(["预处理文本去除停用词", "成功", "", ""]) print("测试预处理文本去除停用词: 成功") # 测试jieba分词是否正常 def test_jieba_segmentation(self): text = "我喜欢学习人工智能。" words = list(jieba.cut(text)) self.assertIn("学习", words, "jieba未正确分词") self.results.append(["jieba分词测试", "成功", "", ""]) print("测试jieba分词: 成功") # 测试对英文文章的分词 def test_jieba_for_english_text(self): text = "I love studying Artificial Intelligence." words = list(jieba.cut(text)) self.assertIn("Artificial", words, "jieba对英文文本未正确分词") self.results.append(["jieba对英文文本测试", "成功", "", ""]) print("测试jieba对英文文本分词: 成功") # 测试主函数成功运行 def test_main_success(self): with patch('main.parse_args', return_value=argparse.Namespace(orig_file='orig_file', compare_file='compare_file', output_file='output_file')), \ patch('main.read_text', side_effect=["test content 1", "test content 2"]), \ patch('main.preprocess_text', side_effect=["test content 1", "test content 2"]), \ patch('builtins.open', mock_open()) as mock_file: main() mock_file.assert_called_once_with('output_file', 'w', encoding='utf-8') similarity = self.calculate_similarity("test content 1", "test content 2") self.results.append(["main 函数成功运行", "成功", "", f"{similarity:.2f}"]) print("主函数运行测试: 成功, 相似度为:", f"{similarity:.2f}") # 测试空文本的处理 def test_empty_text_processing(self): result = preprocess_text("") self.assertEqual(result, "", "空文本处理未返回空字符串") self.results.append(["空文本处理", "成功", "", ""]) print("空文本处理测试: 成功") # 测试TF-IDF算法 def test_tfidf_transformation(self): texts = ["这是一个测试文本。", "这是另一个测试文本。"] tfidf_matrix = self._tfidf_transform(texts) self.assertEqual(tfidf_matrix.shape, (2, 5), "TF-IDF矩阵形状不正确") self.results.append(["TF-IDF算法执行成功", "成功", "", ""]) print("TF-IDF算法测试: 成功, 矩阵形状为:", tfidf_matrix.shape) # 测试余弦相似度计算 def test_cosine_similarity(self): text1 = "我喜欢学习机器学习。" text2 = "机器学习是我的最爱。" similarity = self.calculate_similarity(text1, text2) self.assertGreaterEqual(similarity, 0, "余弦相似度计算错误,应大于或等于0") self.results.append(["余弦相似度计算成功", "成功", "", f"{similarity:.2f}"]) print("余弦相似度计算测试: 成功, 相似度为:", f"{similarity:.2f}")
if name == 'main':
unittest.main()
测试报告:
使用pytest和coverage库得到主函数和测试函数的覆盖率:
异常处理
测试情景 | 测试结果 | 异常原因 | 重复率 |
---|---|---|---|
解析参数失败 | 成功 | 参数解析时应引发系统退出 | |
读取不存在的文件 | 失败 | 文件 'non_existent_file.txt' 未找到,请检查文件路径。 | |
预处理文本去除标点 | 成功 | ||
预处理文本去除停用词 | 成功 | ||
jieba 分词测试 | 成功 | ||
jieba 对英文文本分词测试 | 成功 | ||
主函数运行测试 | 成功 | 相似度为: 0.50 | 0.50 |
空文本处理测试 | 成功 | ||
TF-IDF 算法执行成功 | 成功 | ||
余弦相似度计算成功 | 成功 | 相似度为: 0.75 | 0.75 |
总结表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 20 |
Estimate | 估计这个任务需要多少时间 | 300 | 320 |
Development | 开发 | 150 | 200 |
Analysis | 需求分析 (包括学习新技术) | 50 | 30 |
Design Spec | 生成设计文档 | 10 | 20 |
Design Review | 设计复审 | 20 | 20 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
Design | 具体设计 | 20 | 20 |
Coding | 具体编码 | 100 | 100 |
Code Review | 代码复审 | 50 | 50 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 33 |
Reporting | 报告 | 20 | 17 |
Test Report | 测试报告 | 10 | 10 |
Size Measurement | 计算工作量 | 5 | 5 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 5 | 5 |
总计 | 600 | 680 |