第一次个人编程作业
| 这个作业属于哪个课程 | 计科23级12班 |
|---|---|
| 这个作业要求在哪里 | 【作业2】个人项目 |
| 这个作业的目标 | 掌握 GitHub 及 Git 的使用方法, , 学习性能分析和单元测试, 积累个人编程项目的经验 |
📎Github链接:https://github.com/zhiyuxinxuan/zhiyuxinxuan/tree/main/3223004340
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 35 | 30 |
| · Estimate | 估计这个任务需要多少时间 | 35 | 30 |
| Development | 开发 | 400 | 520 |
| · Analysis | 需求分析 (包括学习新技术) | 70 | 80 |
| · Design Spec | 生成设计文档 | 45 | 40 |
| · Design Review | 设计复审 | 30 | 25 |
| · Coding Standard | 代码规范 (制定开发规范) | 15 | 15 |
| · Design | 具体设计 | 40 | 45 |
| · Coding | 具体编码 | 120 | 150 |
| · Code Review | 代码复审 | 35 | 35 |
| · Test | 测试(自我测试,修改代码,提交修改) | 45 | 130 |
| Reporting | 报告 | 120 | 200 |
| · Test Report | 测试报告 | 70 | 90 |
| · Size Measurement | 计算工作量 | 15 | 15 |
| · Postmortem | 事后总结, 并提出过程改进计划 | 35 | 95 |
| Total | 合计 | 555 | 750 |
二、模块接口的设计与实现过程
本论文查重模块采用函数式编程架构,未设计类(因核心逻辑以独立数据处理流程为主,无需状态维护),共包含 8 个核心函数
核心函数
1. read_file函数
- 功能:读取指定路径的文本文件,处理 “文件不存在”“IO 读取失败” 两类异常,确保数据输入有效性。
- 接口参数:
file_path: str(文件路径) - 返回值:
str(文件内容,已去除首尾空白) - 相对独立,主要与操作系统的文件系统交互,不依赖于其他自定义函数的内部状态,仅在
main函数中被调用获取原始文本和抄袭文本。
2. preprocess函数
- 功能:对原始文本进行标准化处理,为后续相似度计算提供高质量词序列。
- 接口参数
text: str(待处理文本)use_stopwords: bool = True(是否使用停用词过滤,默认开启)
- 返回值:
list[str](处理后的词列表,已过滤空词、数字、停用词) - 关键逻辑
- 调用
jieba.lcut(text, cut_all=False)进行精确分词; - 基于全局变量
GLOBAL_STOPWORDS过滤停用词,支持动态扩展停用词表; - 过滤纯数字词(避免 “2023”“100” 等无意义数字影响相似度计算)。
- 调用
3. jaccard_similar函数
- 功能:计算两个词集合的 Jaccard 相似度(交集 / 并集),用于快速筛选低相似度文本(初筛环节)。
- 接口参数:
set1: set[str]、set2: set[str](两个文本的词集合) - 返回值:
float(Jaccard 相似度,范围 [0,1],并集为空时返回 0.0)
4. _calculate_vectors函数
- 功能:基于两个文本的词序列,构建全局词汇表并生成词频向量(为余弦相似度计算提供输入)。
- 接口参数:
orig_tokens: list[str]、copy_tokens: list[str](原始 / 抄袭文本的词序列) - 返回值:
tuple[np.ndarray, np.ndarray](原始文本向量、抄袭文本向量) - 关键逻辑
- 用
collections.Counter统计词频; - 词汇表取两个文本词的并集,确保向量维度一致;
- 缺失词的词频记为 0,生成 numpy 数组格式的向量。
- 用
5. cosine_similar函数
- 功能:计算两个词频向量的余弦相似度,衡量文本词频分布的相似性。
- 接口参数:
vec1: np.ndarray、vec2: np.ndarray(两个文本的词频向量) - 返回值:
float(余弦相似度,范围 [0,1],向量全零时返回 0.0) - 优化处理:分母加
1e-8避免除以零错误,提升数值稳定性。
6. lcs_similar函数
- 功能:计算两个词序列的最长公共子序列(LCS)相似度,衡量文本语序的相似性。
- 接口参数:
seq1: list[str]、seq2: list[str](两个文本的词序列) - 返回值:
float(LCS 相似度,范围 [0,1],由difflib.SequenceMatcher.ratio()实现)
7. hybrid_similarity(核心流程函数)
- 功能:整合所有基础算法,实现 “预处理→初筛→多算法融合” 的完整查重逻辑,输出综合相似度。
- 接口参数:
orig_text: str(原始文本)、copy_text: str(抄袭文本) - 返回值:
float(综合相似度,范围 [0,1])
8. main(入口函数)
- 功能:解析命令行参数,调度
read_file和hybrid_similarity,将结果写入指定文件。 - 接口参数:无(依赖
sys.argv获取命令行参数) - 参数校验:若命令行参数数量不为 4(原文路径、抄袭文本路径、结果路径),打印正确格式并退出。
- 结果输出:将综合相似度保留 2 位小数,写入结果文件,同时在控制台打印结果路径与相似度。
流程图
算法关键与独到之处
1. 分层预处理策略
- 首次预处理(
use_stopwords=True):过滤停用词和数字,聚焦核心语义词,用于 Jaccard 初筛(减少无意义词对初筛结果的干扰);
- 二次预处理(
use_stopwords=False):保留全部有效词(除数字),用于余弦相似度和 LCS 计算(确保词频分布、语序的完整性)。
2. “初筛 + 融合” 的高效计算流程
- 初筛环节:用 Jaccard 相似度(计算复杂度 O (n))快速过滤相似度低于 0.05 的文本,避免后续高复杂度计算(如向量生成、LCS),提升整体效率;
- 融合环节:结合 “词频分布”(余弦相似度,权重 0.6)和 “语序结构”(LCS 相似度,权重 0.4),既考虑词汇重复度,也兼顾文本结构相似性,结果更全面。
3. 灵活的停用词机制
- 全局变量
GLOBAL_STOPWORDS包含标点、常用无意义词(如 “的”“了”),支持根据场景扩展(如学术场景可添加 “摘要”“关键词” 等); - 通过
use_stopwords参数实现 “按需过滤”,避免单一预处理方式的局限性。
4. 数值稳定性优化
- 余弦相似度计算中,分母添加
1e-8,避免因向量全零导致的除以零错误; - Jaccard 相似度中,判断并集为空时返回 0.0,处理 “两个文本均无有效词” 的极端场景。
三、模块接口部分的性能改进
代码质量分析

使用了三种常用的 Python 代码质量分析工具对 main.py 和 test.py 这两个文件代码风格、整体质量与可维护性、静态类型等进行分析:
1. flake8 分析
flake8 是一个集成工具,结合了代码风格检查(如 PEP8 规范)、代码复杂度分析等功能。
- 命令
flake8 main.py test.py执行后,没有输出任何错误或警告信息,说明这两个文件在代码风格(如缩进、换行、命名规范等)、基本语法等方面符合flake8的检查要求,没有明显的风格或简单语法问题。
2. pylint 分析
pylint 是一款功能强大的代码分析工具,能检查代码的可读性、可维护性、潜在错误等多个维度,并给出综合评分(满分 10 分)。
- 对
main.py分析后,输出Your code has been rated at 10.00/10,说明main.py在代码结构、命名规范、逻辑清晰度、潜在错误等各方面都达到了pylint的最高标准。 - 对
test.py分析后,同样输出Your code has been rated at 10.00/10,表明test.py也在代码质量的各个维度表现优异。
3. mypy 分析
mypy 是用于 Python 静态类型检查的工具,能检查代码中变量、函数等的类型注解是否正确,是否存在类型不匹配等问题。
- 命令
mypy main.py test.py执行后,输出Success: no issues found in 2 source files,说明这两个文件的静态类型相关部分(如类型注解、类型使用等)没有问题,类型检查通过。
性能分析
- 改进前性能分析图:

1. 性能瓶颈
- 文本预处理:
preprocess函数中,停用词集合较小且定义在函数内部,每次调用都需重新构建,同时分词后过滤逻辑简单(仅过滤词长),导致预处理后词序列 “噪声” 较多,为后续相似度计算带来额外开销;jieba.lcut分词操作本身因涉及复杂词库匹配,在处理长文本时耗时较久。 - 向量计算:构建词频向量时,使用列表推导式结合
count方法统计词频,时间复杂度为O(n²)(n为词汇表大小),当文本词汇量较大时,该操作会成为性能热点。 - 流程与算法:Jaccard 初筛阈值较高(
0.2),导致较多低相似度文本仍需进入后续余弦相似度和 LCS 相似度计算流程,增加了不必要的计算量;余弦和 LCS 权重相同,未充分利用更能体现语义关联的余弦相似度的计算效率优势。
2. 消耗最大的函数
jieba.lcut(在preprocess函数中调用)是消耗最大的函数。分词操作涉及中文词库的复杂匹配与语法分析,尤其是处理长文本时,计算开销大,且原代码中预处理环节的其他低效操作(如停用词处理、词频统计)进一步放大了其性能影响。
- 改进后性能分析图

1. 性能优化效果
针对找到的性能瓶颈进行优化:
- 文本预处理:全局
GLOBAL_STOPWORDS集合使停用词过滤更高效(O(1)查找),且过滤数字等操作让词序列更简洁;jieba.lcut使用精确模式,在保证分词质量的同时,配合预处理其他环节的优化,降低了其在整体耗时中的占比。 - 向量计算:
collections.Counter的使用将词频统计时间复杂度从O(n²)优化到O(m)(m为文本长度),词汇表生成通过集合并集操作也更高效,向量计算环节的耗时大幅减少。 - 流程与算法:更低的 Jaccard 初筛阈值(
0.05)提前过滤了更多低相似度文本,减少后续高开销计算;余弦相似度权重提高(0.6),使其在综合相似度中占比更大,而余弦相似度计算因向量生成的优化,整体流程效率提升。
2. 消耗最大的函数
尽管进行了诸多优化,jieba.lcut仍为消耗最大的函数。但相较于改进前,其耗时占比有所降低。这是因为分词操作本身的算法复杂度决定了其必然是计算热点,不过预处理、向量计算等环节的优化,使得整体流程中其他部分耗时减少,从而让分词的耗时占比相对突出,但绝对耗时已得到一定控制。
四、模块部分单元测试
- 单元测试覆盖率:

1. 基础功能验证
-
测试
preprocess函数:- 思路:通过
test_preprocess_with_stopwords和test_preprocess_without_stopwords测试preprocess函数使用和不使用停用词的预处理效果,测试是否能够正确过滤停用词以及进行分词。
def test_preprocess_with_stopwords(self): """测试带停用词的文本预处理""" raw_text = "今天是星期天,天气晴,今天晚上我要去看电影。" expected = ["星期天", "天气", "晴", "晚上", "电影"] result = preprocess(raw_text, use_stopwords=True) self.assertEqual(result, expected) def test_preprocess_without_stopwords(self): """测试不带停用词的文本预处理""" raw_text = "今天是星期天,天气晴。" expected = ["今天", "是", "星期天", ",", "天气", "晴", "。"] result = preprocess(raw_text, use_stopwords=False) self.assertEqual(result, expected) - 思路:通过
-
测试
hybrid_similarity函数:test_hybrid_long_texts测试用例- 思路:构造两段长文本,原文和抄袭文本的文本结构相似但用词略有差异,符合 “部分改写抄袭” 的真实场景,测试综合相似度是否落在合理区间(0.6~0.9)。
def test_hybrid_long_texts(self): """测试长文本的混合相似度计算""" orig = """ 自然语言处理是人工智能的一个重要分支,它研究计算机与人类语言的交互。 主要任务包括机器翻译、情感分析、文本摘要等。近年来,基于Transformer的模型 在自然语言处理领域取得了突破性进展。 """ copy = """ 自然语言处理作为人工智能的重要领域,专注于计算机与人类语言的交互方式。 其主要任务有机器翻译、情感分析和文本摘要等。最近几年,基于Transformer的 模型在该领域获得了显著进步。 """ self.assertTrue(0.6 < hybrid_similarity(orig, copy) < 0.9)test_hybrid_identical_text测试用例- 思路:构造了完全相同的原始文本和抄袭文本。测试当两个文本完全相同时,是否能正确返回接近1.0 的相似度。
def test_hybrid_identical_text(self): """测试完全相同文本的混合相似度计算""" orig = "今天是星期天,天气晴,今天晚上我要去看电影。" copy = "今天是星期天,天气晴,今天晚上我要去看电影。" self.assertAlmostEqual(round(hybrid_similarity(orig, copy), 2), 1.00)test_hybrid_no_similarity测试用例- 思路:构造两个完全不同的原始文本和抄袭文本,测试当两个文本完全不同时,是否能正确返回接近0.0的相似度。
def test_hybrid_no_similarity(self): """测试完全不相似文本的混合相似度计算""" orig = "人工智能是计算机科学的一个分支" copy = "天气晴朗适合户外活动" self.assertEqual(round(hybrid_similarity(orig, copy), 2), 0.00)test_jaccard_pass_cosine_zero测试用例- 思路:构造了内容不同但有一定相似性的原始文本和抄袭文本,测试当Jaccard初筛通过,但余弦相似度较低的情况下,
hybrid_similarity函数是否能正确返回小于1.00的相似度,是对hybrid_similarity函数中综合相似度计算,特别是余弦相似度部分的逻辑测试。
def test_jaccard_pass_cosine_zero(self): """测试Jaccard初筛通过但余弦相似度低的场景""" orig_text = "我喜欢苹果" copy_text = "我喜欢香蕉" similarity = hybrid_similarity(orig_text, copy_text) self.assertTrue(similarity < 1.00)test_jaccard_pass_lcs_zero测试用例
- 思路: 构造了内容不同的原始文本和抄袭文本,测试当Jaccard初筛通过,但最长公共子序列(LCS)相似度较低时,函数是否能正确返回小于1.00的相似度,是对
hybrid_similarity函数中综合相似度计算,特别是LCS相似度部分的逻辑测试。
def test_jaccard_pass_lcs_zero(self): orig_text = "红色的花朵" copy_text = "蓝色的天空" similarity = hybrid_similarity(orig_text, copy_text) self.assertEqual(type(similarity), float) self.assertTrue(similarity < 1.00)test_cosine_special_case测试用例
- 思路:构造了仅包含标点符号的原始文本和抄袭文本,测试在特殊的、几乎没有实际内容的输入下,函数是否能正确返回小于1.00的相似度,小于0.01进一步约束极端低相似度的合理性,验证是否符合 “无实际语义关联” 的预期
def test_cosine_special_case(self): """测试纯标点文本的混合相似度计算(特殊输入场景)""" orig_text = ",,,,,,,,,," copy_text = "。。。。。。。。。。" similarity = hybrid_similarity(orig_text, copy_text) self.assertEqual(type(similarity), float) self.assertTrue(similarity < 1.00) self.assertTrue(similarity <= 0.1)test_jaccard_special_case测试用例
- 思路:构造了仅包含特殊符号的原始文本和抄袭文本,测试在特殊符号组成的文本输入下,函数是否能正确返回0.00的相似度。
def test_jaccard_special_case(self): orig_text = "####" copy_text = "$$$$" similarity = hybrid_similarity(orig_text, copy_text) self.assertEqual(round(similarity, 2), 0.00)test_hybrid_special_characters测试用例- 思路:构造两段含特殊字符(如
【】、\n、、)的文本,测试在 “含特殊字符文本” 场景下的综合计算合理性。
def test_hybrid_special_characters(self): """测试含特殊字符文本的混合相似度计算""" orig = "【论文标题】:Python在数据分析中的应用\n关键词:Python、数据分析、 Pandas" copy = "【论文标题】:Python在数据分析中的应用\n关键词:Python、数据分析、NumPy" self.assertTrue(0.8 < hybrid_similarity(orig, copy) < 1.0)
2. 边界场景验证
- 空文本测试:
思路:构造单空文本和双空文本测试 ,预期相似度均为 0.0,确保函数在无有效文本时不会报错,且返回合理结果。
def test_hybrid_empty_texts(self):
"""测试空文本的混合相似度计算"""
self.assertEqual(round(hybrid_similarity("", ""), 2), 0.00)
def test_hybrid_one_empty_text(self):
"""测试一个文本为空时的混合相似度计算"""
orig = "测试文本"
copy = ""
self.assertEqual(round(hybrid_similarity(orig, copy), 2), 0.00)
-
零向量测试:
思路:验证余弦相似度计算时,对 “零向量” 的特殊处理逻辑(避免除以零错误),预期返回 0.0。def test_cosine_similar_zero_vector(self): """测试余弦相似度(零向量场景)""" vec1 = np.array([0, 0, 0]) vec2 = np.array([1, 2, 3]) self.assertEqual(cosine_similar(vec1, vec2), 0.0) -
全停用词测试:
构造全部为停用词测试文本,预处理后词序列为空,预期返回 0.0,验证函数对 “无有效语义词” 场景的兼容。def test_hybrid_stopwords_only(self): """测试全停用词文本的混合相似度计算""" orig = "的是了,。和我要今天" copy = "的是了,。和我要今天" orig_pre = preprocess(orig, use_stopwords=True) copy_pre = preprocess(copy, use_stopwords=True) self.assertTrue(not orig_pre and not copy_pre) result = hybrid_similarity(orig, copy) self.assertEqual(round(result, 2), np.float64(0.00))
3. 流程完整性验证:模拟主函数执行
针对 main 函数(入口流程),通过 @patch 模拟命令行参数、read_file 文本返回值、hybrid_similarity 相似度结果,验证完整执行流程。
思路:模拟命令行参数 ['main.py', 'orig.txt', 'copy.txt', 'result.txt'],read_file 返回 “测试文本”,hybrid_similarity 返回 0.85,预期 main 函数执行后,result.txt 中写入 “0.85”,且执行完成后删除临时文件 —— 验证主函数的参数解析、依赖调用、结果写入全流程。
@patch('sys.argv', ['main.py', 'orig.txt', 'copy.txt', 'result.txt'])
@patch('main.read_file', return_value="测试文本")
@patch('main.hybrid_similarity', return_value=0.85)
def test_main_normal_execution(self, _, __):
"""测试主函数正常执行流程"""
main()
with open('result.txt', 'r', encoding='utf-8') as f:
self.assertEqual(f.read(), "0.85")
os.remove('result.txt')
五、模块部分异常处理说明
在read_file函数中,当指定的文件路径不存在时,会主动抛出FileNotFoundError异常并附带 “文件不存在,请检查路径” 的提示信息;当文件路径存在但读取过程中触发 IO 错误(如权限不足、文件损坏、文件被占用等)时,会先打印 “文件读取失败:[具体错误信息]” 的提示,再通过sys.exit(1)终止程序。这是为了避免程序在文件操作环节出现难以预测的错误:通过主动检测文件是否存在并明确抛出异常,让用户能快速定位 “路径拼写错误”“文件未在指定位置” 等问题;通过捕获 IO 错误并打印详情、终止程序,既清晰告知用户读取失败的原因(如权限不足),又避免错误扩散导致后续流程异常,确保文件操作环节的错误可感知、可定位。
def read_file(file_path):
"""读取文件内容"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件 {file_path} 不存在,请检查路径.")
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except IOError as e:
print(f"文件读取失败: {e}")
sys.exit(1)
1. 文件不存在异常
样例故意指定了一个不存在的文件路径(non_existent_file.txt),然后调用read_file函数。根据函数的设计,当检测到文件路径不存在时,通过 raise FileNotFoundError(...) 主动抛出文件不存在异常,明确告知错误类型;测试中验证该异常是否被正确抛出 —— 若异常成功捕获,说明函数能准确识别 “文件不存在” 的错误,符合预设的异常处理逻辑。
def test_read_file_not_exist(self):
"""测试文件不存在时的读取"""
with self.assertRaises(FileNotFoundError):
read_file("non_exist.txt")
2. IO异常
通过 @patch('builtins.open', side_effect=IOError("权限不足")) 模拟文件读取时的权限错误,预期函数打印错误信息并以 sys.exit(1) 终止 ,验证 IO 异常的捕获与处理流程。
@patch('os.path.exists', return_value=True)
@patch('builtins.open', side_effect=IOError("权限不足"))
def test_read_file_io_error(self, _, __):
"""测试文件读取IO错误场景"""
with self.assertRaises(SystemExit) as cm:
read_file("test.txt")
self.assertEqual(cm.exception.code, 1)

浙公网安备 33010602011771号