第一次个人编程作业
第一次个人软件工程作业
GitHub 项目链接
https://github.com/kecit33/3124004503
3124004503/
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 个人项目 |
| 这个作业的目标 | 独立完成论文查重算法 |
一、psp表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 15 |
| · Estimate | · 估计这个任务需要多少时间 | 180 | 260 |
| Development | 开发 | 130 | 150 |
| · Analysis | · 需求分析 (包括学习新技术) | 20 | 30 |
| · Design Spec | · 生成设计文档 | 10 | 20 |
| · Design Review | · 设计复审 | 15 | 20 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 25 | 40 |
| · Design | · 具体设计 | 20 | 30 |
| · Coding | · 具体编码 | 90 | 110 |
| · Code Review | · 代码复审 | 20 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 25 | 30 |
| Reporting | 报告 | 20 | 25 |
| · Test Repor | · 测试报告 | 20 | 30 |
| · Size Measurement | · 计算工作量 | 20 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 10 | 15 |
| · 合计 | 640 | 715 |
二、计算模块接口的设计与实现过程
2.1 代码如何组织
采用"入口 + 三模块"的清晰分层:
| 模块 | 职责 |
|---|---|
main.py |
命令行入口:解析三个路径参数、调度、统一异常处理、输出答案 |
plagiarism/similarity.py |
查重算法核心:normalize / _count_ngrams / _cosine / compute_similarity |
plagiarism/file_io.py |
文件读写与编码检测:read_text / write_result |
plagiarism/exceptions.py |
自定义异常体系:PlagiarismError 及其子类 |
类与函数关系:main → run → read_text → compute_similarity →(内部)
normalize → _count_ngrams → _cosine,再 format_rate → write_result。
算法模块 similarity.py 与 I/O 完全解耦,便于单独测试。
2.2 算法关键(流程)
读取原文A、抄袭版B
│
▼ normalize: 小写 + 去空白标点 → 连续字符序列
│
▼ 对 n ∈ {1,2,3}:
│ _count_ngrams(A,n) 与 _count_ngrams(B,n) → 两个频率向量
│ _cosine(...) → 该粒度相似度 sim_n
│
▼ repeat = 0.4sim_1 + 0.4sim_2 + 0.2*sim_3
│
▼ clamp 到 [0,1] → format_rate 保留两位小数 → 写入答案文件
关键函数流程图:
[compute_similarity(A,B)]
│
├── normalize(A) → A'
├── normalize(B) → B'
│ (若 A'、B' 同时为空→返回1.0;恰一为空→返回0.0)
├── total = 0
├── for (n,w) in {1:0.4, 2:0.4, 3:0.2}:
│ total += w * _similarity_for_n(A',B',n)
│ └─ _count_ngrams(A',n) → vecA
│ └─ _count_ngrams(B',n) → vecB
│ └─ _cosine(vecA, vecB)
└── return clamp(total, 0, 1)
2.3 为什么采用字符 n-gram 加权余弦(独到之处)
- 抗分词误差:中文分词依赖词典与上下文,易错;本算法在字符层面滑动窗口,
不引入第三方分词库,中英文语料通用。 - 用频数而非布尔集合:能区分"出现一次"与"反复出现",对抄袭版做局部扩写
的场景更敏感,重复率估计更平滑。 - **多粒度融合、加权"😗*一元看字面重合、二元看相邻共现、三元看更长的局部片断;
对"增删改"后的抄袭版,一元、二元置信度高,三元对增字/换序更敏感,故给
较低的 0.2 权重——既能识大体照抄,又不至于对轻微改写判 0。 - 长度归一化:余弦按向量模长归一,抄袭版被大幅扩充时重复率会自动回落,
更接近人工判读。 - 零运行期依赖、可控复杂度:一次
compute_similarity为 O(3×n) 线性扫描,
冷启动即达最优性能,契合评测"5 秒内出结果、内存<2048MB"的要求。
三、计算模块接口部分的性能改进
一、性能分析工具
Python 项目使用标准库内置的 cProfile 进行性能剖析(等价于 VS 2017 自带的
Studio Profiling Tools 之于 C++ 项目)。获取各函数累计耗时与调用次数:
python -m cProfile -s cumulative main.py samples\orig.txt samples\orig_add.txt samples\ans.txt
二、剖析定位瓶颈
对一段约 21,000 字符的虚构论文文本剖析,按累计耗时排列前几位如下(截取
cProfile 输出):
ncalls tottime cumtime filename:lineno(function)
3 0.000 0.009 similarity.py:77(_similarity_for_n)
6 0.000 0.004 similarity.py:53(_build_ngrams) # n-gram 切片
6 0.004 0.004 _collections._count_elements # Counter 统计
4 0.004 0.004 similarity.py:60(<listcomp>) # 中间列表
2 0.000 0.000 similarity.py:44(normalize)
结论:耗时集中在 3 处——
_build_ngrams用列表推导生成 n-gram 字符串切片;Counter的 C 层统计_count_elements;normalize的正则替换。
对一元(单字符)而言,_build_ngrams 返回 list(seq) 会产生一个 O(n) 的
单字符中间列表,这是可避免的内存与时间开销。
三、改进思路与实施
改进点:一元 n-gram 不再构造中间列表。
原实现:
counter = Counter(_build_ngrams(seq, 1)) # _build_ngrams(seq,1) 会生成 list(seq)
改进后:
counter = Counter()
if n == 1:
counter.update(seq) # 直接统计单字符,C 层完成,无中间列表
return counter
counter.update(_build_ngrams(seq, n))
同时,对二元/三元保留"列表推导 + Counter(C 层统计)"的方式——实测直接 Python
循环逐窗口 += 反而更慢,故不采用。改进只对剖析中最有收益的一元路径下手,
避免"为优化而优化"。
四、改进前后对比
在 29 万字符规模下,单独统计一元路径的耗时:
| 实现 | 一元统计耗时 | 说明 |
|---|---|---|
改进前(list(seq) + Counter) |
~0.0178 s | 额外分配单个字符组成的 29 万元素列表 |
改进后(Counter.update(seq)) |
~0.0113 s | 由 C 层直接逐字符计数,无中间列表 |
一元路径约提速 36%,并降低峰值内存占用。整体端到端验证:42 万字符规模下
单次查重约 0.225s,远低于 5 秒评测时限,内存占用也远低于 2048MB 上限。
五、改进后消耗最大的函数
改进后 cProfile 显示,耗时最大的仍为 Counter 的 C 层统计 _count_elements
与二元/三元的列表推导 _build_ngrams,二者均为 Counter 内部的高效 C 实现,
已无进一步纯 Python 层面的明显优化空间。若要继续压性能,后续可行方向为
用 Cython 或 numpy 向量化 n-gram 计数(因运行期"零第三方依赖"约束,暂不引入)。
四、计算模块部分单元测试展示
共编写 24 个单元测试,依据 pytest,覆盖正确性、归一化、边界、鲁棒性四类。
4.1 部分单元测试代码
def test_assignment_example_high_rate():
"""作业样例仅做增删改,重复率应较高。"""
rate = compute_similarity(ORIG, PLAG)
assert rate >= 0.6
def test_completely_different_low_rate():
"""毫无关联的两段文本重复率应很低。"""
a = "今天是晴天,我去公园散步。"
b = "量子力学研究微观粒子的运动规律与能量量子化现象。"
assert compute_similarity(a, b) <= 0.2
def test_punctuation_not_affect_result():
"""去除标点后内容相同的文本重复率接近 1.0。"""
a = "下雨,记得带伞!今天降温了?"
b = "下雨 记得带伞 今天降温了"
assert compute_similarity(a, b) >= 0.99
完整用例见 tests/,运行方式:
python -m pytest tests/ -q --cov=plagiarism --cov=main --cov-report=term
4.2 构造测试数据的思路
| 类别 | 用例意图 | 示例 |
|---|---|---|
| 正确性 | 高度相似/完全无关键对出处 | 作业样例、无关主题对 |
| 归一化 | 标点/大小写不应影响结果 | 下雨… 对 下雨 … |
| 边界 | 空文本、全空、完全一致 | ("","")→1.0;("",x)→0.0 |
| 鲁棒性 | 中文+英文混排、长度差异、超大文本 | 见 test_result_in_unit_interval |
4.3 测试覆盖率
pytest-cov 统计结果(生成命令如 5.1),覆盖率为 100%(见 COVERAGE_REPORT.txt):
Name Stmts Miss Cover
main.py 31 0 100%
plagiarism\__init__.py 2 0 100%
plagiarism\exceptions.py 5 0 100%
plagiarism\file_io.py 28 0 100%
plagiarism\similarity.py 48 0 100%
TOTAL 114 0 100%
24 passed
(此处粘贴 pytest-cov 的 HTML/终端覆盖率截图。)
五、计算模块部分异常处理说明
程序定义了清晰的异常体系,main.py 在统一入口捕获并给出友好提示,返回非零
退出码,绝不以 traceback 崩溃退出。下面每一类异常都对应一个单元测试样例。
| 异常 | 设计目标 | 对应测试用例 |
|---|---|---|
InvalidArgumentError |
命令行参数数量不足 | test_main_missing_args_returns_nonzero |
NoSuchFileError |
原文/抄袭版路径不存在 | test_read_missing_file_raises |
FileReadError |
路径是目录、无权限、解码失败 | test_read_directory_path_raises |
FileWriteError |
输出目录不存在、无写权限 | test_write_to_missing_dir_raises |
兜底 except Exception |
任何未预期异常,抑制 traceback | test_main_wraps_unexpected_error |
场景说明示例:评测若传入不存在的原文路径,程序会打印 错误: 文件不存在: …
并向标准错误输出提示,返回退出码 1,而不是打印 Python 堆栈导致"异常退出"——
这正是评测规则所防范的行为。
设计目标说明:
-
NoSuchFileError/FileReadError/FileWriteError—— 把"文件不可用"这一
最常见的评测失败场景,从底层一行行裸露的 OSError 中抽象出来,便于统一提示; -
InvalidArgumentError—— 参数个数错误是人为操作错误,对应"用法提示"而非崩溃; -
兜底异常 —— 保证健壮性兜底:只要被测点不涉及非法输入与资源缺失,程序
必在时限内正常结束并写出答案。

浙公网安备 33010602011771号