第一次个人项目
作业 GitHub 链接:https://github.com/konglang123/3124007607/tree/main/3124007607
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | 作业要求 |
| 这个作业的目标 | 使用 Python 完成支持命令行文件输入输出的论文查重程序,并通过 PSP、单元测试、分支覆盖率、代码质量检查和性能分析实践个人软件开发流程。 |
第一次个人编程作业——论文查重
学号:3124007607
开发语言:Python 3
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | — | — |
| · Estimate | · 估计任务所需时间 | 10 | 20 |
| Development | 开发 | — | — |
| · Analysis | · 需求分析(包括学习新技术) | 40 | 30 |
| · Design Spec | · 生成设计文档 | 30 | 40 |
| · Design Review | · 设计复审 | 15 | 25 |
| · Coding Standard | · 制定代码规范 | 10 | 10 |
| · Design | · 具体设计 | 30 | 35 |
| · Coding | · 具体编码 | 90 | 95 |
| · Code Review | · 代码复审 | 25 | 25 |
| · Test | · 测试与修改 | 60 | 60 |
| Reporting | 报告 | — | — |
| · Test Report | · 测试报告 | 30 | 40 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结与改进计划 | 30 | 30 |
| 合计 | 380 | 420 |
实际耗时比预估多 40 分钟,主要偏差出现在设计文档、设计复审和具体设计阶段。开始编码后,我发现文件异常处理、短文本边界和性能验证需要比预想更多的设计工作。需求分析的实际耗时略低于预估,而编码、设计和测试相关工作有所增加。
二、需求分析
程序从命令行接收三个参数:原文文件绝对路径、待查重文件绝对路径和答案文件绝对路径。程序读取前两个 UTF-8 文本文件,计算 [0.0, 1.0] 区间内的重复率,并在答案文件中写入保留两位小数的结果。
以 Windows 为例,运行方式如下:
python main.py D:\软件工程\3124007607\samples\orig.txt D:\软件工程\3124007607\samples\orig_add.txt D:\软件工程\3124007607\samples\ans.txt
示例输出为 0.52。程序还需要处理参数数量错误、输入文件不存在、文件编码错误和答案文件无法写入等情况。运行过程中不访问网络,也不会读写命令行参数之外的业务文件。
三、计算模块接口的设计与实现
3.1 代码组织
项目把命令行输入输出与相似度计算分开,主要包含两个模块:
命令行参数
↓
main.py
├── read_text():读取 UTF-8 文本
├── run():检查参数并组织执行流程
└── write_result():写入两位小数结果
↓
plagiarism_checker.py
├── normalize_text():文本规范化
├── _character_ngrams():统计字符 n-gram
├── _cosine_similarity():计算余弦相似度
└── calculate_similarity():计算最终重复率
main.py 只处理命令行和文件系统,plagiarism_checker.py 只负责字符串计算。这样可以在单元测试中直接测试算法,不必为每个计算测试都创建文件。
3.2 程序入口和执行流程
程序入口通过 sys.argv[1:] 取得命令行中的三个路径:
if __name__ == "__main__":
raise SystemExit(run(sys.argv[1:]))
run()检查参数数量,然后依次读取原文、读取待查重文本、调用 calculate_similarity()并写入答案。运行成功返回退出码 0,输入输出错误返回 1,参数格式错误返回 2。
读取文件 → 规范化文本 → 统计 2-gram/3-gram
→ 计算余弦相似度 → 加权融合 → 保留两位小数写入文件
3.3 文本规范化
查重前先调用 normalize_text():
- 使用 casefold() 统一英文大小写;
- 删除空格、换行、标点和下划线;
- 保留中文字符、英文字母和数字。
因此,只有标点、空白或英文大小写不同的文本会被视为相同内容。
3.4 字符 n-gram
中文文本不能简单地按空格切分。如果使用外部分词词典,还可能受到未登录词和词典版本的影响,因此本项目使用字符级 n-gram。
例如,“今天是星期天”的二元组为:
今天、天是、是星、星期、期天
三元组为:
今天是、天是星、是星期、星期天
二元组对局部增删改具有一定容错性,三元组能更好地反映连续内容和语序。程序分别计算两类相似度。
3.5 余弦相似度
程序使用 Counter 统计每个 n-gram 的出现次数,将论文表示为稀疏频率向量。余弦相似度公式为:
cos(A, B) = (A · B) / (||A|| × ||B||)
最终分数采用加权组合:
重复率 = 0.6 × 二元组相似度 + 0.4 × 三元组相似度
结果被限制在 [0.0, 1.0]。完全相同的规范化文本直接返回 1.0;只有一方为空返回 0.0;双方都为空返回 1.0。对于长度分别为 n 和 m 的两篇论文,核心算法的时间复杂度为 O(n + m),空间复杂度为 O(n + m)。
四、计算模块性能分析与改进
4.1 优化前的性能分析
我使用 Python 自带的 cProfile 分析计算模块。性能脚本构造两篇较长的中文文本并连续执行 10 次查重:
python profile_checker.py
优化前的主要数据如下:
总耗时:3.861 秒
函数调用次数:14,802,253 次
_character_ngrams 累计耗时:3.580 秒
_character_ngrams() 占总时间约 93%,说明瓶颈不在余弦公式,而在逐字符生成二元组和三元组。原实现使用 Python 生成器反复执行字符串切片,产生了约 1480 万次函数调用。

4.2 优化方法
原来的 n-gram 统计核心为:
Counter(text[index:index + size] for index in range(len(text) - size + 1))
优化后使用 Python 内置的 zip() 构造滑动窗口,并使用 join()合并字符:
shifted_texts = (text, *(text[offset:] for offset in range(1, size)))
return Counter(map("".join, zip(*shifted_texts, strict=False)))
这样把大量逐字符操作转移到 Python 内置函数中执行,显著减少 Python 层函数调用。另外增加了两个不会改变结果的提前返回:
- 两字符文本没有三元组,计算完二元组后直接返回;
- 如果两篇文本没有公共二元组,则不可能存在公共三元组,直接返回 0.0。
本次性能分析与优化实际花费:【60】分钟。
4.3 优化后的结果
使用相同输入和循环次数再次运行性能脚本:
总耗时:2.024 秒
函数调用次数:2,333 次
_character_ngrams 累计耗时:1.714 秒
总体耗时下降约:
(3.861 - 2.024) / 3.861 × 100% ≈ 47.6%
函数调用次数从约 1480 万次下降到 2333 次。运行时间会受到系统负载影响,每次结果可能略有波动,但多次运行都能观察到明显改进。优化后全部单元测试仍然通过,说明修改没有破坏原有功能。

五、单元测试与覆盖率
5.1 测试设计
项目使用 pytest 编写了 22 个单元测试,包括计算模块测试和命令行/文件接口测试。
计算模块覆盖:完全相同、标点差异、英文大小写、完全不同、局部增删改、语序变化、空文本、单字符、短文本、重复内容、相似度对称性和长文本稳定性。
文件接口覆盖:正常输入输出、参数数量错误、输入文件不存在、带 UTF-8 BOM 的文本、非法 UTF-8 文件和答案路径不可写。
代表性的算法测试如下:
def test_small_edit_has_intermediate_similarity() -> None:
score = calculate_similarity("今天是星期天天气晴", "今天是周天天气晴朗")
assert 0.3 < score < 1.0
该测试不强行指定固定小数,而是验证局部修改后的相似度应大于完全不同文本,同时小于完全相同文本。
代表性的文件输入输出测试如下:
def test_run_writes_two_decimal_places(tmp_path: Path) -> None:
original = tmp_path / "orig.txt"
suspected = tmp_path / "copy.txt"
answer = tmp_path / "answer.txt"
original.write_text("今天是星期天,天气晴。", encoding="utf-8")
suspected.write_text("今天是周天,天气晴朗。", encoding="utf-8")
assert run([str(original), str(suspected), str(answer)]) == 0
result = answer.read_text(encoding="utf-8")
assert len(result.split(".")[1]) == 2
assert 0.0 <= float(result) <= 1.0
tmp_path会为测试创建独立临时目录,测试结束后不会污染项目文件。
5.2 测试结果
执行 python -m pytest,最终结果为:
22 passed

5.3 分支覆盖率
执行以下命令统计覆盖率:
python -m coverage run -m pytest
python -m coverage report -m
python -m coverage html
最终语句与分支综合覆盖率为 96%。主要正常流程、边界条件和异常分支均已覆盖。HTML 报告保存在 htmlcov/index.html,该目录是自动生成的本地报告。

六、异常处理
异常处理集中在 main.py,目标是避免显示冗长的异常堆栈,并通过非零退出码告诉评测系统执行失败。
6.1 参数数量错误
用户没有传入三个路径或传入多余参数时,程序打印用法并返回退出码 2。
def test_run_rejects_wrong_argument_count(capsys) -> None:
assert run([]) == 2
assert "用法" in capsys.readouterr().err
6.2 输入文件不存在或无法读取
原文或待查重文件路径错误、文件被删除或没有权限时,程序输出“无法读取文件”,返回退出码 1,并且不生成答案文件。对应测试为 test_run_reports_missing_input。
6.3 文件编码错误
输入文件不是有效 UTF-8 文本时,程序捕获 UnicodeError,提示编码要求并返回退出码 1;同时兼容带 BOM 的 UTF-8 文件。对应测试为 test_read_text_rejects_invalid_utf8 和 test_read_text_accepts_utf8_bom。
6.4 答案文件无法写入
答案路径指向目录、父目录不存在或没有写入权限时,程序捕获系统错误,输出“无法写入答案文件”并返回退出码 1。对应测试为 test_write_result_reports_directory_target。

七、代码质量分析
项目使用 Ruff 进行静态检查:
python -m ruff check .
最终输出:
All checks passed!
检查内容包括语法错误、未使用的导入、导入顺序、代码格式、常见缺陷和部分 Python 最佳实践。开发过程中曾发现 main.py 的导入语句行尾存在多余空格,修正后已经消除全部警告。

八、项目结构
3124007607
├── main.py # 命令行入口和文件读写
├── plagiarism_checker.py # 查重计算模块
├── profile_checker.py # 性能分析脚本
├── requirements.txt # 测试和质量工具依赖
├── pyproject.toml # pytest、coverage、Ruff 配置
├── README.md # 项目运行说明
├── PSP.md # PSP 记录
├── samples # 10 组基础查重样例
└── tests
├── test_main.py
└── test_plagiarism_checker.py
程序实际运行只依赖 Python 标准库。pytest、coverage 和 ruff 只用于开发、测试和质量检查。
九、总结与改进计划
本次作业让我实践了从需求分析、模块设计、编码、测试、质量检查到性能优化的个人开发流程。相比只关注程序能否运行,单元测试和覆盖率帮助我验证边界情况,静态检查帮助我发现代码质量问题,性能分析则让我能够根据数据定位瓶颈,而不是凭感觉修改代码。
字符级 n-gram 的优点是不依赖中文分词工具、环境简单且运行稳定,并能识别局部增删改;不足是主要比较字符连续关系,对同义词替换和大幅度语序调整的识别能力有限。后续可以研究中文分词、TF-IDF、局部敏感哈希或语义向量,并比较准确率、速度和内存占用。
本次实际耗时为 420 分钟,比预估的 380 分钟多 40 分钟。今后制定计划时,需要为异常处理、测试设计、性能验证和文档整理预留更多时间。

浙公网安备 33010602011771号