第一次个人编程作业

这个作业属于哪个课程 计科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()作为总调度,串联全部业务流程:

  1. main()调用parse_argv(),获取原文路径、抄袭版路径、输出文件路径;参数非法直接退出。
  2. 两次调用read_file(),分别读取原文、抄袭版文本;读取失败直接终止后续流程。
  3. 将读取得到的原始文本传入preprocess(),完成文本清洗,得到预处理后的干净文本。
  4. 将两份清洗后的文本传入核心计算函数calc_similarity()calc_similarity()内部会分别调用ngram_segment(),对原文、抄袭版做 2‑gram 切分。
  5. 将计算得到的重复率传给write_answer(),格式化并写入输出文件。

数据流向:命令行参数 → 文件路径 → 原始字符串 → 预处理后字符串 → n‑gram 片段集合 → 重复率浮点数 → 输出文件。

② 关键函数流程图说明

核心计算函数calc_similarity()需要绘制流程图,该函数是整个项目算法核心,内部存在条件分支判断;其余函数逻辑均为简单线性执行。
流程图如下:

屏幕截图 2026-09-14 185915

(2)算法的关键

项目采用2‑gram 滑动窗口算法实现论文重复率计算:

  1. 文本预处理阶段:过滤换行、空格、标点符号,只保留中文字符,消除格式、标点改动对查重结果带来的干扰。
  2. 2‑gram 分词:对清洗完成的中文文本,以连续 2 个汉字为窗口向后滑动切分,得到若干字符片段,使用集合存储片段,自动去重。
  3. 求集合交集:原文集合与抄袭版集合求交集,交集内的片段即为两份文本中相似的连续片段。
  4. 重复率计算公式:重复率=交集片段数量/抄袭版全部片段数量
    分母选用抄袭版片段总数,代表抄袭论文中有多少片段来源于原文,贴合查重业务含义。
  5. 边界防护:若抄袭版文本过短或者为空,切分后集合为空,直接返回 0.0,规避除零异常,防止程序异常退出,对应评测扣分项。

(3)设计独到之处

  1. 模块高内聚低耦合,单一职责
    文件 IO、预处理、分词、相似度计算、输出完全拆分。更换查重算法,只修改calc_similarity();修改清洗规则,仅改动preprocess(),不会影响读写文件等其他模块。每个函数可以独立传入字符串进行单元测试,不需要依赖真实文件。
  2. 无全局变量
    全部数据依靠入参、返回值传递,没有全局变量污染,单元测试更加方便,函数可复用。
  3. 边界异常分层隔离
    IO 异常放在read_file中处理;计算模块只负责纯文本逻辑,不处理文件读写异常。针对空文本、极短文本做保护,保证遇到极端输入不会直接崩溃退出,适配助教自动化评测的要求。
  4. 集合优化计算效率
    使用 set 集合保存 n‑gram 片段,集合求交集的运算效率高,相比列表遍历比对,减少循环次数。
  5. 输出严格格式化
    输出函数统一完成保留两位小数格式化,保证输出文件只包含浮点数,没有多余中文信息,满足作业输出规范。

4.计算模块接口部分的性能改进

说明:因为我们用的是 Python + cProfile + Snakeviz,不是 VS2017/JProfiler。

(1)改进思路

原始基线代码的文本预处理、文件读写逻辑直接写在主函数中。每次调用预处理都会执行re.compile编译正则表达式,多次重复编译带来不必要的 CPU 开销;文件读写代码散落在主流程,没有封装接口,代码复用性差,不利于模块独立测试。

优化思路分为两点:

  1. 正则预编译:将re.compile移到全局作用域,程序启动只编译一次正则对象,预处理函数直接复用,消除重复正则编译的 CPU 开销。
  2. IO 接口封装:抽取文件读取read_file()、结果写入write_answer()独立函数,使用with上下文管理器管理文件句柄,统一 IO 入口,实现模块解耦,便于对计算模块单独做性能分析。
  3. 模块拆分:把预处理、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.prof profile_base_opt.prof

生成的Snakeviz 火焰图
屏幕截图 2026-09-14 224819
图 基线版本长文本性能分析冰柱图

屏幕截图 2026-09-14 225239
图 基线版本短文本性能分析冰柱图

屏幕截图 2026-09-14 225724
图 优化版本长文本性能分析冰柱图

屏幕截图 2026-09-14 230018
图 优化版本短文本性能分析冰柱图

基线(优化前)与优化版 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_similarityngram_segment分词函数;
  • 短文本:性能瓶颈为文件 IO 操作(io.open),IO 封装后代码结构更规范。

现象解释:由于测试文本整体规模偏小,cProfile 性能采样本身引入额外开销,优化后总运行时间小幅上升,属于小样本性能测试的正常现象,并不代表优化失效。优化的核心目标 —— 消除重复正则编译已经达成。

5.计算模块部分单元测试展示

本项目论文查重程序包含文本预处理、n-gram 切片、相似度计算、文件读写与程序入口等多个功能单元。本次单元测试主要采用白盒测试方法,阅读各函数源代码,梳理代码分支、条件判断与异常处理路径,保证代码内部逻辑分支都有测试用例覆盖;同时结合等价类划分、边界值分析等测试方法,构造多组测试数据。一共设计 16 个单元测试用例,覆盖正常输入、边界输入、异常输入场景,检验程序在各类输入条件下的稳定性。

(1)测试函数与构造测试数据的思路

本次测试覆盖的核心函数:preprocess()文本预处理、ngram_segment()n 元分词、calc_similarity()相似度计算、read_file()文件读取、write_answer()结果写入、main()程序入口。
构造测试数据思路:

  1. 白盒测试:逐行分析代码分支,例如预处理的字符过滤分支、ngram 函数对文本长度判断、n 参数合法性校验、相似度计算中空集合除零保护、main 函数命令行参数判断分支,保证每一条可执行分支都存在对应的测试用例;
  2. 等价类划分:划分正常文本、纯标点、空白字符、空字符串、普通英文数字等输入等价类,检验预处理函数;
  3. 边界值分析:选取边界场景,文本长度等于 n、文本长度小于 n、n=0 非法参数、空集合计算相似度等极限输入;
  4. IO 场景:使用临时文件tempfile创建文件,测试文件读写功能,避免生成多余测试文件;使用unittest.mock模拟文件读取,在不操作真实文件的前提下验证主入口逻辑;
  5. 异常场景:主动构造非法参数,检验异常抛出逻辑,防止程序崩溃。

(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)覆盖率报告

屏幕截图 2026-09-15 002848

使用 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
posted @ 2026-09-15 01:05  yc-1  阅读(16)  评论(0)    收藏  举报