第一次个人编程作业

第一次个人软件工程作业

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 及其子类

类与函数关系:mainrunread_textcompute_similarity →(内部)
normalize_count_ngrams_cosine,再 format_ratewrite_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 加权余弦(独到之处)

  1. 抗分词误差:中文分词依赖词典与上下文,易错;本算法在字符层面滑动窗口,
    不引入第三方分词库,中英文语料通用。
  2. 用频数而非布尔集合:能区分"出现一次"与"反复出现",对抄袭版做局部扩写
    的场景更敏感,重复率估计更平滑。
  3. **多粒度融合、加权"😗*一元看字面重合、二元看相邻共现、三元看更长的局部片断;
    对"增删改"后的抄袭版,一元、二元置信度高,三元对增字/换序更敏感,故给
    较低的 0.2 权重——既能识大体照抄,又不至于对轻微改写判 0。
  4. 长度归一化:余弦按向量模长归一,抄袭版被大幅扩充时重复率会自动回落,
    更接近人工判读。
  5. 零运行期依赖、可控复杂度:一次 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 处——

  1. _build_ngrams 用列表推导生成 n-gram 字符串切片;
  2. Counter 的 C 层统计 _count_elements;
  3. 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 —— 参数个数错误是人为操作错误,对应"用法提示"而非崩溃;

  • 兜底异常 —— 保证健壮性兜底:只要被测点不涉及非法输入与资源缺失,程序
    必在时限内正常结束并写出答案。

posted @ 2026-09-15 23:58  kecit  阅读(5)  评论(0)    收藏  举报