作业一

GitHub 仓库地址:https://github.com/LS-XH/3124004171

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15693
这个作业的目标 学习使用github进行项目管理,熟悉项目开发流程和规划

PSP 记录(打分点2,7)

估计耗时在编码开始前填写,实际耗时在开发、测试和报告完成后填写,单位均为分钟。

PSP Personal Software Process Stages 估计耗时(min) 实际耗时(min)
Planning 计划 30 20
· Estimate · 估计这个任务需要多少时间 30 20
Development 开发 390 285
· Analysis · 需求分析(包括学习新技术) 40 30
· Design Spec · 生成设计文档 30 20
· Design Review ·设计复审 20 15
· Coding Standard ·代码规范 10 10
· Design ·具体设计 40 30
· Coding ·具体编码 120 75
· Code Review ·代码复审 30 25
· Test ·测试、自测、修改代码 100 80
Reporting 报告 120 95
· Test Report ·测试报告 40 30
· Size Measurement ·计算工作量 20 15
· Postmortem & Process Improvement Plan ·事后总结并提出过程改进计划 60 50
合计 540 400

计算模块接口的设计与实现(打分点3)

项目没有需要保存状态的对象,因此没有为了形式而设计类,而是采用职责单一的函数式接口。命令行、文件读写和计算逻辑彼此分离,其中 calculate_similarity 是核心计算模块对外提供的接口,不直接访问文件,因而可以独立测试。

核心模块和函数

所属文件 模块或函数 输入 输出 主要功能 直接调用或依赖
main.py 命令行入口模块 命令行中的三个绝对路径 进程退出码 连接用户输入、文件接口和计算模块,是程序的执行入口 main()
main.py main(argv=None) 参数字符串序列;省略时读取 sys.argv int:0 成功、1 运行错误、2 参数错误 检查参数数量及绝对路径,调用业务入口,统一捕获可预期异常并向标准错误输出提示 run()
main.py run(original_path, suspect_path, answer_path) 三个 Path 对象 float 重复率 读取两篇论文,调用纯计算接口,并以两位小数写入指定答案文件 read_text()calculate_similarity()Path.write_text()
plagiarism_checker.py 相似度计算模块 两段文本或文本路径 文本、特征集合或相似度 集中实现编码兼容、文本预处理、特征生成和相似度公式 下列计算函数
plagiarism_checker.py read_text(path) 输入文件的 Path str 文本 优先按 UTF-8(含 BOM)读取,解码失败后尝试 GB18030;无法解码时抛出明确异常 Path.read_text()
plagiarism_checker.py normalize_text(text) 原始字符串 规范化字符串 执行 NFKC 全半角统一、英文小写化,并移除空白和标点,降低排版差异干扰 unicodedata.normalize()、正则表达式
plagiarism_checker.py tokenize(text) 规范化文本 list[str] 提取英文单词和中文二元子词,作为可单独使用和测试的辅助分词接口 _ngrams()
plagiarism_checker.py _ngrams(text, size=2) 文本和 n-gram 长度 Iterable[str] 以生成器形式产生连续字符二元组;单字符文本自身作为一个特征,避免额外列表内存
plagiarism_checker.py _cosine_similarity(left, right) 两个特征频次 Counter [0, 1] 内的 float 计算稀疏词频向量的余弦相似度,衡量两篇文本的特征频率分布 math.sqrt()Counter.get()
plagiarism_checker.py _dice_similarity(left, right) 两个特征频次 Counter [0, 1] 内的 float 计算两个多重集合的 Sørensen-Dice 系数,衡量实际重合特征数量 Counter 交集运算
plagiarism_checker.py calculate_similarity(original, suspect) 原文和疑似抄袭文本 [0, 1] 内的 float 规范化两篇文本,只生成一次特征计数,融合 0.65 × cosine + 0.35 × dice,并处理空文本边界 normalize_text()_ngrams()_cosine_similarity()_dice_similarity()

以下划线开头的函数属于模块内部实现细节;read_textnormalize_texttokenizecalculate_similarity 可以作为公开接口导入。实际命令行计算的主路径不会调用 tokenize,而是让两项相似度直接复用同一对 _ngrams Counter,以减少约一半的特征生成开销。

性能分析和测试辅助模块

所属文件 模块或函数 功能
profile/profile_benchmark.py build_documents(repetitions=12000) 构造包含局部增删改的大型中文原文和疑似文本,为性能测试提供可重复输入
profile/profile_benchmark.py benchmark() 调用 calculate_similarity() 完成一次大文本基准计算并返回结果
profile/generate_profile_report.py cumulative_time(stats, function_name) pstats.Stats 中提取指定函数的累计耗时
profile/generate_profile_report.py main() 读取优化前后的 .prof 数据,计算耗时下降比例,并生成 TXT 与 SVG 性能报告
tests/test_main.py 文件接口测试模块 测试两位小数输出、正确参数、参数不足、相对路径、文件不存在和输出失败等分支
tests/test_plagiarism_checker.py 核心算法测试模块 测试文本规范化、特征生成、数学公式、字符编码、空文本和相似度边界

调用关系

下面的图只表示函数之间的静态调用关系,不表示程序处理步骤:

graph TD CLI[命令行 / sys.argv] --> MAIN[main.main] MAIN --> RUN[main.run] RUN --> READ1[read_text:原文] RUN --> READ2[read_text:疑似文本] RUN --> CALC[calculate_similarity] CALC --> NORM[normalize_text × 2] CALC --> NGRAM[_ngrams × 2] CALC --> COS[_cosine_similarity] CALC --> DICE[_dice_similarity] RUN --> WRITE[Path.write_text:答案] BENCH[profile.benchmark] --> CALC TEST1[test_main] --> MAIN TEST1 --> RUN TEST2[test_plagiarism_checker] --> CALC

性能分析与改进(打分点4)

性能分析与改进阶段实际耗时 35 分钟。使用 Python 自带的 cProfile 对合计约 61.2 万字符的两篇构造文本进行分析,基准代码为 profile/profile_benchmark.py。首个版本的热点是 Counter 构造及 _ngrams 生成器:余弦和 Dice 分别生成特征,共建立四份 Counter。

优化前 cProfile 按累计耗时排序的核心函数如下:

函数 调用次数 函数自身耗时 累计耗时 分析结论
calculate_similarity() 1 0.006 s 0.311 s 整个核心接口累计耗时最大,是性能改进的主要对象
_ngrams 生成器表达式 1,056,000 0.139 s 0.139 s 内部实际计算热点,说明特征被重复生成
tokenize() 2 0.000 s 0.116 s 累计时间主要来自其内部生成、扩展和计数操作
normalize_text() 2 0.001 s 0.022 s 文本规范化耗时较小,不是首要瓶颈

改进后,两种指标复用原文和疑似文本各一份二元组 Counter,生成器调用次数由约 105.6 万次降至 52.8 万次。核心函数累计耗时降至 0.1692 秒,下降 45.6%,相似度结果仍为 0.6823。性能改进与复测约耗时 35 分钟。

性能指标 实测结果
原文字符数 300,000
疑似文本字符数 312,000
优化前核心函数累计耗时 0.3110 s
优化后核心函数累计耗时 0.1692 s
耗时下降比例 45.6%
tracemalloc 测得峰值额外内存 5.30 MiB
优化前后相似度 均为 0.6823

时间和内存都远低于评测限制的 5 秒与 2048 MB。字符二元组保留局部顺序,余弦与 Dice 的加权同时兼顾频率分布和实际重合量;完全相同、完全不同、局部增删改等自动化测试均符合预期,有效课程 add 样例输出 0.84。由于其余四份课程样例实际为 HTML 页面,项目不使用这些无效文件虚构准确率结论。

图片

性能分析可以使用以下命令复现:

python -m cProfile -o profile/before.prof profile/profile_benchmark.py
# 切换到优化后的代码,再生成 after.prof
python -m cProfile -o profile/after.prof profile/profile_benchmark.py
python profile/generate_profile_report.py

生成脚本从 .prof 数据自动输出 profile/profile_report.txtprofile/performance_comparison.svg

单元测试与覆盖率(打分点5)

采用等价类、边界值和白盒分支覆盖设计了 21 个自动化测试,包括完全相同、完全不同、局部增删改、对称性、双方空文本、单方空文本、纯标点、全半角与大小写、单字符、多重特征、UTF-8 BOM、GB18030、未知编码、文件不存在、参数数量错误、相对路径和输出目录不存在等情况。

测试对象和数据构造思路如下:

被测试函数或模块 测试数据构造方法 主要验证内容
calculate_similarity() 构造完全相同、完全不同、局部替换和前后顺序互换的文本 结果边界、局部修改敏感性和算法对称性
normalize_text() 混合全角/半角、大小写、空格和中英文标点 与内容无关的格式差异被正确消除
_ngrams()tokenize() 使用空串、单字符、三字符及中英文混合内容 循环边界、短文本分支和特征顺序
_cosine_similarity()_dice_similarity() 手工构造空 Counter、单边为空、完全重合及重复特征 数学公式及零向量分支正确
read_text() 临时生成 UTF-8 BOM、GB18030、非法字节和不存在的文件 编码回退与异常分支正确
main()run() 使用临时绝对路径、错误参数数量、相对路径和无效输出目录 文件接口、退出码、两位小数格式和错误提示

代表性的计算模块测试如下:

def test_small_edit_retains_similarity() -> None:
    original = "今天是星期天,天气晴,今天晚上我要去看电影。"
    suspect = "今天是周天,天气晴朗,我晚上要去看电影。"
    assert 0.5 < calculate_similarity(original, suspect) < 1.0

该用例直接测试纯计算接口,通过只替换“星期天/周天”、增加“晴朗”中的字符和删除一个“今天”,验证少量增删改后重复率应下降但仍明显大于完全不同文本。数学边界另用可人工计算期望值的 Counter 验证:

def test_dice_handles_multisets_and_empty_vectors() -> None:
    assert _dice_similarity(Counter(), Counter()) == 1.0
    assert _dice_similarity(Counter({"ab": 2}), Counter()) == 0.0
    assert _dice_similarity(Counter({"ab": 2}), Counter({"ab": 1})) == pytest.approx(
        2 / 3
    )

执行以下命令进行测试和质量检查:

python -m pytest
python -m ruff check .
python -m ruff format --check .

测试结果为 21 passed,语句与分支综合覆盖率为 96.43%,超过配置中要求的 95%;Ruff 静态分析和格式检查均为零警告。
图片

处理说明(打分点6)

异常处理的目标是在输入不合法时明确失败、返回非零退出码,并且不留下伪造的答案文件。

异常类型 设计目标 对应错误场景 单元测试样例
参数错误 在访问文件前拒绝无效调用,打印标准用法并返回退出码 2 参数不足、多余或任一路径不是绝对路径 test_main_rejects_wrong_argument_counttest_main_rejects_relative_paths
输入文件 I/O 错误 不发生异常退出,不生成误导性的答案文件,返回退出码 1 文件不存在、无读取权限或把目录作为输入文件 test_main_handles_missing_input_file
字符编码错误 UTF-8 失败后兼容 GB18030,两种编码均失败才报告明确错误 输入包含两种编码都无法解析的非法字节 test_read_text_rejects_unknown_encoding
输出文件 I/O 错误 计算后若答案无法落盘,给出错误信息并返回退出码 1 输出目录不存在或文件不可写 test_main_handles_unwritable_output_location
空正文边界 对合法但没有正文的输入给出确定结果,不发生除零错误 双方为空、单方为空或文本仅含标点 test_empty_content_boundaries

下面为每类异常或边界情况各选取的实际测试代码。

参数错误:

def test_main_rejects_relative_paths(capsys) -> None:
    assert main(["original.txt", "suspect.txt", "answer.txt"]) == 2
    assert "绝对路径" in capsys.readouterr().err

输入文件不存在:

def test_main_handles_missing_input_file(tmp_path: Path, capsys) -> None:
    missing = tmp_path / "missing.txt"
    answer = tmp_path / "answer.txt"
    assert main([str(missing), str(missing), str(answer)]) == 1
    assert "错误" in capsys.readouterr().err
    assert not answer.exists()

无法识别字符编码:

def test_read_text_rejects_unknown_encoding(tmp_path) -> None:
    invalid_file = tmp_path / "invalid.txt"
    invalid_file.write_bytes(b"\xff")
    with pytest.raises(ValueError, match="无法识别文件编码"):
        read_text(invalid_file)

输出目录不存在:

def test_main_handles_unwritable_output_location(tmp_path: Path, capsys) -> None:
    original = tmp_path / "original.txt"
    suspect = tmp_path / "suspect.txt"
    original.write_text("正文", encoding="utf-8")
    suspect.write_text("正文", encoding="utf-8")
    invalid_answer = tmp_path / "missing-directory" / "answer.txt"
    assert main([str(original), str(suspect), str(invalid_answer)]) == 1
    assert "错误" in capsys.readouterr().err

空正文边界使用参数化测试同时覆盖三个分支:

@pytest.mark.parametrize(
    ("left", "right", "expected"),
    [("", "", 1.0), ("", "正文", 0.0), ("!!!", "??", 1.0)],
)
def test_empty_content_boundaries(left: str, right: str, expected: float) -> None:
    assert calculate_similarity(left, right) == expected
posted @ 2026-09-15 01:14  星痕酱丫  阅读(7)  评论(0)    收藏  举报