第二次软件工程作业
软件工程个人项目作业报告:论文查重算法的设计与实现
项目链接:https://github.com/HXY-cell/3124004467-HXY
一、PSP 2.1 表格记录
在项目初期与交付后,各阶段的耗时评估与实际支出记录如下表所示。初期低估了 Git 环境调试以及边界分支覆盖率的耗时,实际开发在测试用例覆盖与异常处理上投入了较多精力。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| Estimate | 估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 470 | 630 |
| Analysis | 需求分析(包括学习新技术、环境配置) | 60 | 90 |
| Design Spec | 生成设计文档 | 40 | 35 |
| Design Review | 设计复审 | 20 | 20 |
| Coding Standard | 代码规范(制定 PEP8 及工程结构规范) | 20 | 15 |
| Design | 具体设计(算法选型、模块接口设计) | 60 | 50 |
| Coding | 具体编码(余弦算法、分词清洗、文件 I/O) | 120 | 150 |
| Code Review | 代码复审 | 30 | 40 |
| Test | 测试(单元测试编写、分支覆盖率达到 100%、Bug 修复) | 120 | 230 |
| Reporting | 报告 | 100 | 110 |
| Test Report | 测试报告(覆盖率报告导出与整理) | 30 | 35 |
| Size Measurement | 计算工作量(代码行数统计、文件归档) | 20 | 15 |
| Postmortem & Improvement | 事后总结,并提出过程改进计划 | 50 | 60 |
| Total | 合计 | 600 | 765 |
二、计算模块接口的设计与实现过程
- 代码组织与模块关系
工程划分为三个职责明确的层级,实现松耦合与高可测性:
utils.py(I/O 与校验工具层):封装安全的文件读取、写入与命令行参数校验逻辑。负责处理空文件、不存在文件以及写权限等异常。
sim_calculator.py(核心计算与文本处理层):包含 SimCalculator 核心类。实现分词过滤、词频统计向量化及余弦相似度计算。
main.py(系统交互入口层):负责接收 sys.argv 命令行输入,协调文件读取、相似度计算与最终结果写入。
[ main.py ] (CLI 调度入口)
/ \
v v
[ utils.py ] [ sim_calculator.py ]
(安全文件读写) (分词过滤 / 余弦相似度)
- 核心算法流程
算法核心由文本预处理、向量空间投影与余弦相似度计算三阶段组成:
第一,正则清洗与分词。输入原始文本后,首先通过正则表达式剔除中英文标点、换行符及无意义特殊符号,随后调用结巴分词(jieba.lcut)将连续文本离散化为词汇序列。
第二,构建词频向量空间。将两个文本分词后的词汇合并取并集作为特征维度字典 V。使用 collections.Counter 统计两篇文本在各特征维度上的词频,分别构成高维稀疏特征向量 A 与 B。
第三,余弦相似度计算:
Similarity = cos(theta) = (A · B) / (||A|| ||B||) = (sum(Ai * Bi)) / (sqrt(sum(Ai^2)) * sqrt(sum(Bi^2)))
计算完成后,通过截断函数将浮点数限定在 0.00 到 1.00 范围,并保留两位小数输出。
- 算法独到之处
精准边界拦截:在向量计算前预先判断空序列边界。若两篇文档清洗后均无有效词素,算法定义其相似度为 1.00;若仅有一方有效词素为空,则提前短路返回 0.00。
规避除零风险:由于前置拦截排除了空序列,后续进入特征向量平方和计算时,分母必满足 denominator 大于 0,从数学结构上消除了除零异常(ZeroDivisionError)隐患,兼顾了理论严谨性与运行安全性。
三、计算模块接口部分的性能改进
- 性能分析与改进耗时
在性能分析与代码优化阶段累计耗时 50 分钟。
- 优化思路与瓶颈定位
由于 Python 属于解释型脚本语言,在处理数万字长文本时,双层循环构建字典并集会导致 O(|V|) 的高频哈希检索。
改进思路:在计算向量点积 A · B 时,不再遍历全局词典并集,改为仅遍历长度较短的那个 Counter 字典的键。因为未出现在交集中的词汇,其乘积必为 0。这一优化将点积计算的时间复杂度从 O(|V|) 降至 O(min(|A|, |B|))。
分析工具应用:在 Windows 与 PyCharm 生产环境下,使用内置的 Profile 工具(底层基于 cProfile 与 pstats)生成调用树拓扑图与火焰图(Flame Graph)。对于 C++ 体系则对应使用 Visual Studio 2017 的诊断工具抓取 CPU 使用率采样。
- 消耗最大的函数分析
性能剖析图清晰显示,程序中总耗时最大(Cumulative Time)与单次调用耗时最高(Self Time)的函数均为 jieba.lcut 及其底层调用(如 jieba.viterbi 与 re.finditer)。中文分词涉及前缀词典加载、有向无环图(DAG)构建与动态规划求解,占据了全流程约 85% 的 CPU 时间。计算模块内部消耗最大的则是点积与模长计算中的高频乘加运算。
(在此处插入一张 PyCharm Profiler 或 VS 2017 生成的调用耗时表或调用图截图)
四、计算模块部分单元测试展示
- 单元测试设计思路
测试模块 test_main.py 采用 pytest 驱动,针对文本分词、向量点积、文件读写及命令行解析进行矩阵式用例覆盖。构造数据的思路涵盖:
常规基准测试:语义完全相同的长文本(预期 1.00)、完全不相交的文本(预期 0.00)、部分抄袭、乱序、替换近义词的现实用例。
极限边界测试:两篇全空文本、仅包含空格与标点符号的文本、超长重复单字文本。
- 部分核心单元测试代码展示
import pytest
from sim_calculator import SimCalculator
class TestSimCalculator:
def setup_method(self):
self.calc = SimCalculator()
def test_identical_texts(self):
"""测试完全相同文本的相似度"""
text = "软件工程是一门研究用工程化方法构建和维护有效的实用的软件的学科。"
assert self.calc.calc_similarity(text, text) == 1.00
def test_completely_different_texts(self):
"""测试完全无关文本的相似度"""
t1 = "苹果 香蕉 橘子 西瓜"
t2 = "量子力学 相对论 天体物理"
assert self.calc.calc_similarity(t1, t2) == 0.00
def test_punctuation_and_empty_edge(self):
"""测试全标点文本经清洗后退化为空文本的场景"""
assert self.calc.calc_similarity("!!!???", ",。;:") == 1.00
assert self.calc.calc_similarity("有效文本内容", "。。。") == 0.00
- 测试覆盖率说明
通过执行以下指令执行全量覆盖测试并输出统计数据:
pytest --cov=sim_calculator --cov=utils --cov-report=term-missing test_main.py
测试结果输出:
---------- coverage: platform win32, python 3.8.20-final-0 -----------
Name Stmts Miss Cover Missing
-------------------------------------------------
sim_calculator.py 34 0 100%
utils.py 24 0 100%
-------------------------------------------------
TOTAL 58 0 100%
核心算法与工具类各语句均被完备执行,语句覆盖率(Statement Coverage)达到 100%。
(在此处插入终端 100% 覆盖率输出的截图)
五、计算模块部分异常处理说明
针对文件系统交互与外部参数输入的不可控性,系统设计了四类防御性异常处理机制:
- 异常场景与设计目标
输入文件不存在(FileNotFoundError):
设计目标:防止程序抛出底层未捕获的系统栈错误而崩溃,捕获后向终端输出明确的文件缺失路径提示,退出码为非 0。
文件路径为目录而非实体文件(IsADirectoryError):
设计目标:拦截用户参数误将文件夹当成文本文件传入的非法操作。
文件编码解析异常(UnicodeDecodeError):
设计目标:默认采用 UTF-8 编码解析;若文件包含损坏字节或不兼容编码格式,拦截并给出编码规范指引。
命令行参数数量不足(SystemExit 或参数校验错误):
设计目标:用户未提供足额的三个路径参数时,输出规范的 Usage 帮助文本,拒绝进入计算链路。
- 异常测试用例展示
import pytest
from utils import read_file, validate_args
def test_read_file_not_found(tmp_path):
"""测试读取不存在路径时的异常抛出与处理"""
non_existent = str(tmp_path / "missing_file.txt")
with pytest.raises(FileNotFoundError):
read_file(non_existent)
def test_invalid_cli_arguments():
"""测试命令行入参不全时的参数校验异常"""
invalid_argv = ["main.py", "only_one_path.txt"]
with pytest.raises(ValueError) as excinfo:
validate_args(invalid_argv)
assert "参数数量错误" in str(excinfo.value)
六、事后总结与改进计划
在本次个人项目的全流程推进中,深刻体会到了软件工程中测试驱动开发(TDD)与规范化版本控制的价值。在早期开发中,由于过早编写了未被触发的死代码,导致初次覆盖率测试未达标;后续通过分析向量代数的分母非零特性重构逻辑,最终使测试语句覆盖率稳定达到 100%。
后续若对该查重引擎进行二次迭代,计划从以下两方面进行改进:
第一,算法精度优化。单纯的 TF(词频)余弦相似度无法捕捉高频虚词的干扰,未来可引入 TF-IDF 权重矩阵或 SimHash 局部敏感哈希算法,在降低大文本计算开销的同时抑制高频通用词对相似度计算的偏差。
第二,多线程与异步分词。长篇大文档场景下,可利用 Python 多进程(multiprocessing)或 Rust 绑定重构文本预处理模块,解除 GIL 锁限制,大幅提升文本分词阶段的吞吐率。

浙公网安备 33010602011771号