第一次个人编程作业
| 这个作业属于哪个课程 | 我所在的班级 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 尝试完成一份“软件工程作业”,包含测试等完成流程 |
一、准备
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 的执行流程如下:

(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.py的calculate_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_similarity、calculate_simhash、get_hamming_distance、filter_text,以及 FileHandler 类中的 read_file 与 write_file。
(3)测试数据的构造思路:
测试数据采用白盒测试与等价类划分法构造,共包含 10 个测试用例:
- 等价类与边界值用例:构造“两段完全一致的文本”测试极限上限(1.00);构造“两段互不相关的领域文本”测试低相似度判定;构造“双文本均为空”验证极端边界(1.00);构造“单侧为空文本”验证下限分支(0.00)。
- 文本抗噪与鲁棒性用例:在文本中插入大量特殊字符、多重标点与无规律空格,验证正则预处理模块的过滤完整性。
- 特征稳定性用例:调换句中主谓宾语序,测试基于词频加权的 SimHash 算法对非线性语序变换的检测有效性;模拟局部增删替换句子,验证相似度处于合理评估区间。
- 异常与文件接口用例:传入不存在的文件路径,验证
FileNotFoundError的正确抛出;执行写入与读取闭环,验证磁盘 I/O 模块的稳定性。
(4)单元测试覆盖率展示:
运行覆盖率工具对 10 个测试用例执行自动化检测,覆盖率报告如下所示:

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 标准解码器识
三、代码结果
运行测试文本结果如下




浙公网安备 33010602011771号