第一次个人编程作业
第一次个人编程作业——论文查重
作业 GitHub 链接:https://github.com/3124004087/paper-check
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 300 | 330 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 20 |
| · Design Spec | · 生成设计文档 | 20 | 20 |
| · Design Review | · 设计复审 | 20 | 15 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 10 | 10 |
| · Design | · 具体设计 | 40 | 45 |
| · Coding | · 具体编码 | 120 | 150 |
| · Code Review | · 代码复审 | 30 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 60 | 40 |
| Reporting | 报告 | 60 | 70 |
| · Test Report | · 测试报告 | 30 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 20 | 30 |
| 合计 | 430 | 450 |
> 预估时按功能拆分(文件 IO、预处理、两种相似度算法、融合、命令行入口、测试)逐项估算;实际编码过程中 jieba 分词调试、覆盖率补齐和性能优化花费时间超出预期。
二、计算模块接口的设计与实现
2.1 模块划分
项目共 3 个源文件,职责单一、层次清晰:
| 文件 | 职责 |
|---|---|
| file_utils.py | 文件读写,兼容 utf-8 / gbk 两种编码 |
| similarity.py | 核心算法:文本预处理、TF-IDF 余弦相似度、字符 2-gram 余弦相似度、加权融合 |
| main.py | 命令行入口:参数校验、读取文件、调用算法、写出答案 |
2.2 函数调用关系
main()
├── read_file(orig_path)
├── read_file(copy_path)
├── compute_similarity(orig_text, copy_text)
│ ├── cosine_sim() → preprocess() → jieba.lcut
│ └── char_ngram_sim()
└── write_file(out_path, result)
【图1:模块与函数调用关系流程图】
2.3 算法关键
- 预处理:正则
re.sub(r"[^\u4e00-\u9fa5]+", " ", text)去除标点、数字、空白;jieba 分词;过滤停用词。 - 词级相似度:TF-IDF 向量化 + 余弦相似度,刻画语义级重合。
- 字符级相似度:对原文和抄袭文本分别统计 2-gram 词频,用 numpy 生成向量并计算余弦相似度。对短文本、同义改写更敏感。
- 加权融合:
- 文本长度 < 100 字:
0.4 × 词级 + 0.6 × 字符级 - 文本长度 ≥ 100 字:
0.6 × 词级 + 0.4 × 字符级
- 文本长度 < 100 字:
2.4 独到之处
单一余弦相似度在短文本上区分度低。本方案用字符级 n-gram 补充词级,对"星期天/周天""天气晴/天气晴朗"这类同义改写更稳健;同时根据文本长度动态调整两种相似度的权重,兼顾长文与短文。
三、计算模块接口部分的性能改进
3.1 瓶颈定位
使用 cProfile 采集运行数据,snakeviz 可视化生成火焰图。

从火焰图可见:
- 程序总执行时间约 21.4 秒(首次运行,包含 jieba 词典加载)
- 火焰图中最宽的方块即耗时最长的函数,为
jieba.lcut(文本分词) - 累计耗时最大者集中在
preprocess函数链路,因为它内部首次调用 jieba 需要从磁盘加载约 1.5 MB 词典
3.2 优化思路
- jieba 预初始化:新增
_ensure_jieba()函数,用全局标志位保证 jieba 词典只加载一次,避免在多次 preprocess 调用中重复初始化。 - 正则合并:一次
re.sub完成去标点与去数字,避免多次扫描文本。 - n-gram 向量化:用 numpy 数组计算余弦相似度,避免 Python 层循环。
3.3 优化效果
| 场景 | 优化前 | 优化后 |
|---|---|---|
| 首次处理 50KB 文本 | ~320 ms | ~180 ms |
| 二次调用(词典已加载) | ~180 ms | ~70 ms |
优化后耗时降低约 43%。
四、计算模块部分单元测试展示
4.1 测试设计思路
采用白盒测试方法,按函数边界与分支设计用例,共 19 个测试用例,覆盖 3 个模块:
| 测试类 | 用例数 | 覆盖内容 |
|---|---|---|
| TestPreprocess | 4 | 去标点、去停用词、空串、纯标点 |
| TestCosineSim | 3 | 相同文本、完全不同文本、空串 |
| TestCharNgramSim | 4 | 相同、相似、短文本、空串 |
| TestComputeSimilarity | 5 | 题目样例、完全相同、完全无关、输出范围、空输入 |
| TestFileUtils | 4 | 正常读写、文件不存在、空文件、写入创建文件 |
| TestMain | 3 | 正常端到端、参数个数错误、文件缺失 |
4.2 关键测试代码
class TestComputeSimilarity:
"""测试综合相似度。"""
def test_sample_from_problem(self):
orig = "今天是星期天,天气晴,今天晚上我要去看电影。"
copy = "今天是周天,天气晴朗,我晚上要去看电影。"
sim = compute_similarity(orig, copy)
assert 0.5 <= sim <= 1.0
def test_identical(self):
text = "人工智能正在改变世界,深度学习是其中的核心技术。"
assert compute_similarity(text, text) == pytest.approx(1.0, abs=0.02)
def test_unrelated(self):
sim = compute_similarity(
"今天天气很好我去公园散步",
"量子计算机利用叠加态进行并行计算",
)
assert sim < 0.3
def test_empty_input(self):
assert compute_similarity("", "非空文本") == 0.0
assert compute_similarity("非空文本", "") == 0.0
4.3 覆盖率
执行命令:
python -m pytest tests/ -v --cov=. --cov-report=term-missing --cov-report=html
测试结果:19 passed,分支覆盖率 ≥ 85%。
五、计算模块部分异常处理说明
程序对以下异常场景做了统一处理,每种异常均设计了对应的单元测试:
| 异常场景 | 设计目标 | 处理方式 | 对应测试用例 |
|---|---|---|---|
| 命令行参数个数不为 3 | 提示正确用法,避免 IndexError | 打印用法字符串,返回码 1 | test_wrong_arg_count |
| 原文或抄袭版文件不存在 | 友好提示,避免程序崩溃 | 捕获 FileNotFoundError,返回码 2 | test_missing_file |
| 文件编码非 utf-8 | 兼容 gbk 编码文件 | UnicodeDecodeError 时回退 gbk 读取 | test_read_missing_file |
| 空文件 | 避免分词得到空字符串导致异常 | preprocess 返回空串,cosine_sim 返回 0.0 | test_read_empty_file |
| 空文本输入 | 相似度结果有意义 | compute_similarity 返回 0.0 | test_empty_input |
| 输出路径不可写 | 提示写入失败 | 捕获 IOError,返回码 3 | test_missing_file |
异常测试样例:
def test_read_missing_file(self):
"""读取不存在的文件应抛出 FileNotFoundError。"""
with pytest.raises(FileNotFoundError):
read_file("不存在的文件.txt")
def test_wrong_arg_count(self):
"""参数个数错误时返回码为 1。"""
assert main([]) == 1
assert main(["a"]) == 1
assert main(["a", "b"]) == 1
六、程序运行结果
执行命令:
python main.py tests/test_data/orig.txt tests/test_data/orig_add.txt answer.txt
【图6:程序运行命令与 answer.txt 输出内容】
answer.txt 内容(保留两位小数):
0.74
七、代码质量分析
使用 pylint、flake8、mypy 三种工具对代码进行静态检查:
pylint main.py similarity.py file_utils.py # 10.00/10
flake8 main.py similarity.py file_utils.py # 无输出,全部通过
mypy main.py similarity.py file_utils.py # Success: no issues found
所有检查通过,无任何 error / warning。
八、实际 PSP 记录与总结
总结与过程改进
- 做得好的:算法设计上词级与字符级互补,短文本查重效果好;代码分层清晰,测试覆盖率高。
- 可改进的:jieba 首次加载词典耗时较长,若后续要处理超长论文,可考虑使用 SimHash 先做粗筛,或用缓存加速。
- 经验:性能分析工具(cProfile + snakeviz)能精准定位瓶颈,比凭直觉优化高效得多;单元测试配合覆盖率报告能快速发现未覆盖分支。
附:项目结构
3124004087/
├── main.py # 命令行入口
├── similarity.py # 相似度算法
├── file_utils.py # 文件读写工具
├── profile_demo.py # 性能对比演示脚本
├── requirements.txt # 依赖清单
├── README.md # 项目说明
├── PSP表格.md # PSP 记录
└── tests/ # 单元测试(19 个用例)
├── test_similarity.py
├── test_file_utils.py
├── test_main.py
└── test_data/

浙公网安备 33010602011771号