第一次个人编程作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15693
这个作业的目标 完成个人项目“论文查重”

作业github链接:https://github.com/gdutmi/3124004105
一、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 10 10
Estimate 估计这个任务需要多少时间 10 10
Development 开发 110 220
Analysis 需求分析 (包括学习新技术) 10 20
Design Spec 生成设计文档 10 20
Design Review 设计复审 5 20
Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 30
Design 具体设计 20 40
Coding 具体编码 25 40
Code Review 代码复审 10 20
Test 测试(自我测试,修改代码,提交修改) 20 30
Reporting 报告 60 60
Test Repor 测试报告 20 20
Size Measurement 计算工作量 20 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 20 20
合计 180 290

二、计算模块接口的设计

  1. 关键函数及其作用

read_file(filepath)
作用:负责文件读取的 I/O 操作。接收一个文件路径,尝试以 UTF-8 编码读取文件内容并返回字符串。

write_result(filepath, similarity)
作用:负责结果输出的 I/O 操作。接收输出路径和相似度浮点数,将结果格式化后写入文件。

preprocess_text(text)
作用:文本清洗(数据预处理)。利用正则表达式去除文本中的标点符号(如逗号、句号)、空格、换行符等非内容字符。

calculate_similarity(text1, text2)
作用:核心算法函数。计算两段文本的 Jaccard 相似度。

main()
作用:程序的主入口和调度中心。负责解析命令行参数,并按顺序调用上述功能函数。

2.函数与函数之间的关系

整个程序的执行流程是线性的(流水线式),main() 函数充当了控制器的角色:

启动:main() 首先运行,解析命令行参数,获取原文路径、抄袭版路径和输出路径。

数据输入:main() 调用 read_file() 两次,分别获取原文和抄袭版的原始文本内容。

数据清洗:main() 将原始文本传递给 preprocess_text(),得到去除标点和空格的纯净文本。

核心计算:main() 将清洗后的两段文本传递给 calculate_similarity(),得到相似度浮点数。

数据输出:main() 将相似度结果传递给 write_result(),写入指定文件。

结束:打印提示信息,程序结束。

3.算法独到之处

使用 set 是此算法的效率关键。它将时间复杂度从 O(N^2) 降低到接近 O(N),因为集合的查找和交集运算非常快。

三、计算模块接口部分的性能改进

e52682120ebc73e2c72c08cc152464a6

文本预处理 preprocess_text 是最大瓶颈,累计耗时占总时间的 55.6%,其中正则引擎 re.sub 占 0.013 s;其次是相似度计算 calculate_similarity,占 25.9%。

第一次改进:
re.sub 每次都要启动正则引擎,直觉上认为用 Python 层的字符过滤(生成器 + isascii() / isalnum() 判断)能避开正则引擎的开销,理论上应更快。结果:cProfile 采样显示总耗时从 0.027 s 劣化到 0.240 s,慢了约 8.9 倍。

第二次改进:
保留 re.sub 的预处理方案(它已是最优),转而优化 calculate_similarity。原实现在计算并集时使用 set1.union(set2),会额外创建一个新集合对象。可以改用 len(A) + len(B) - len(A∩B) 直接计算并集大小,避免构建第三个集合,节省内存与时间。结果:calculate_similarity 的累计耗时从 0.007 s 降至 0.006 s,程序总耗时从 0.027 s 降至 0.018 s,整体提速约 33%。
image

四、计算模块部分单元测试展示

  1. 单元测试代码
点击查看代码
"""check_plagiarism.py 的单元测试。"""
import os
import sys
import tempfile
import unittest
from io import StringIO
from unittest.mock import patch

import check_plagiarism as cp


class TestPreprocessText(unittest.TestCase):
    """测试 preprocess_text:文本预处理。"""

    def test_mixed_chinese_english_digit(self):
        """中英数混合:只保留中文、字母、数字。"""
        result = cp.preprocess_text("你好,World!123 世界。")
        self.assertEqual(result, "你好World123世界")

    def test_only_punctuation(self):
        """纯标点:结果为空串。"""
        result = cp.preprocess_text(",。!?、;:''【】")
        self.assertEqual(result, "")

    def test_empty_string(self):
        """空串:返回空串。"""
        result = cp.preprocess_text("")
        self.assertEqual(result, "")

    def test_space_and_newline_removed(self):
        """空格与换行:被移除。"""
        result = cp.preprocess_text("abc def\nghi\tjkl")
        self.assertEqual(result, "abcdefghijkl")


class TestCalculateSimilarity(unittest.TestCase):
    """测试 calculate_similarity:Jaccard 相似度。"""

    def test_identical_text(self):
        """完全相同:相似度 1.0。"""
        similarity = cp.calculate_similarity("abcdef", "abcdef")
        self.assertAlmostEqual(similarity, 1.0)

    def test_completely_different(self):
        """完全不同:相似度 0.0。"""
        similarity = cp.calculate_similarity("abc", "xyz")
        self.assertAlmostEqual(similarity, 0.0)

    def test_partial_overlap(self):
        """部分重叠:交集 / 并集。"""
        # 交集 {a,b} 大小 2,并集 {a,b,c,d} 大小 4,结果 2/4 = 0.5
        similarity = cp.calculate_similarity("abc", "abd")
        self.assertAlmostEqual(similarity, 0.5)

    def test_both_empty(self):
        """两段都为空:返回 0.0。"""
        similarity = cp.calculate_similarity("", "")
        self.assertEqual(similarity, 0.0)

    def test_one_empty(self):
        """一段为空:返回 0.0。"""
        similarity = cp.calculate_similarity("abc", "")
        self.assertEqual(similarity, 0.0)


class TestReadFile(unittest.TestCase):
    """测试 read_file:文件读取。"""

    def test_read_normal_file(self):
        """正常读取:内容正确。"""
        with tempfile.NamedTemporaryFile(
            mode='w', suffix='.txt', delete=False, encoding='utf-8'
        ) as tmp:
            tmp.write("测试内容 hello")
            tmp_path = tmp.name
        try:
            content = cp.read_file(tmp_path)
            self.assertEqual(content, "测试内容 hello")
        finally:
            os.unlink(tmp_path)

    def test_read_missing_file_exits(self):
        """文件不存在:程序打印错误并退出。"""
        with self.assertRaises(SystemExit) as ctx:
            cp.read_file("不存在的文件_xyz.txt")
        self.assertEqual(ctx.exception.code, 1)


class TestWriteResult(unittest.TestCase):
    """测试 write_result:结果写入。"""

    def test_write_result_format(self):
        """正常写入:保留两位小数。"""
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = os.path.join(tmpdir, "out.txt")
            cp.write_result(out_path, 0.876)
            with open(out_path, 'r', encoding='utf-8') as f:
                self.assertEqual(f.read(), "0.88")

    def test_write_result_to_invalid_path_exits(self):
        """路径不可写:程序打印错误并退出。"""
        with self.assertRaises(SystemExit) as ctx:
            cp.write_result("/不存在的目录_xyz/out.txt", 0.5)
        self.assertEqual(ctx.exception.code, 1)


class TestMain(unittest.TestCase):
    """测试 main:命令行入口。"""

    def test_main_wrong_arg_count_exits(self):
        """参数数量不足:打印用法并退出。"""
        with patch.object(sys, 'argv', ['check_plagiarism.py']):
            with self.assertRaises(SystemExit) as ctx:
                cp.main()
            self.assertEqual(ctx.exception.code, 1)

    def test_main_normal_flow(self):
        """正常流程:两段相同文本,输出相似度 1.00。"""
        with tempfile.TemporaryDirectory() as tmpdir:
            orig = os.path.join(tmpdir, "orig.txt")
            copy = os.path.join(tmpdir, "copy.txt")
            out = os.path.join(tmpdir, "out.txt")
            with open(orig, 'w', encoding='utf-8') as f:
                f.write("今天天气真好")
            with open(copy, 'w', encoding='utf-8') as f:
                f.write("今天天气真好")

            with patch.object(sys, 'argv', ['check_plagiarism.py', orig, copy, out]):
                cp.main()

            with open(out, 'r', encoding='utf-8') as f:
                self.assertEqual(f.read(), "1.00")


if __name__ == "__main__":
    unittest.main()
  1. 测试函数说明
测试类 测试函数 测试的函数 构造的测试数据 预期结果 测试类型
TestPreprocessText test_mixed_chinese_english_digit preprocess_text "你好,World!123 世界。" "你好World123世界" 正常
TestPreprocessText test_only_punctuation preprocess_text ",。!?、;:''【】" "" 边界
TestPreprocessText test_empty_string preprocess_text "" "" 边界
TestPreprocessText test_space_and_newline_removed preprocess_text "abc def\nghi\tjkl" "abcdefghijkl" 正常
TestCalculateSimilarity test_identical_text calculate_similarity "abcdef" vs "abcdef" 1.0 正常
TestCalculateSimilarity test_completely_different calculate_similarity "abc" vs "xyz" 0.0 边界
TestCalculateSimilarity test_partial_overlap calculate_similarity "abc" vs "abd" 0.5 正常
TestCalculateSimilarity test_both_empty calculate_similarity "" vs "" 0.0 边界
TestCalculateSimilarity test_one_empty calculate_similarity "abc" vs "" 0.0 边界
TestReadFile test_read_normal_file read_file 临时文件内容 "测试内容 hello" 返回 "测试内容 hello" 正常
TestReadFile test_read_missing_file_exits read_file 路径 "不存在的文件_xyz.txt" 抛出 SystemExit,码 1 异常
TestWriteResult test_write_result_format write_result 0.876 到临时文件 文件内容 "0.88" 正常
TestWriteResult test_write_result_to_invalid_path_exits write_result 路径 "/不存在的目录_xyz/out.txt" 抛出 SystemExit,码 1 异常
TestMain test_main_wrong_arg_count_exits main sys.argv = ['check_plagiarism.py'] 抛出 SystemExit,码 1 异常
TestMain test_main_normal_flow main 两个内容相同的临时文件 "今天天气真好" 输出文件为 "1.00" 正常(端到端)
  1. 覆盖率截图
    dae7f5a86faf264fd59de8a9752006ed

五、计算模块部分异常处理说明

异常 场景 设计目标
FileNotFoundError 输入文件路径不存在 提示文件不存在,避免程序崩溃
PermissionError 文件无权限读取或写入 提示权限错误
IsADirectoryError 传入的路径是目录而非文件 提示路径类型错误
SystemExit(参数不足时主动触发) 命令行参数数量不等于 4 提示正确用法并退出
OSError(统一捕获) 其他文件系统相关异常 统一捕获,避免异常退出

异常测试样例

test_read_missing_file_exits ... 读取文件 不存在的文件_xyz.txt 失败: [Errno 2] No such file or directory: '不存在的文件_xyz.txt' ok

六、实际 PSP 时间
见上文。

七、总结

通过本次项目,完整实践了软件工程的基本流程:
需求分析 → 设计 → 编码 → 代码质量检查 → 单元测试 → 性能优化 → 文档撰写
我还获得收获:

  • 函数必须有类型注解和 docstring;
  • 精确捕获异常,避免 except Exception;
  • 掌握 unittest 框架,覆盖正常、边界、异常三类路径;
  • 使用 coverage 生成 HTML 可视化覆盖率报告;
  • 熟练 git add / commit / push;
  • 处理 rejected 冲突:git pull --rebase;
posted @ 2026-09-15 17:14  口米  阅读(9)  评论(0)    收藏  举报