第一次个人编程作业

这个作业属于哪个课程 我所在的班级
这个作业要求在哪里 作业要求
这个作业的目标 尝试完成一份“软件工程作业”,包含测试等完成流程

一、准备

1.我的 GitHub 项目链接:

https://github.com/ChloeZhongN/PaperCheck

2. PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning(计划) / /
· Estimate(估计这个任务需要多少时间) 225 208
Development(开发) / /
· Analysis(需求分析,包括学习新技术) 10 15
· Design Spec(生成设计文档) 15 10
· Design Review(设计复审) 5 5
· Coding Standard(代码规范,为目前的开发制定合适的规范) 5 10
· Design(具体设计) 30 28
· Coding(具体编码) 60 65
· Code Review(代码复审) 20 10
· Test(测试:自我测试,修改代码,提交修改) 30 25
Reporting(报告) / /
· Test Report(测试报告) 20 30
· Size Measurement(计算工作量) 10 5
· Postmortem & Process Improvement Plan(事后总结,并提出过程改进计划) 20 5
合计 225 208

3.需求分析

  • 本系统旨在实现一个论文查重工具,用于评估两篇文本的相似程度。程序通过命令行接收原文文件路径、抄袭版论文文件路径以及答案输出文件路径三个参数。

  • 功能上,系统需自动读取并清洗文本中的无效标点与空白字符,基于中文分词提取词频特征,计算两篇文本的相似比例,最终将结果以保留两位小数的浮点数格式写入答案文件中。

  • 非功能层面,程序运行时间需严格控制在 5 秒以内,内存占用不得超过 2048MB,并在遇到文件不存在或参数缺失时进行异常拦截与安全退出。

4.代码设计

  • 系统整体采用面向对象结构设计,由两个核心类和一个主入口函数构成。

  • FileHandler 类负责文件磁盘交互;

  • SimHashCheck 类负责核心算法实现,包含基于正则表达式的文本清洗、Jieba 中文分词、MD5 词哈希转换、64 维特征向量加权降维,以及基于海明距离的相似度映射计算;

  • 主函数 main 负责校验命令行入参并调度上述两类完成全流程。

  • 算法选用 SimHash 局部敏感哈希,具备 O(N) 的低时间复杂度与抗局部增删、抗语序置换的特性。

  • 此外,系统针对文件缺失设计了 FileNotFoundError 捕获机制,并配备了 10 组覆盖常规与边界的单元测试用例。

二、代码分析

1.计算模块接口的设计与实现过程

(1)模块组织与类结构设计

程序由两个类和一个主函数构成:

  • FileHandler 类(文件操作类):

    • read_file(file_path):接收文件路径参数,检查文件是否存在,使用 UTF-8 编码读取并返回全部文本内容。

    • write_file(file_path, content):接收输出路径和字符串内容,若目标目录不存在则创建该目录,将字符串写入文件。

  • SimHashCheck 类(查重计算类):

    • filter_text(text):使用正则表达式删除汉字、字母和数字以外的所有字符。

    • get_token_hash(token):对传入的词语计算 MD5 散列值,截取前 16 位十六进制字符串并转换为 64 位整数。

    • calculate_simhash(text):清洗文本,调用 jieba.lcut 进行中文分词,统计词频并对 64 维向量进行加权累加,最终通过二值化生成 64 位整型指纹。

    • get_hamming_distance(hash1, hash2):对两个 64 位整型进行异或运算,统计二进制数值中为 1 的位数总量。

    • compute_similarity(text1, text2):提供外部调用接口,处理空文本边界,计算两个文本的指纹与海明距离,计算并返回 0.00 到 1.00 之间的相似度数值。

类与函数之间的调用关系:

  • 主函数 main() 从命令行获取 3 个路径参数,调用 FileHandler.read_file() 读取两个文件的文本内容。随后实例化 SimHashCheck 类,调用 compute_similarity() 函数计算相似度。最后由 FileHandler.write_file() 将结果字符串写入答案文件。

(2)关键函数流程图(compute_similarity)

核心函数 compute_similarity 的执行流程如下:

compute_similarity.drawio

(3)算法的关键要素词频统计:

  • 通过分词获取词语,记录每个词出现的具体次数。出现次数直接作为该词在 64 维特征向量计算中的权重数值。向量加权与降维:建立一个长度为 64 的整数列表。每个词对应一个 64 位二进制数。对应二进制位为 1 则加上词频,为 0 则减去词频。所有词计算完毕后,数值大于 0 的位置设为 1,小于等于 0 的位置设为 0,输出一个 64 位整数。

  • 海明距离度量:使用位异或操作对比两个 64 位整数,不同位的结果为 1。统计 1 的总数量,即为海明距离。

(4)算法的技术特征

  • 输入微调对应指纹小幅度变化:MD5 算法在输入改变一个字符时输出数值完全改变。SimHash 算法基于词频累加与降维,文本局部修改或增删词语时,64 位二进制中改变的位数有限,海明距离较小。

  • 语序无关性:算法仅根据分词结果和词频计算权重,不依赖词语的排列先后顺序。调换句子语序不改变分词统计结果,生成的指纹数值保持不变。

  • 计算复杂度:文本特征提取的时间复杂度为 O(N),其中 N 为文本长度。两篇文本特征比对的时间复杂度为 O(1),仅需进行一次整型异或运算与位数统计。内存占用为常数级别。

2. 计算模块接口部分的性能改进

(1)性能改进所花费的时间:45 分钟。

(2)性能分析图展示:

  • 初版(优化前)性能分析图:
    image

  • 改进版(优化后)性能分析图:
    image

(3)程序中消耗最大的函数:

根据性能分析工具 cProfile 的统计输出:

  • 在外部依赖层面,自身耗时(tottime)最大的是系统底层词典反序列化函数 {built-in method marshal.load}(初版 0.499 秒,改进版 0.532 秒),这是第三方中文分词库 jieba 加载默认词典缓存产生的环境固定开销。

  • 在自研业务逻辑层面,初版程序中自身耗时最大的核心函数为 main.pycalculate_simhash(自身耗时 0.097 秒,累计耗时 0.773 秒)。初版中用于提取词哈希的函数 get_token_hash 被频繁调用了 12,123 次,对两篇文章中切分词(包含高频重复出现的虚词与名词)均调用 MD5 与进制转换,构成了最主要的计算性能瓶颈。

(4)改进思路与实现:

  • 哈希记忆化缓存(Memoization):
    针对长文本中高频词反复出现的特性,在 SimHashCheck 类中维护一个字典 hash_cache。当词语被切分出来后,优先在字典中检索;仅对首次遇到的新词调用 hashlib.md5() 并截取 64 位哈希值存入缓存,后续遇到相同词直接读取整型哈希,降低了散列计算与字符串编码的 CPU 开销。

  • 位掩码预生成(Bit-mask Pre-computation):
    初版在内层 64 维特征向量的遍历中,对每个词的每个比特位均动态执行 1 << i 左移运算。改进版在类初始化 init 中直接预生成 64 维掩码列表 self.masks = [1 << i for i in range(bit_num)],在加权与降维循环中按索引取值,减少了多层循环内部重复的指令级位移运算。

  • 词频聚合与层级缩减:
    对切词结果先进行词频聚合统计(freq_dict),随后仅遍历去重后的词语集合(freq_dict.items())。权重直接由词频数值替代多次累加,避免了重复词多次触发 64 维向量的遍历。

(5)优化效果对比分析:

  • 函数调用次数缩减:程序总函数调用次数由初版的 743,895 次 降低至改进版的 723,202 次,消除了 20,693 次冗余的方法调用。

  • 核心业务耗时下降:核心算法函数 calculate_simhash 的自身运行时间从初版的 0.097 秒 骤降至改进版的 0.037 秒,耗时直接缩减了 61.8%,算法自身的纯计算性能获得了显著提升。

  • 结果精确度保持一致:两版算法对官方样本的比对结果均精确稳定在 0.98,未因性能优化损失计算精度。

3.计算模块部分单元测试展示

(1)单元测试代码片段展示:

Python
import unittest
from main import SimHashCheck, FileHandler


class TestPaperCheck(unittest.TestCase):

    def setUp(self):
        self.checker = SimHashCheck(bit_num=64)

    def test_exact_same(self):
        # 测试完全相同的文本
        t = "人工智能是计算机科学的一个分支。"
        sim = self.checker.compute_similarity(t, t)
        self.assertAlmostEqual(sim, 1.00, places=2)

    def test_special_characters_noise(self):
        # 测试含大量标点与特殊符号的抗噪能力
        t1 = "数据结构与算法设计。"
        t2 = "【数 据 结 构】与……%&*(算法设计)!"
        sim = self.checker.compute_similarity(t1, t2)
        self.assertAlmostEqual(sim, 1.00, places=2)

    def test_exception_file_not_found(self):
        # 测试读取不存在文件时的异常捕获
        with self.assertRaises(FileNotFoundError):
            FileHandler.read_file("non_existent_path_12345.txt")

(2)测试的函数与接口:

测试覆盖了 SimHashCheck 类中的 compute_similaritycalculate_simhashget_hamming_distancefilter_text,以及 FileHandler 类中的 read_filewrite_file

(3)测试数据的构造思路:

测试数据采用白盒测试与等价类划分法构造,共包含 10 个测试用例:

  • 等价类与边界值用例:构造“两段完全一致的文本”测试极限上限(1.00);构造“两段互不相关的领域文本”测试低相似度判定;构造“双文本均为空”验证极端边界(1.00);构造“单侧为空文本”验证下限分支(0.00)。
  • 文本抗噪与鲁棒性用例:在文本中插入大量特殊字符、多重标点与无规律空格,验证正则预处理模块的过滤完整性。
  • 特征稳定性用例:调换句中主谓宾语序,测试基于词频加权的 SimHash 算法对非线性语序变换的检测有效性;模拟局部增删替换句子,验证相似度处于合理评估区间。
  • 异常与文件接口用例:传入不存在的文件路径,验证 FileNotFoundError 的正确抛出;执行写入与读取闭环,验证磁盘 I/O 模块的稳定性。

(4)单元测试覆盖率展示:

运行覆盖率工具对 10 个测试用例执行自动化检测,覆盖率报告如下所示:

image

4.计算模块部分异常处理说明

(1)文件不存在异常(FileNotFoundError

  • 设计目标:
    防止由于用户在命令行输入了错误的原文或抄袭文文件路径、路径拼写错误或目标文件被删除时,程序直接抛出未捕获的系统级崩溃异常。通过在数据读取层进行文件存在性显式判定并抛出标准异常,使控制层能够捕获该错误并向用户输出明确的排查提示,最终安全退出。

  • 单元测试样例:

Python
def test_exception_file_not_found(self):
    # 测试读取不存在文件时的异常处理
    with self.assertRaises(FileNotFoundError):
        FileHandler.read_file("non_existent_file_path_12345.txt")
  • 对应场景:
    用户在命令行运行程序时,输入的原文路径或抄袭版论文路径在磁盘中不存在(例如文件已被重命名、移动或路径拼写错误)。

(2)命令行参数缺失或异常(SystemExit

  • 设计目标:
    防止在调用脚本时由于传入的参数数量不符合规范(缺少原文路径、缺少抄袭文路径或缺少输出路径),导致程序在访问 sys.argv 列表索引时触发系统级数组越界错误(IndexError)。程序需校验入参数量,当数量不等于 4 时,主动输出标准格式说明并调用 sys.exit(1) 退出。

  • 单元测试样例:

python
  import sys
  from unittest.mock import patch
  from main import main


  def test_exception_invalid_arguments(self):
      # 模拟命令行仅传入 2 个参数的错误场景
      test_args = ["main.py", "orig.txt"]
      with patch.object(sys, "argv", test_args):
          with self.assertRaises(SystemExit) as cm:
              main()
          # 验证程序退出状态码为 1
          self.assertEqual(cm.exception.code, 1)
  • 对应场景:
    用户在终端输入执行命令时参数遗漏,例如仅输入了 python main.py orig.txt 便按下回车,缺少了抄袭论文路径与答案输出路径。

(3)字符编码异常(UnicodeDecodeError 容错处理)

  • 设计目标:
    防止输入的文本文件中混杂有非 UTF-8 编码的特殊二进制字节或异常字符时,导致内置 open() 函数抛出 UnicodeDecodeError 中断程序运行。通过设置 errors="ignore" 模式,自动跳过无法解析的异常字节,保证主要文本内容的正常读取与后续算法流程的继续执行。

  • 单元测试样例:

Python
import os
from main import FileHandler


def test_exception_invalid_encoding(self):
    # 构造含有无法被标准 UTF-8 解析的异常字节文件
    corrupt_file = "corrupt_test.txt"
    with open(corrupt_file, "wb") as f:
        f.write(b"\xff\xfe\x00\x12\xab")  # 写入非 UTF-8 字节
    try:
        # 验证函数能正常读取而不抛出 UnicodeDecodeError 崩溃
        content = FileHandler.read_file(corrupt_file)
        self.assertIsInstance(content, str)
    finally:
        if os.path.exists(corrupt_file):
            os.remove(corrupt_file)
  • 对应场景:
    待检测的论文文件从不同操作系统之间拷贝,或者混入了特殊格式的非文本字符,导致文本内容中存在无法被 UTF-8 标准解码器识

三、代码结果

运行测试文本结果如下

image

posted @ 2026-09-13 15:40  zcnnn  阅读(27)  评论(0)    收藏  举报