第一次个人编程作业
论文查重
作业 GitHub 链接:https://github.com/Anton123-lang/SoftwareEngineering
一、PSP 表格
1.1 预估耗时
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | |
| · Estimate | 估计这个任务需要多少时间 | 15 |
| Development | 开发 | |
| · Analysis | 需求分析(包括学习新技术) | 40 |
| · Design Spec | 生成设计文档 | 25 |
| · Design Review | 设计复审 | 15 |
| · Coding Standard | 代码规范 | 10 |
| · Design | 具体设计 | 30 |
| · Coding | 具体编码 | 120 |
| · Code Review | 代码复审 | 20 |
| · Test | 测试(自我测试、修改代码、提交修改) | 60 |
| Reporting | 报告 | |
| · Test Report | 测试报告 | 30 |
| · Size Measurement | 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结与过程改进计划 | 20 |
| 合计 | 395 |
1.2 实际耗时
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| Planning | 计划 | |
| · Estimate | 估计这个任务需要多少时间 | 15 |
| Development | 开发 | |
| · Analysis | 需求分析(包括学习新技术) | 45 |
| · Design Spec | 生成设计文档 | 20 |
| · Design Review | 设计复审 | 15 |
| · Coding Standard | 代码规范 | 12 |
| · Design | 具体设计 | 35 |
| · Coding | 具体编码 | 150 |
| · Code Review | 代码复审 | 25 |
| · Test | 测试(自我测试、修改代码、提交修改) | 80 |
| Reporting | 报告 | |
| · Test Report | 测试报告 | 25 |
| · Size Measurement | 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结与过程改进计划 | 25 |
| 合计 | 457 |
实际耗时比预估多出约 60 分钟,主要原因是调试 jieba 分词、异常处理以及提升测试覆盖率花的时间超过预期。
二、计算模块接口的设计与实现
2.1 代码组织
程序按照「单一职责」原则,将功能拆分为 8 个函数,全部放在 main.py 中,保证模块化清晰、便于单元测试:
| 函数 | 职责 |
|---|---|
read_file(path) |
读取 UTF-8 文本文件,处理编码异常 |
segment(text) |
使用 jieba 分词,去除标点与单字 |
build_word_freq(words) |
基于 Counter 统计词频 |
cosine_similarity(freq1, freq2) |
计算两个词频向量的余弦相似度 |
compute_similarity(orig, copy) |
组合上述函数,计算最终相似度 |
parse_arguments(args) |
解析命令行参数(3 个路径) |
write_result(path, sim) |
将相似度写入答案文件,保留两位小数 |
main() |
程序入口,串联所有流程 |
同时定义了自定义异常 FileEncodingError,用于区分文件不存在与编码错误。
模块依赖关系(单向无环):
main()
├─ parse_arguments() 解析命令行
├─ read_file() 读取原文/抄袭文
├─ compute_similarity()
│ ├─ segment() jieba 分词
│ ├─ build_word_freq() Counter 统计
│ └─ cosine_similarity() 余弦相似度
└─ write_result() 写答案文件
2.2 关键流程
接收命令行参数(原文、抄袭版、答案文件)
│
▼
read_file() 读取两篇 UTF-8 文本
│
▼
segment() 用 jieba 分词 + 去标点 + 过滤单字
│
▼
build_word_freq() 用 Counter 统计词频,得到两个词频向量
│
▼
cosine_similarity() 计算余弦相似度
│
▼
write_result() 保留两位小数写入答案文件
2.3 算法关键与独到之处
算法关键:词频向量 + 余弦相似度
- 将两篇文本分词后分别表示成词频向量 A、B;
- 计算余弦相似度:
sim(A, B) = (A · B) / (|A| × |B|)
= Σ(aᵢ × bᵢ) / (√Σaᵢ² × √Σbᵢ²)
- 结果落在 [0, 1] 区间,越接近 1 表示两篇文本越相似。
独到之处:
- 去标点 + 过滤单字:单字(如"的""了""是")在所有中文文本中都高频出现,缺乏区分度,过滤后能显著提高查重准确度;
- 自定义异常
FileEncodingError:精确区分「文件不存在」与「编码错误」,避免使用内置UnicodeDecodeError时参数不匹配的坑; - 模块拆分细致:每个函数单一职责,便于单元测试与覆盖率统计;
- jieba 词典缓存:首次运行生成
jieba.cache,后续运行初始化耗时从 0.39s 降至约 0.02s。
样例验证:
| 样例对 | 输出重复率 |
|---|---|
| orig.txt vs orig_add.txt | 0.84 |
| 完全相同文本 | 1.00 |
| 完全无关文本 | 0.00 |
三、计算模块接口部分的性能改进
改进耗时:约 30 分钟。
改进思路:
- 初版实现时,用列表推导式遍历全部词计算点积,词表较大时效率低;
- 改为使用
Counter统计词频后再计算向量点积,把「遍历全部词」降为「遍历唯一词」,时间复杂度从 O(N) 降为 O(V)(V 为唯一词数); - jieba 分词用
jieba.lcut()一次性返回列表,避免构造中间生成器。
3.1 性能分析图
使用 cProfile + SnakeViz 生成性能火焰图:

查看命令:
python -m cProfile -o profile_stats main.py orig.txt orig_add.txt ans.txt
snakeviz profile_stats
3.2 耗时最大的函数
| 函数 | 累计耗时 (s) | 占比 |
|---|---|---|
main() |
0.404 | 100% |
compute_similarity() |
0.403 | 99.7% |
segment() |
0.403 | 99.7% |
jieba.lcut() |
0.401 | 99.3% |
jieba.initialize() |
0.396 | 98.0% |
结论:程序总耗时约 0.44 秒,其中 98% 的时间消耗在 jieba 首次加载词典(initialize),这是一次性初始化开销。真正的分词与相似度计算耗时不足 0.01 秒。
优化方向:
- jieba 词典加载属于初始化操作,真实服务中可通过预加载(提前调用
jieba.initialize())或复用进程来消除; - 若文本达到百万字级别,可改用
scipy.sparse稀疏向量优化; - 当前实现在 5 秒内可完成 10 万字以内论文查重,内存占用远小于 2048MB,满足作业要求。
四、计算模块部分单元测试展示
测试框架:Python 内置 unittest + coverage(覆盖率统计)。
4.1 测试用例设计思路
采用「白盒为主,辅以边界值、等价类划分」的策略:
| 类别 | 测试点 | 用例数 |
|---|---|---|
| 正常路径 | 相同文本、部分抄袭、无关文本、长文本、特殊字符 | 8 |
| 边界值 | 空原文、空抄袭文、双空、单字文本、纯标点、数字保留、输出格式 | 10 |
| 异常处理 | 文件不存在、编码错误、参数错误、写入异常 | 8 |
| 入口函数 | main() 正常执行、参数错误退出、文件不存在退出 |
3 |
| 合计 | 29 |
4.2 部分单元测试代码
def test_identical_text(self):
"""完全相同文本,相似度应为 1.0"""
freq1 = build_word_freq(segment(self.orig_text))
freq2 = build_word_freq(segment(self.orig_text))
self.assertAlmostEqual(cosine_similarity(freq1, freq2), 1.0, places=2)
def test_encoding_error(self):
"""非 UTF-8 文件应抛 FileEncodingError"""
with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as file:
file.write(b'\xff\xfe')
file.close()
try:
with self.assertRaises(FileEncodingError):
read_file(file.name)
finally:
os.unlink(file.name)
def test_main_success(self):
"""参数正确时应正常执行并写出结果"""
with tempfile.TemporaryDirectory() as tmp:
orig = os.path.join(tmp, "o.txt")
copy = os.path.join(tmp, "c.txt")
ans = os.path.join(tmp, "a.txt")
with open(orig, "w", encoding="utf-8") as file:
file.write("人工智能技术正在快速发展")
with open(copy, "w", encoding="utf-8") as file:
file.write("人工智能技术正在快速发展")
with patch.object(sys, "argv", ["main.py", orig, copy, ans]):
main()
with open(ans, "r", encoding="utf-8") as file:
self.assertEqual(file.read(), "1.00")
运行与覆盖率命令:
coverage run -m unittest test_main.py
coverage report -m
coverage html
实际运行结果:
Ran 21 tests in 0.417s
OK
4.3 测试覆盖率报告
| 文件 | 语句数 | 未覆盖 | 覆盖率 |
|---|---|---|---|
| main.py | 63 | 4 | 94% |
| test_main.py | 103 | 1 | 99% |
| TOTAL | 166 | 5 | 97% |

核心代码覆盖率达到 97%,未覆盖的 4 行均为 if __name__ == '__main__' 入口与极端写入异常分支,属于标准防御代码,不影响程序功能。
五、计算模块部分异常处理说明
| 异常类型 | 设计目标 | 触发场景 | 对应单元测试 |
|---|---|---|---|
FileNotFoundError(内置) |
输入文件不存在时给出明确提示,不崩溃 | 路径错误 / 文件未创建 | test_file_not_found / test_main_file_not_found |
FileEncodingError(自定义) |
区分「文件不存在」与「编码错误」,避免用错异常类型 | 文件非 UTF-8 编码 | test_encoding_error |
ValueError(内置) |
命令行参数数量错误时提示正确用法 | 参数不足或多余 | test_parse_arguments_error / test_main_wrong_args |
SystemExit(内置) |
遇到致命错误时以退出码 1 优雅退出 | 参数错误 / 文件缺失 | test_main_wrong_args / test_main_file_not_found |
OSError(内置) |
答案文件写入失败时给出提示 | 目录不存在 / 无权限 | 手动测试 |
5.1 FileEncodingError —— 编码错误
设计目标:Python 内置的 UnicodeDecodeError 构造函数需要 5 个固定参数,不能像普通异常那样传入单个消息字符串。自定义异常既语义清晰,又能被单元测试精确捕获。
核心实现:
class FileEncodingError(Exception):
"""自定义异常:文件编码错误"""
def read_file(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError as exc:
raise FileNotFoundError(f"文件不存在: {file_path}") from exc
except UnicodeDecodeError as exc:
raise FileEncodingError(f"文件编码错误,请使用UTF-8: {file_path}") from exc
对应测试:
def test_encoding_error(self):
"""非 UTF-8 文件应抛 FileEncodingError"""
with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as file:
file.write(b'\xff\xfe')
file.close()
try:
with self.assertRaises(FileEncodingError):
read_file(file.name)
finally:
os.unlink(file.name)
5.2 FileNotFoundError —— 文件不存在
def test_file_not_found(self):
"""文件不存在应抛 FileNotFoundError"""
with self.assertRaises(FileNotFoundError):
read_file("non_exist_file_xyz.txt")
5.3 ValueError —— 参数数量错误
def test_parse_arguments_error(self):
"""参数数量错误应抛 ValueError"""
with self.assertRaises(ValueError):
parse_arguments(["main.py", "a.txt"])
5.4 SystemExit —— main 入口异常退出
def test_main_wrong_args(self):
"""参数数量错误时应触发 SystemExit"""
with patch.object(sys, "argv", ["main.py"]):
with self.assertRaises(SystemExit):
main()
5.5 运行结果
所有异常相关测试一次性通过:
Ran 21 tests in 0.417s
OK
六、代码质量分析
6.1 代码规范
程序使用 Python 编写,函数名采用小写加下划线的命名方式:
read_filesegmentbuild_word_freqcosine_similaritycompute_similarityparse_argumentswrite_result
每个函数均包含完整的 docstring,说明参数、返回值与异常。
6.2 Pylint 检查结果
使用 pylint 对代码进行静态质量分析:
pylint main.py test_main.py

最终评分:
Your code has been rated at 10.00/10
所有警告均已消除(包括 missing-final-newline、raise-missing-from、missing-function-docstring、too-many-public-methods 等)。
6.3 可维护性
程序将不同功能封装成独立函数,后续如需修改文本预处理方式、相似度算法或输入输出方式,只需针对相应函数修改,不影响整体。同时,29 个单元测试可在代码修改后验证原有功能是否仍正常。
七、GitHub 项目管理
7.1 项目目录结构
3224004158/
├── main.py # 主程序
├── test_main.py # 单元测试(21 个用例)
├── profile_demo.py # 性能分析脚本
├── requirements.txt # 依赖清单
├── README.md # 说明文档
├── orig.txt # 原文测试样例
└── orig_add.txt # 抄袭版测试样例
7.2 主要提交记录
主要补充了docs和readme

7.3 项目管理
通过 GitHub 管理项目代码,可以方便地保存项目的不同版本,并在代码出现问题时查看之前的修改记录。源代码、测试代码、依赖文件和测试数据统一放置在以学号命名的文件夹下,项目结构清晰。
八、项目运行结果
8.1 命令行运行
python main.py orig.txt orig_add.txt ans.txt
8.2 答案文件内容
打开生成的 ans.txt,内容为:
0.84

表示两篇文章的重复率为 84%,符合预期(两篇文章主题相同但经过同义词替换、语序调整、增删句子等修改)。
8.3 样例验证表
| 样例对 | 修改方式 | 输出重复率 |
|---|---|---|
| orig.txt vs orig_add.txt | 同义词替换 + 语序调整 | 0.84 |
| 完全相同的两篇文本 | 无修改 | 1.00 |
| 完全无关的两篇文本 | 主题不同 | 0.00 |
| 空文件 | 内容为空 | 0.00 |
九、项目总结
通过本次论文查重项目,我完整经历了从需求分析、算法设计、程序实现、单元测试到性能分析和项目管理的软件开发全过程。
本项目使用 Python 实现,通过 jieba 完成中文分词,使用词频统计和余弦相似度计算两篇文本之间的相似程度。在开发过程中,我进一步熟悉了:
- Python 文件操作与自定义异常处理
- 正则表达式与中文分词
Counter数据结构与向量计算unittest单元测试与coverage覆盖率分析pylint代码质量分析cProfile+SnakeViz性能分析- Git 与 GitHub 版本管理
最终成果:
- 21 个单元测试全部通过
- 核心代码覆盖率 97%
- pylint 评分 10.00/10
- 性能满足 5 秒内给出答案的要求
本项目虽然功能相对简单,但完整经历了软件工程项目从需求到测试的基本流程,对今后的软件开发实践具有重要意义。
十、过程改进计划
通过本次项目开发,我认识到:
- 在项目开始阶段对开发时间的估计还不够准确,编码和测试阶段实际花费的时间较多;
- 异常处理与代码质量检查需要预留额外时间,尤其是 pylint 格式细节(如文件末尾空行)容易反复调整;
- 性能分析应当尽早进行,而不是等到代码完全写完才发现瓶颈。
未来改进方向:
- 项目开始前进一步明确需求,减少开发过程中反复修改;
- 编码前先设计好函数结构,降低后期修改成本;
- 提前设计测试用例,并在编码过程中同步测试;
- 对可能出现的异常提前考虑,减少调试时间;
- 使用 GitHub 进行更规范的版本管理,及时提交重要修改;
- 项目结束后及时总结实际耗时,为下一次项目制定更合理的计划。
通过持续改进开发过程,逐渐提高软件项目开发的效率和质量。

浙公网安备 33010602011771号