第一次个人编程作业

这个作业属于哪个课程 计科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.ndarrayvec2: 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_filehybrid_similarity,将结果写入指定文件。
  • 接口参数:无(依赖sys.argv获取命令行参数)
  • 参数校验:若命令行参数数量不为 4(原文路径、抄袭文本路径、结果路径),打印正确格式并退出。
  • 结果输出:将综合相似度保留 2 位小数,写入结果文件,同时在控制台打印结果路径与相似度。

流程图

flowchart TD A([输入:原始文本、抄袭文本]) --> B[预处理(含停用词)] B --> C[生成含停用词词序列] C --> D{词序列均为空?} D -->|是| E[返回 0.0(无有效文本)] D -->|否| F[转换为词集合] F --> G[计算Jaccard相似度] G --> H{Jaccard相似度 < 0.05?} H -->|是| I[返回 0.0(初筛低相似度)] H -->|否| J[预处理(无停用词)] J --> K[生成词频向量] K --> L[计算余弦相似度] K --> M[计算LCS相似度] L --> N[余弦相似度值] M --> O[LCS相似度值] N & O --> P[加权计算:余弦*0.6 + LCS*0.4] P --> Q[返回综合相似度] E & I & Q --> R([流程结束])

算法关键与独到之处

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,处理 “两个文本均无有效词” 的极端场景。

三、模块接口部分的性能改进

代码质量分析

Code Quality Analysis

​ 使用了三种常用的 Python 代码质量分析工具对 main.pytest.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仍为消耗最大的函数。但相较于改进前,其耗时占比有所降低。这是因为分词操作本身的算法复杂度决定了其必然是计算热点,不过预处理、向量计算等环节的优化,使得整体流程中其他部分耗时减少,从而让分词的耗时占比相对突出,但绝对耗时已得到一定控制。

四、模块部分单元测试

  • 单元测试覆盖率:

coverage

1. 基础功能验证

  • 测试preprocess 函数

    • 思路:通过test_preprocess_with_stopwordstest_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)
posted @ 2025-09-23 16:42  zhiyuxinxuan  阅读(38)  评论(0)    收藏  举报