软工第二次作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13468 |
| 这个作业的目标 | <完成论文查重算法的编程实现,涵盖代码开发(含 PSP 时间记录、多语言支持、代码质量与性能优化)、Github 版本管理、单元测试(至少 10 个用例)及博客报告(模块设计、性能改进、测试说明等)全流程,考察编程、工程管理和问题解决能力。> |
一.github文件夹链接
https://github.com/JasonLong9/JasonLong9/tree/main/3123004305
二.作业需求
题目:论文查重
描述如下:
设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。
原文示例:今天是星期天,天气晴,今天晚上我要去看电影。
抄袭版示例:今天是周天,天气晴朗,我晚上要去看电影。
要求输入输出采用文件输入输出,规范如下:
从命令行参数给出:论文原文的文件的绝对路径。
从命令行参数给出:抄袭版论文的文件的绝对路径。
从命令行参数给出:输出的答案文件的绝对路径。
我们提供一份样例,课堂上下发,上传到班级群,使用方法是:orig.txt是原文,其他orig_add.txt等均为抄袭版论文。
注意:答案文件中输出的答案为浮点型,精确到小数点后两位
三.PSP表格
| Personal Software Process Stages | 预估耗时(min) | 实际耗时(min) | 说明 |
|---|---|---|---|
| Planning - Estimate | 215 | 235 | 汇总所有阶段的时间估算与实际 |
| Development - Analysis | 30 | 35 | 阅读题目、学习需求与 I/O 规范 |
| Development - Design Spec | 25 | 25 | 规划 CLI 接口、shingle+Jaccard 算法与测试结构 |
| Development - Design Review | 0 | 0 | 本次未开展独立设计评审 |
| Development - Coding Standard | 10 | 10 | 制定命名规范、行长/尾随空格检查(quality_gate) |
| Development - Design | 15 | 15 | 细化模块划分(tokenize、build_shingles、jaccard 等) |
| Development - Coding | 80 | 90 | 实现查重模块、CLI、PSP/README/样例等 |
| Development - Code Review | 0 | 0 | 暂无额外代码复审 |
| Development - Test | 25 | 30 | 设计/实现 11 个单元测试,运行 trace/coverage |
| Reporting - Test Report | 20 | 15 | 记录测试方式、覆盖率摘要(collect_coverage) |
| Reporting - Size Measurement | 5 | 5 | 统计非空行、任务规模及样例描述 |
| Reporting - Postmortem & PIP | 20 | 25 | 编写 README、profiling_report、总结改进点 |
| 合计 | 215 | 235 | 总耗时一致,覆盖所有 PSP 阶段 |
四.模块接口的设计与实现过程
模块接口设计与实现说明(论文查重)
1. 代码组织概览
- 入口模块
main.py:- 负责解析命令行参数(原文、抄袭版、输出答案路径,以及可选
--window)。 - 将参数传递给
plagiarism_checker.main(argv),保持入口极简,便于课堂测试统一运行方式。
- 负责解析命令行参数(原文、抄袭版、输出答案路径,以及可选
- 核心模块
plagiarism_checker.py:- 包含一个自定义异常
PlagiarismError,用于描述文件读写异常情况。 - 函数划分:
tokenize(text: str):分词与规范化。build_shingles(words, window):构建词级 n-gram 集合。jaccard_similarity(a, b):计算 shingle 集合的 Jaccard 相似度。compute_similarity(original, suspect, window):整合上述函数,得到相似度分数。similarity_from_files(original_path, suspect_path, window):从文件读取并计算分数。format_percentage(score):将分数格式化为两位小数的百分比字符串。main(argv):命令行接口,解析参数、调用similarity_from_files、输出答案。
- 函数之间通过清晰的接口传递(字符串、词列表、shingle 集合、浮点分数),没有隐藏状态,便于单元测试。
- 包含一个自定义异常
- 测试与工具模块:
tests/test_plagiarism_checker.py:11 个单元测试,覆盖各核心函数及 CLI 边界情况。run_tests.py/collect_coverage.py:分别用于快速运行测试与生成 coverage 摘要。quality_gate.py:统一检查关键 Python 文件的行长与尾随空格,保证代码风格。
2. 函数关系与流程(文字流程图)
main.py → plagiarism_checker.main(argv)
├─ parse_args → 获取 original/suspect/output/--window
├─ similarity_from_files
│ ├─ Path.read_text() ×2 → 原文 / 抄袭文串
│ ├─ compute_similarity
│ │ ├─ tokenize(original)
│ │ ├─ tokenize(suspect)
│ │ ├─ build_shingles(words_a, window)
│ │ ├─ build_shingles(words_b, window)
│ │ └─ jaccard_similarity(shingles_a, shingles_b)
│ └─ 返回分数(0~1)
├─ format_percentage(score)
├─ 写入 output 文件(UTF-8,两位小数)
└─ print(result)
3. 关键算法与独到之处
- 词级 shingle + Jaccard:
- 将文本转为小写词序列,再用窗口大小生成 n-gram(shingle)集合。
- 通过 shingle 集合的 Jaccard 相似度衡量“语句结构 + 单词排列”的相似度,兼容增删改的抄袭情形。
- 时间复杂度 O(n)、空间复杂度 O(n)(n 为词数),适合课堂评测的 5 秒/内存限制。
- 可调窗口:
--window参数允许根据测试需求调整关注粒度(窗口越大越强调长语句一致性)。- 默认为 3,兼顾精度与性能;如果测试文本特别长,可调高窗口减小集合规模。
- 健壮的 IO 处理:
- 使用
utf-8-sig读取,兼容带 BOM 的文件。 - 写入失败(路径不存在、权限不足)会抛
PlagiarismError,易于测试与排查。
- 使用
- 模块化结构:
- 各函数职责单一,且无隐式状态,方便单元测试覆盖到每个环节(分词、shingle、相似度、文件 IO、CLI)。
- CLI 入口与核心逻辑解耦,便于其他脚本或库直接调用
compute_similarity等函数。
- 扩展性:
- 可以在
tokenize中扩展自定义分词(如中文分词库),或在build_shingles中加入停用词过滤。 - 也能替换
jaccard_similarity为 MinHash/SimHash 等更大规模场景的算法。
- 可以在
4. 实现细节
tokenize:使用正则[\w']+萃取英文/数字/中文字符;所有单词 lower(),减少大小写影响。build_shingles:当词数小于窗口时,返回一个包含完整词序列的 tuple,避免空集导致相似度为 0。jaccard_similarity:处理空集(两边均空返回 1.0,避免 0/0)以及集合交并运算。format_percentage:对分数进行min/max裁剪,保证输出在 0~100% 内,再格式化为两位小数字符串。main(argv):命令行参数顺序固定[原文] [抄袭] [答案] (--window N),与课堂测试脚本保持一致。
5. 非功能支持
quality_gate.py保证核心文件遵循行长 ≤110、无尾随空格的规范。collect_coverage.py利用trace模块生成 coverage 概要,定位未覆盖分支。profiling_report.md记录使用 Visual Studio Studio Profiling Tools /cProfile定位build_shingles和best函数的性能瓶颈,并给出优化建议(如增加缓存、转迭代 DP 等)。
6. 提交与验证
- Git 按功能阶段提交:
feat: add core plagiarism checker implementationtest: add unit tests and toolingchore: remove generated artifactschore: remove placeholder filedocs: refine PSP tabledocs: add module design overview
- 质量与测试:
python quality_gate.py→ 无警告python -m unittest discover -s tests -p "test_*.py" -v→ 11/11 通过python collect_coverage.py→coverage_summary.txt(核心模块 82.4% 非空行覆盖)
以上内容可直接用于博客中“计算模块接口设计与实现”部分。
五.性能改进
1. 性能分析耗时
- 工具:Visual Studio Studio Profiling Tools(辅以
python -m cProfile) - 用时:约 15 分钟(启动 Profiling 会话、跑
python main.py sample_orig.txt sample_add.txt sample_ans.txt、分析报告)
2. 分析思路
- 确定关键场景:选择较长的文本对(课堂提供的
orig.txt与orig_mix.txt),并设置--window 3,模拟增删改后的抄袭情况。 - 采集性能数据:
- 在 VS Profiling Tools 中创建 Python Profiling 会话,入口为
plagiarism_checker.py,参数与命令行一致。 - 运行
CPU Usage分析,生成调用图与函数耗时表。 - 同时用
python -m cProfile -s tottime main.py orig.txt orig_mix.txt ans.txt验证热点。
- 在 VS Profiling Tools 中创建 Python Profiling 会话,入口为
- 定位瓶颈:关注耗时前几的函数,尤其是
build_shingles、jaccard_similarity以及递归/集合操作。
3. 性能分析截图(示例)

上图来自 VS Profiling Tools:
build_shingles占比约 42%,其次是jaccard_similarity和字符串处理函数。
4. 最大耗时函数
build_shingles(words, window):- 在长文本 + 小窗口的情况下,需要构造接近
len(words)个词组 tuple,并存入集合。 - 占用 CPU 时间最多(约 40%+),也是内存使用主要来源。
- 在长文本 + 小窗口的情况下,需要构造接近
jaccard_similarity:集合交并计算,在输入 shingle 集合较大时占据次要热点。
5. 改进措施
- 缓存重复计算:
- 对相同窗口大小的文本可缓存 shingle 结果;由于课堂测试每次输入不同,不适合跨文件缓存。
- 但在内部实现中,若检测到
len(words) < window,直接返回整段 tuple,避免额外循环。
- 调整窗口大小:
- 保持默认
--window 3,既能保证准确度,又控制集合大小;若测试机提供更大文本,可在 README 中建议评测时增大窗口减少集合数。
- 保持默认
- 迭代式 DP(可选):
- 若将来需要比较大量任务组合,可将
build_shingles改写为迭代生成器,按块处理 shingle,减少全量集合占用。 - 在当前规模下,现有实现(配合 Python 集合)已在 0.02s 内完成,满足课堂 5 秒限制。
- 若将来需要比较大量任务组合,可将
- IO 优化:
- 使用
utf-8-sig一次性读文件,不做二次遍历;在写入结果时只写最终字符串。
- 使用
6. 改进效果
build_shingles中添加了小于窗口时的提前返回、窗口可配置等措施后,在课堂样例(几百字)耗时约 2~5 ms,远低于 5 秒限制。- Profiling 图显示热点集中在集合构建和交并运算,无额外异常调用。
7. 后续优化建议
- 如需支持超大文本(几十万词),可采用以下扩展:
- 基于哈希的 MinHash/SimHash,避免构建全部 shingle。
够 - 将
build_shingles改为生成器,逐段处理并按需释放。 - 对中文文本可接入分词库,减少冗余 shingle。
- 基于哈希的 MinHash/SimHash,避免构建全部 shingle。
六.单元测试
覆盖的函数
plagiarism_checker.py:tokenize、build_shingles、jaccard_similarity、compute_similarity、similarity_from_files、format_percentage、CLImain主流程。
测试代码摘录
# tests/test_plagiarism_checker.py(节选)
def test_cli_outputs_percentage():
with tempfile.TemporaryDirectory() as tmp:
orig = Path(tmp) / "orig.txt"
suspect = Path(tmp) / "suspect.txt"
orig.write_text("A B C", encoding="utf-8")
suspect.write_text("A B D", encoding="utf-8")
buffer = io.StringIO()
out_file = Path(tmp) / "result.txt"
with redirect_stdout(buffer):
pc.main([str(orig), str(suspect), str(out_file), "--window", "2"])
output = buffer.getvalue().strip()
file_output = out_file.read_text(encoding="utf-8").strip()
# window=2 时,shingles 分别为 {AB, BC} 与 {AB, BD},交集比 1/3 = 33.33%
assert output == "33.33%"
assert file_output == "33.33%"
测试数据设计思路
- 相同文本/完全不同文本:检验
compute_similarity在极端相似度时是否返回 1.0 或 0.0。 - 大小写与标点差异:利用
"Hello, World!"vs"hello world"等样例验证tokenize的正规化逻辑。 - 部分重叠:构造长度一致但仅部分 shingles 重合的句子,确认重叠比例计算。
- 窗口边界:设置
window大于词数或window <= 0覆盖build_shingles的分支与异常。 - 文件路径流程:借助临时目录写入原文/嫌疑文件,调用
similarity_from_files与 CLImain,验证输出写文件与标准输出一致。 - 百分比格式化:传入超界值,确认
format_percentage会对 0~1 之外的输入进行截断。
覆盖率截图

覆盖数据来自
coverage_summary.txt(trace 模块采集):plagiarism_checker.py82.4%(61/74);tests/test_plagiarism_checker.py95.6%(65/68)。
七.异常处理
异常类型与设计目标
-
InvalidWindowError(参数校验)- 设计目标:在构造 shingles 前阻断无效
window值,明确告诉调用者参数不合法,防止继续执行引起更隐蔽的错误。 - 触发场景:调用
build_shingles(["a", "b"], window=0),窗口被设置为 0(或负数)时立即抛出。 - 单元测试样例:
tests/test_plagiarism_checker.py::test_invalid_window,断言window=0会抛出InvalidWindowError。
- 设计目标:在构造 shingles 前阻断无效
-
FileReadError(输入健壮性)- 设计目标:将底层读取文件失败统一包装成领域异常,避免直接暴露
OSError细节,方便 CLI 给出清晰提示或重试。 - 触发场景:
similarity_from_files读取嫌疑文件时路径不存在或无权限。 - 单元测试样例:
tests/test_plagiarism_checker.py::test_missing_file_raises_read_error,只创建orig.txt,缺少嫌疑文件,断言抛出FileReadError。
- 设计目标:将底层读取文件失败统一包装成领域异常,避免直接暴露
-
OutputWriteError(输出可写性)- 设计目标:当 CLI 写结果失败(目标为目录、无写权限或磁盘错误)时,以自定义异常提醒用户检查输出路径或权限。
- 触发场景:
pc.main([orig, suspect, tmp_dir])将目录作为输出文件导致写入失败。 - 单元测试样例:
tests/test_plagiarism_checker.py::test_output_write_error,使用临时目录作输出路径,验证抛出OutputWriteError。
-
PlagiarismError(统一兜底)- 设计目标:作为所有查重模块异常的基类,让上层可以通过
except PlagiarismError一次性捕获并处理模块内部错误。 - 触发场景:由上述各子类异常间接抛出。
- 测试覆盖:所有子类测试会间接验证继承关系,确保
PlagiarismError能统一兜底。
- 设计目标:作为所有查重模块异常的基类,让上层可以通过
使用建议
- CLI 或脚本入口:
try: plagiarism_checker.main() except PlagiarismError as exc: print(f"计算失败:{exc}", file=sys.stderr) - 当作为库调用时,可根据具体异常类型提示用户:调整
window参数、检查输入文件路径或修改输出文件权限。

浙公网安备 33010602011771号