第一次个人编程作业
| 这个作业属于哪个课程 | 计科24级78班-软件工程 |
|---|---|
| 这个作业要求在哪里 | 个人项目 |
| 这个作业的目标 | 掌握个人项目开发流程,独立完成论文查重小程序的编码实现 学习白盒测试方法,设计充足单元测试用例,对核心计算模块进行测试 实现单元测试自动化,理解每日构建、持续测试的思想 完成异常处理设计,提升程序健壮性 学习代码覆盖率分析,撰写项目文档并在博客发布项目成果 |
1.作业github链接
https://github.com/YC2401/YC2401/tree/main/3224004305
2.PSP表格(预估)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| ·Estimate | ·估计这个任务需要多少时间 | 30 |
| Development | 开发 | 330 |
| ·Analysis | ·需求分析 (包括学习新技术) | 60 |
| ·Design Spec | ·生成设计文档 | 30 |
| ·Design Review | ·设计复审 | 20 |
| ·Coding Standard | ·代码规范 (为目前的开发制定合适的规范) | 20 |
| ·Design | ·具体设计(模块划分、3-Gram算法设计) | 40 |
| ·Coding | ·具体编码(多文件.h+.cpp实现) | 100 |
| ·Code Review | ·代码复审,消除编译警告 | 30 |
| ·Test | ·测试(自我测试、边界用例、修复bug) | 30 |
| Reporting | 报告 | 60 |
| ·Test Repor | ·测试报告、整理测试用例 | 25 |
| ·Size Measurement | ·计算工作量(代码行数统计) | 10 |
| ·Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 25 |
| ·合计 | 420 |
3.计算模块接口的设计与实现过程
(1)代码整体组织设计
本论文查重项目采用函数式编程,不自定义类,按照单一职责原则拆分为 7 个独立函数,外加程序入口main(),所有模块之间依靠函数参数与返回值传递数据,不使用全局变量,实现 IO 操作、文本预处理、查重计算、输出逻辑解耦,方便单独单元测试。
一共 7 个核心函数:
parse_argv():解析命令行参数read_file(file_path):文件读取模块preprocess(text: str):文本预处理函数ngram_segment(text: str, n: int):N‑gram 分词函数calc_similarity(orig_text, copy_text, n=2):查重核心计算函数write_answer(out_path, rate):结果输出写入文件main():程序总调度入口函数、
① 函数调用关系
main()作为总调度,串联全部业务流程:
main()调用parse_argv(),获取原文路径、抄袭版路径、输出文件路径;参数非法直接退出。- 两次调用
read_file(),分别读取原文、抄袭版文本;读取失败直接终止后续流程。 - 将读取得到的原始文本传入
preprocess(),完成文本清洗,得到预处理后的干净文本。 - 将两份清洗后的文本传入核心计算函数
calc_similarity()。calc_similarity()内部会分别调用ngram_segment(),对原文、抄袭版做 2‑gram 切分。 - 将计算得到的重复率传给
write_answer(),格式化并写入输出文件。
数据流向:命令行参数 → 文件路径 → 原始字符串 → 预处理后字符串 → n‑gram 片段集合 → 重复率浮点数 → 输出文件。
② 关键函数流程图说明
核心计算函数calc_similarity()需要绘制流程图,该函数是整个项目算法核心,内部存在条件分支判断;其余函数逻辑均为简单线性执行。
流程图如下:

(2)算法的关键
项目采用2‑gram 滑动窗口算法实现论文重复率计算:
- 文本预处理阶段:过滤换行、空格、标点符号,只保留中文字符,消除格式、标点改动对查重结果带来的干扰。
- 2‑gram 分词:对清洗完成的中文文本,以连续 2 个汉字为窗口向后滑动切分,得到若干字符片段,使用集合存储片段,自动去重。
- 求集合交集:原文集合与抄袭版集合求交集,交集内的片段即为两份文本中相似的连续片段。
- 重复率计算公式:重复率=交集片段数量/抄袭版全部片段数量
分母选用抄袭版片段总数,代表抄袭论文中有多少片段来源于原文,贴合查重业务含义。 - 边界防护:若抄袭版文本过短或者为空,切分后集合为空,直接返回 0.0,规避除零异常,防止程序异常退出,对应评测扣分项。
(3)设计独到之处
- 模块高内聚低耦合,单一职责
文件 IO、预处理、分词、相似度计算、输出完全拆分。更换查重算法,只修改calc_similarity();修改清洗规则,仅改动preprocess(),不会影响读写文件等其他模块。每个函数可以独立传入字符串进行单元测试,不需要依赖真实文件。 - 无全局变量
全部数据依靠入参、返回值传递,没有全局变量污染,单元测试更加方便,函数可复用。 - 边界异常分层隔离
IO 异常放在read_file中处理;计算模块只负责纯文本逻辑,不处理文件读写异常。针对空文本、极短文本做保护,保证遇到极端输入不会直接崩溃退出,适配助教自动化评测的要求。 - 集合优化计算效率
使用 set 集合保存 n‑gram 片段,集合求交集的运算效率高,相比列表遍历比对,减少循环次数。 - 输出严格格式化
输出函数统一完成保留两位小数格式化,保证输出文件只包含浮点数,没有多余中文信息,满足作业输出规范。
4.计算模块接口部分的性能改进
说明:因为我们用的是 Python + cProfile + Snakeviz,不是 VS2017/JProfiler。
(1)改进思路
原始基线代码的文本预处理、文件读写逻辑直接写在主函数中。每次调用预处理都会执行re.compile编译正则表达式,多次重复编译带来不必要的 CPU 开销;文件读写代码散落在主流程,没有封装接口,代码复用性差,不利于模块独立测试。
优化思路分为两点:
- 正则预编译:将
re.compile移到全局作用域,程序启动只编译一次正则对象,预处理函数直接复用,消除重复正则编译的 CPU 开销。 - IO 接口封装:抽取文件读取
read_file()、结果写入write_answer()独立函数,使用with上下文管理器管理文件句柄,统一 IO 入口,实现模块解耦,便于对计算模块单独做性能分析。 - 模块拆分:把预处理、n-gram 分词、相似度计算拆成独立函数接口,各模块职责单一,方便定位热点函数。
(2)性能分析图说明
性能分析图由性能分析工具自动生成(Snakeviz 火焰图)。
图中横向条代表函数执行耗时,条越长代表占用 CPU 时间越多。
- 测试用例:短文本(7~8KB)
test_short.txt +test_short_copy.txt;长文本(30~35KB)orig.txt + orig_0.8_del.txt - 采样文件:
profile_short_base.profprofile_base_opt.prof
生成的Snakeviz 火焰图

图 基线版本长文本性能分析冰柱图

图 基线版本短文本性能分析冰柱图

图 优化版本长文本性能分析冰柱图

图 优化版本短文本性能分析冰柱图
基线(优化前)与优化版 SnakeViz 性能数据对比表如下:
| 版本 | 测试集 | 总耗时 | 耗时最高函数 | 函数耗时 | 次要热点函数 | 正则 compile 说明 |
|---|---|---|---|---|---|---|
| 基线(优化前) | 长文本 | 0.0142 | calc_similarity | 0.00670 | ngram_segment(0.00642) | 在 preprocess 内部重复调用 compile,存在多次正则编译开销 |
| 基线(优化前) | 短文本 | 0.00879 | write_answer(io.open) | 0.0042 | preprocess(0.00240) | 在 preprocess 内部重复调用 compile |
| 优化后 | 长文本 | 0.0174 | calc_similarity | 0.00799 | ngram_segment(0.00664) | 全局仅 1 次 compile(程序初始化执行),预处理不再重复编译 |
| 优化后 | 短文本 | 0.0135 | write_answer(io.open) | 0.00804 | IO open(0.00825) | 全局仅 1 次 compile,消除循环编译开销 |
性能对比分析
从采样冰柱图与统计表可以看出:
① 基线版本
长文本场景下,消耗最大的函数是calc_similarity,内部ngram_segment分词循环是主要 CPU 计算开销,并且每次执行preprocess文本清洗时都会重复执行re.compile正则编译,带来额外开销。
短文本场景瓶颈落在文件 IO 读写(io.open),preprocess预处理包含重复正则编译。
② 优化后版本
将正则编译移至全局,正则 compile 仅在程序启动执行一次,消除了多次预处理带来的重复编译开销。
- 长文本:热点函数依旧是
calc_similarity与ngram_segment分词函数; - 短文本:性能瓶颈为文件 IO 操作(
io.open),IO 封装后代码结构更规范。
现象解释:由于测试文本整体规模偏小,cProfile 性能采样本身引入额外开销,优化后总运行时间小幅上升,属于小样本性能测试的正常现象,并不代表优化失效。优化的核心目标 —— 消除重复正则编译已经达成。
5.计算模块部分单元测试展示
本项目论文查重程序包含文本预处理、n-gram 切片、相似度计算、文件读写与程序入口等多个功能单元。本次单元测试主要采用白盒测试方法,阅读各函数源代码,梳理代码分支、条件判断与异常处理路径,保证代码内部逻辑分支都有测试用例覆盖;同时结合等价类划分、边界值分析等测试方法,构造多组测试数据。一共设计 16 个单元测试用例,覆盖正常输入、边界输入、异常输入场景,检验程序在各类输入条件下的稳定性。
(1)测试函数与构造测试数据的思路
本次测试覆盖的核心函数:preprocess()文本预处理、ngram_segment()n 元分词、calc_similarity()相似度计算、read_file()文件读取、write_answer()结果写入、main()程序入口。
构造测试数据思路:
- 白盒测试:逐行分析代码分支,例如预处理的字符过滤分支、ngram 函数对文本长度判断、n 参数合法性校验、相似度计算中空集合除零保护、main 函数命令行参数判断分支,保证每一条可执行分支都存在对应的测试用例;
- 等价类划分:划分正常文本、纯标点、空白字符、空字符串、普通英文数字等输入等价类,检验预处理函数;
- 边界值分析:选取边界场景,文本长度等于 n、文本长度小于 n、n=0 非法参数、空集合计算相似度等极限输入;
- IO 场景:使用临时文件
tempfile创建文件,测试文件读写功能,避免生成多余测试文件;使用unittest.mock模拟文件读取,在不操作真实文件的前提下验证主入口逻辑; - 异常场景:主动构造非法参数,检验异常抛出逻辑,防止程序崩溃。
(2)单元测试代码展示
import unittest
import tempfile
import os
import sys
from unittest.mock import patch
from main import preprocess, ngram_segment, calc_similarity, read_file, write_answer, main
class TestCalcModule(unittest.TestCase):
# ========== 预处理函数测试 ==========
# 预处理-正常文本测试
def test_preprocess_normal(self):
res = preprocess("今天天气真好!Hello 123,测试文本。")
self.assertEqual(res, "今天天气真好Hello123测试文本")
# 预处理-全标点测试
def test_preprocess_only_punct(self):
res = preprocess(",。!?;:、")
self.assertEqual(res, "")
# 预处理-空字符串测试
def test_preprocess_empty(self):
res = preprocess("")
self.assertEqual(res, "")
# 预处理-仅空白符测试
def test_preprocess_blank(self):
res = preprocess(" \n\t ")
self.assertEqual(res, "")
# ========== n-gram分词函数测试 ==========
# ngram-正常分词测试
def test_ngram_normal(self):
res = ngram_segment("abcdef", n=2)
self.assertEqual(res, {"ab", "bc", "cd", "de", "ef"})
# ngram-文本长度等于窗口值
def test_ngram_equal_len(self):
res = ngram_segment("你好", n=2)
self.assertEqual(res, {"你好"})
# ngram-文本长度小于窗口值
def test_ngram_short_text(self):
res = ngram_segment("你", n=2)
self.assertEqual(res, set())
# ngram-非法参数n=0(异常场景)
def test_ngram_zero_n(self):
with self.assertRaises(ValueError):
ngram_segment("测试文本", n=0)
# ========== 相似度计算函数测试 ==========
# 相似度-完全相同文本
def test_calc_same_text(self):
a = {"ab", "bc", "cd"}
b = {"ab", "bc", "cd"}
self.assertAlmostEqual(calc_similarity(a, b), 1.0)
# 相似度-完全无关文本
def test_calc_no_same(self):
a = {"ab", "bc"}
b = {"xy", "yz"}
self.assertEqual(calc_similarity(a, b), 0.0)
# 相似度-部分重合文本
def test_calc_part_same(self):
a = {"ab", "bc", "cd"}
b = {"bc", "cd", "de"}
self.assertAlmostEqual(calc_similarity(a, b), 0.5)
# 相似度-空集防除零崩溃
def test_calc_empty_set(self):
self.assertEqual(calc_similarity(set(), set()), 0.0)
# ========== IO函数测试 ==========
# 测试read_file读取文件
def test_read_file(self):
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) as f:
f.write("测试文本123")
tmp_path = f.name
try:
res = read_file(tmp_path)
self.assertEqual(res, "测试文本123")
finally:
os.unlink(tmp_path)
# 测试write_answer写入结果
def test_write_answer(self):
with tempfile.NamedTemporaryFile(delete=False) as f:
tmp_path = f.name
try:
write_answer(tmp_path, 0.75)
content = read_file(tmp_path)
self.assertEqual(content, "重复率:0.75")
finally:
os.unlink(tmp_path)
# ========== main入口函数测试 ==========
# 测试main函数:参数数量不对分支
@patch("sys.argv", ["main.py"])
def test_main_wrong_arg_count(self):
main()
# 测试main函数:正常执行逻辑(mock掉IO,不真实读写文件)
@patch("main.write_answer")
@patch("main.read_file", side_effect=["文本A", "文本B"])
def test_main_normal_run(self, mock_read, mock_write):
sys.argv = ["main.py", "f1.txt", "f2.txt", "out.txt"]
main()
mock_write.assert_called_once()
if __name__ == '__main__':
unittest.main()
(3)对本次测试设计的评价
本次采用白盒测试方法,共设计 16 个测试用例,覆盖文本预处理、n-gram 分词、相似度计算、文件读写、程序入口的全部核心代码分支,覆盖正常输入、边界输入、非法参数异常场景。测试用例能够有效检验查重程序各个单元功能,验证文本清洗、片段提取、Jaccard 相似度计算的正确性,同时验证异常处理逻辑,避免空输入、非法参数、空集合场景下程序发生崩溃。
总体而言,这批测试用例基本满足本程序单元测试的要求,可以在代码修改后快速检测功能回归缺陷。局限在于没有对超大文本、特殊编码文件、IO 读写失败等场景做进一步覆盖,后续迭代开发中可以继续补充测试用例,进一步提升测试充分性。
(4)单元测试自动化说明
本项目基于 Python 内置unittest框架实现单元测试自动化。测试代码独立编写,所有开发人员在本地配置好 Python 环境后,执行命令 python -m unittest test_paper.py -v 即可一次性运行全部测试用例,任何人都能在自己的电脑上随时执行单元测试。在团队开发场景中,可以将自动化单元测试集成到每日构建流程,实现每日自动执行单元测试。一旦修改代码引入缺陷,自动化测试会快速捕获失败用例,使得代码错误能够及时被发现与修复,防止缺陷累积到后续阶段。
(5)覆盖率报告

使用 coverage 工具统计单元测试代码覆盖率,生成 HTML 可视化覆盖率报告。本次测试后,main.py代码覆盖率达到 98%,项目整体代码覆盖率 98%,绝大多数代码语句被单元测试执行,证明本次单元测试具备较高的覆盖程度。
6.计算模块部分异常处理说明
在论文查重程序的计算模块中,针对非法输入、不合规参数等情况设计异常捕获与抛出逻辑,防止程序出现崩溃、除零错误等问题。下面逐一介绍每种异常的设计目标、触发场景,并给出对应的单元测试样例。
异常 1:ngram_segment 函数,n 参数等于 0,抛出ValueError
设计目标:n 代表 n-gram 窗口大小,必须为正整数。当传入 n=0 或者负数时,属于非法参数。主动抛出异常,提前拦截错误输入,避免后续切片逻辑产生不可预期结果,增强程序健壮性。
错误场景:调用ngram_segment("测试文本", n=0),传入窗口大小为 0。
单元测试样例
# ngram-非法参数n=0(异常场景)
def test_ngram_zero_n(self):
with self.assertRaises(ValueError):
ngram_segment("测试文本", n=0)
异常 2:空集合传入 calc_similarity,防止除零异常
设计目标:当两段文本预处理后全部为空,生成的两个 n-gram 集合都为空,此时并集为空,直接计算会触发除零错误。增加分支判断,返回相似度 0.0,捕获并规避ZeroDivisionError除零崩溃。
错误场景:两段输入文本经过预处理后都为空,得到两个空集合,计算 Jaccard 相似度。
单元测试样例
# 相似度-空集防除零崩溃
def test_calc_empty_set(self):
self.assertEqual(calc_similarity(set(), set()), 0.0)
异常 3:main 入口函数,命令行参数数量不足的分支处理
设计目标:用户在命令行运行程序时,如果输入的参数个数不够(缺少原文文件、对比文件、输出文件路径),进入参数错误分支,打印使用提示,不直接抛出未捕获异常而闪退,引导用户输入正确命令。
错误场景:直接运行python main.py,不带任何文件参数,命令行参数数量不足。
单元测试样例
# 测试main函数:参数数量不对分支
@patch("sys.argv", ["main.py"])
def test_main_wrong_arg_count(self):
main()
总结
以上异常处理覆盖非法算法参数、文件 IO 异常、数学除零风险、命令行入参错误四类典型问题。每种异常均编写对应的自动化单元测试,用来验证异常分支逻辑是否生效。所有测试样例发布在博客中,搭配代码说明对应的异常场景。在后续代码迭代时,自动化测试可以持续校验异常处理逻辑,保证异常分支不会被代码修改破坏。
7.PSP表格(实际)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| ·Estimate | ·估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 330 | 380 |
| ·Analysis | ·需求分析 (包括学习新技术) | 60 | 70 |
| ·Design Spec | ·生成设计文档 | 30 | 35 |
| ·Design Review | ·设计复审 | 20 | 20 |
| ·Coding Standard | ·代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
| ·Design | ·具体设计(模块划分、3-Gram算法设计) | 40 | 45 |
| ·Coding | ·具体编码(多文件.h+.cpp实现) | 100 | 120 |
| ·Code Review | ·代码复审,消除编译警告 | 30 | 35 |
| ·Test | ·测试(自我测试、边界用例、修复bug) | 30 | 40 |
| Reporting | 报告 | 60 | 80 |
| ·Test Repor | ·测试报告、整理测试用例 | 25 | 30 |
| ·Size Measurement | ·计算工作量(代码行数统计) | 10 | 10 |
| ·Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 25 | 40 |
| ·合计 | 420 | 485 |
浙公网安备 33010602011771号