第一次个人编程作业
软件工程个人项目:论文查重(Python实现)
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 个人项目 |
| 这个作业的目标 | 完成一个论文查重程序,熟悉个人软件开发流程 |
我的 GitHub 仓库链接:https://github.com/Deanna8080/SoftwareEngineering
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | 300 |
| · Analysis | · 需求分析 (包括学习新技术) | 40 |
| · Design Spec | · 生成设计文档 | 20 |
| · Design Review | · 设计复审 | 20 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
| · Design | · 具体设计 | 30 |
| · Coding | · 具体编码 | 120 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 40 |
| Reporting | 报告 | 60 |
| · Test Repor | · 测试报告 | 20 |
| · Size Measurement | · 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 |
| 合计 | 390 |
二、计算模块接口的设计与实现过程
1. 命令行接口规范
本程序采用命令行参数的方式读取和输出文件,入口文件命名为 main.py。运行方式如下:
python main.py <原文文件路径> <抄袭版论文文件路径> <答案文件路径>
例如:
python main.py samples/orig.txt samples/orig_0.8_add.txt samples/ans_add.txt
2.基本架构
├── main.py
├── test_main.py
├── requirements.txt
├── .gitignore
├── orig.txt
├── orig_add.txt
└── samples/
├── orig.txt
├── orig_0.8_add.txt
├── orig_0.8_del.txt
├── orig_0.8_dis_1.txt
├── orig_0.8_dis_10.txt
└── orig_0.8_dis_15.txt
3. 代码组织
本项目采用 Python3 实现,代码在 main.py 中分为五个核心函数:
read_file(path):负责读取指定路径的文件内容,采用 UTF-8 编码。
normalize(text):文本预处理。通过正则表达式 re.sub 去掉所有标点、空格和换行,并将英文字母统一转为小写。
ngram_counter(text, n):将预处理后的文本切分为字符 n-gram,并使用 collections.Counter 统计词频。
cosine_similarity(text_a, text_b, n):核心计算模块。利用词频向量计算余弦相似度,返回 0.00 到 1.00 之间的浮点数。
main():负责解析命令行参数,调用上述函数,并将结果写入答案文件。
关键函数调用关系:main() → read_file() / cosine_similarity() → normalize() / ngram_counter()。结构清晰,便于单元测试。
4.关键函数流程图

流程图展示了程序从启动、参数校验、读取文件、文本清洗、n-gram 统计到余弦相似度计算、写入答案的完整过程。
5. 算法关键
本算法采用的是字符 2-gram + 余弦相似度。
(1)为什么用字符 n-gram 而不分词?
因为中文分词需要引入第三方库,会增加依赖。使用字符 2-gram 对中文支持非常好,即使有增删改,只要保留部分连续字,就能计算较高的相似度。
(2)余弦相似度
将文本转化为向量的点积除以模长的乘积,完美解决了文本长度不一导致的相似度偏差问题。
6. 独到之处
预处理阶段不仅去除了标点,还去掉了空格和换行,并统一了大小写。这保证了“今天,天气 晴!”与“今天天气晴”被判定为完全相同,排除了格式对查重结果的干扰。此外,对空文本、极短文本、非法 n 值等边界情况均做了妥善处理。
三、计算模块接口部分的性能改进
1. 性能分析图

说明:本项目采用 Python3,使用内置 cProfile 配合 SnakeViz 进行性能分析.
2. 改进思路
通过 SnakeViz 对长文本样例进行分析,冰柱图显示:
1)程序处理大文本的总耗时约为 0.0119 秒,远远低于评测要求的 5 秒限制。
2)消耗最大的函数是 ngram_counter() 中的字典统计操作,耗时约 0.00516 秒,占用了接近一半的 CPU 时间。
改进思路:
由于文本较长,切分 2-gram 会产生海量的小字符串,Python 内置字典的哈希计算和扩容成为了性能瓶颈。目前的性能满足作业要求,但如果未来需要处理几十 MB 的超大论文,可以考虑以下改进:
1)将 collections.Counter 替换为手动维护的 array 或预分配容量的字典,减少哈希冲突和扩容开销。
2)使用 numpy 对字符数组进行向量化统计,利用底层 C 语言优化性能。
四、计算模块部分单元测试展示
1. 单元测试代码
我设计了 12 个测试用例,涵盖单元测试和集成测试两个层次:
单元测试(11个):构造短文本数据,验证 normalize、ngram_counter、cosine_similarity 等核心函数的边界与逻辑。
集成测试(1个):使用老师下发的长文本样例,验证程序在真实大规模文本下的正确性。
测试用例覆盖了:完全相同、略有改动、完全不同、空文本、纯标点、大小写、极短文本、非法参数 n、文件读写、文件不存在、输出格式化、长文本等场景。
def test_identical_text(self):
# 测试1:完全相同文本,相似度应为 1.0
score = cosine_similarity("今天是星期天", "今天是星期天")
self.assertAlmostEqual(score, 1.0)
def test_empty_original(self):
# 测试3:原文为空,相似度应为 0.0
self.assertEqual(cosine_similarity("", "任意文本"), 0.0)
def test_invalid_n(self):
# 测试10:非法 n 值应抛出 ValueError
with self.assertRaises(ValueError):
ngram_counter("abc", 0)
def test_long_text_sample(self):
# 测试12:长文本集成测试,相似度应在 0.7~0.99 之间
original_text = read_file("samples/orig.txt")
copied_text = read_file("samples/orig_0.8_add.txt")
score = cosine_similarity(original_text, copied_text, n=2)
self.assertGreater(score, 0.7)
self.assertLess(score, 0.99)
2. 测试覆盖率截图

说明:总计覆盖率达到 81%,其中 test_main.py 覆盖率为 97%。main.py 未覆盖的部分主要是 if name == "main": 里的命令行入口解析以及异常捕获打印逻辑,属于正常现象,符合预期。
3. 长文本样例集成测试结果
| 样例文件 | 相似度 |
|---|---|
| orig_0.8_add.txt | 0.90 |
| orig_0.8_del.txt | 0.90 |
| orig_0.8_dis_1.txt | 0.97 |
| orig_0.8_dis_10.txt | 0.91 |
| orig_0.8_dis_15.txt | 0.75 |
结果说明:
由于本程序采用字符 2-gram + 余弦相似度,对“打乱字序”类抄袭(orig_0.8_dis_*.txt)敏感度较低,因此相似度偏高(0.91~0.97)。这与在线工具(基于词序或编辑距离)的结果存在算法差异,属于正常现象。程序在“完全相同”(1.00)、“完全不同”(0.00)、“空文件”(0.00)等边界条件下均表现正确,证明算法逻辑无误。
五、计算模块部分异常处理说明
| 异常场景 | 设计目标 | 对应单元测试 |
|---|---|---|
| 文件不存在 | 文件路径输入错误时,抛出 FileNotFoundException,防止程序崩溃,提示用户。 |
test_read_file(修改路径触发) |
| 空文本输入 | 当原文或抄袭版为空时,直接返回相似度 0.0,防止除以零错误(ZeroDivisionError)。 | test_empty_original、test_empty_copy |
| 非法参数 n | 当 n <= 0 时,抛出 ValueError 异常,防止切片陷入死循环或逻辑错误。 |
test_invalid_n |
对应的异常测试代码示例:
def test_empty_copy(self):
self.assertEqual(cosine_similarity("任意文本", ""), 0.0)
def test_invalid_n(self):
with self.assertRaises(ValueError):
ngram_counter("abc", 0)
六、PSP 表格(实际时间)
在项目完成之后,我记录了各个模块实际花费的时间,如下表所示:
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | 380 |
| · Analysis | · 需求分析 (包括学习新技术) | 60 |
| · Design Spec | · 生成设计文档 | 30 |
| · Design Review | · 设计复审 | 20 |
| · Coding Standard | · 代码规范 | 15 |
| · Design | · 具体设计 | 45 |
| · Coding | · 具体编码 | 120 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 60 |
| Reporting | 报告 | 80 |
| · Test Repor | · 测试报告 | 50 |
| · Size Measurement | · 计算工作量 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 |
| 合计 | 490 |
七、代码质量分析
本项目从以下几方面保证代码质量:
1.函数职责单一:文件读取、文本清洗、n-gram 统计、余弦计算、命令行入口相互独立。
2.变量与函数命名规范:均采用小写加下划线风格(PEP 8),见名知意。
3.异常处理完善:对文件不存在、空文本、非法参数、命令行参数数量错误等情况均做了处理。
4.不联网、不读写无关文件:程序只读取命令行指定的两个输入文件,只写入一个答案文件。
5.使用 unittest 自动化测试:12 个测试用例全部通过。
基础检查命令:
python -m unittest test_main.py
结果:Ran 12 tests in 0.064s OK
八、总结与反思
通过本次个人项目,我完整地实践了从需求分析、代码设计、Git 版本控制到单元测试、性能分析的软件开发流程。
1.技术收获:掌握了字符 n-gram + 余弦相似度的查重算法,学会了用 SnakeViz 分析性能瓶颈、用 Coverage 查看测试覆盖率。
2.工程能力:学会了用 Git 管理代码版本,合理使用 .gitignore 排除临时文件,保持仓库整洁。
3.问题解决:在配置 Git 时遇到了 SSL 证书报错,通过配置 Windows 证书存储和切换协议解决了问题,锻炼了排查环境问题的能力;处理HTML页面文本转换。
未来若继续改进,可以考虑加入更精细的 n-gram 权重混合(如 1-gram/2-gram/3-gram 加权),以及针对超大规模文本的分块处理,进一步提升算法的鲁棒性与性能。
浙公网安备 33010602011771号