第二周
第一次个人编程作业——论文查重
GitHub 链接:https://github.com/t2506290-bot/paper-check-homework
一、PSP 表格
预估耗时
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 10 |
| Development | 开发 | 120 |
| · Analysis | · 需求分析 (包括学习新技术) | 20 |
| · Design Spec | · 生成设计文档 | 10 |
| · Design Review | · 设计复审 | 10 |
| · Coding Standard | · 代码规范 | 10 |
| · Design | · 具体设计 | 15 |
| · Coding | · 具体编码 | 40 |
| · Code Review | · 代码复审 | 10 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 20 |
| Reporting | 报告 | 30 |
| · Test Report | · 测试报告 | 10 |
| · Size Measurement | · 计算工作量 | 5 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 |
| 合计 | 160 |
实际耗时
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| Planning | 计划 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 10 |
| Development | 开发 | 150 |
| · Analysis | · 需求分析 (包括学习新技术) | 25 |
| · Design Spec | · 生成设计文档 | 10 |
| · Design Review | · 设计复审 | 10 |
| · Coding Standard | · 代码规范 | 10 |
| · Design | · 具体设计 | 20 |
| · Coding | · 具体编码 | 50 |
| · Code Review | · 代码复审 | 15 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 |
| Reporting | 报告 | 35 |
| · Test Report | · 测试报告 | 10 |
| · Size Measurement | · 计算工作量 | 5 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 |
| 合计 | 195 |
二、计算模块接口的设计与实现过程
1. 代码组织
程序使用 Python3 实现,入口文件为 main.py,其中包含以下函数:
| 函数名 | 功能 |
|---|---|
read_file(path) |
读取文件内容,忽略无法解码的字符 |
clean_text(text) |
只保留中文、英文、数字,去掉标点、空格、换行 |
get_ngrams(text, n=2) |
把文本切成连续的 n 个字符片段 |
calc_similarity(text1, text2) |
计算两个文本的余弦相似度 |
main() |
解析命令行参数,读取文件,计算相似度,写入答案文件 |
2. 函数之间关系
main()负责整个流程控制。main()调用read_file()读取原文和抄袭版论文。main()调用clean_text()清洗文本。main()调用calc_similarity()计算相似度。calc_similarity()内部调用get_ngrams()把文本转成 2-gram。- 最后
main()把结果写入答案文件,保留两位小数。
3. 算法关键
算法核心是字符 2-gram + 余弦相似度。
步骤如下:
- 读取原文和抄袭版论文。
- 清洗文本,只保留中文、英文、数字。
- 把清洗后的文本切成连续的 2 个字符片段,例如:
今天是星期天→今天、天是、是星、星期、期天
- 统计两个文本的 2-gram 词频。
- 把词频看成向量,计算余弦相似度。
- 输出结果,保留两位小数。
4. 独到之处
- 不依赖任何第三方库,只用 Python 标准库,运行环境简单。
- 使用字符 2-gram,对中文短文本比较友好。
- 对空文本、短文本做了边界处理,避免除零错误。
- 使用
Counter统计词频,代码简洁,性能较好。
三、计算模块接口部分的性能改进
1. 性能瓶颈
主要耗时在:
- 文本清洗:正则替换。
- 2-gram 生成:遍历整个文本。
- 词频统计:遍历所有 2-gram。
- 余弦相似度计算:遍历词频向量。
2. 改进思路
- 使用
collections.Counter代替手动字典计数,减少代码量,提高速度。 - 只清洗一次文本,避免重复清洗。
- 使用局部变量保存中间结果,减少重复计算。
- 输出时只写答案文件,不打印多余内容。
3. 性能分析图
使用 Python 自带的 cProfile 进行性能分析:
python -m cProfile -s time main.py orig.txt orig_add.txt answer.txt




从分析结果看,程序总耗时约 0.004 秒,耗时最大的函数是 main 和正则编译相关函数,整体性能良好。
四、计算模块部分单元测试展示
1. 单元测试代码
import unittest
from main import clean_text, get_ngrams, calc_similarity
class TestPaperCheck(unittest.TestCase):
def test_clean_text(self):
self.assertEqual(clean_text("你好,世界!"), "你好世界")
def test_get_ngrams_empty(self):
self.assertEqual(get_ngrams(""), [])
def test_get_ngrams_short(self):
self.assertEqual(get_ngrams("你"), ["你"])
def test_get_ngrams_normal(self):
self.assertEqual(get_ngrams("你好"), ["你好"])
def test_similarity_same(self):
score = calc_similarity("今天是星期天", "今天是星期天")
self.assertAlmostEqual(score, 1.0, places=2)
def test_similarity_different(self):
score = calc_similarity("今天是星期天", "苹果香蕉梨")
self.assertLess(score, 0.5)
def test_similarity_empty(self):
self.assertEqual(calc_similarity("", "你好"), 0.0)
def test_similarity_partial(self):
score = calc_similarity("今天是星期天,天气晴", "今天是周天,天气晴朗")
self.assertGreater(score, 0.0)
self.assertLessEqual(score, 1.0)
def test_similarity_symmetric(self):
a = "今天天气很好"
b = "今天天气不错"
self.assertAlmostEqual(
calc_similarity(a, b),
calc_similarity(b, a),
places=6
)
def test_output_two_decimal(self):
score = calc_similarity("abcdef", "abcdef")
self.assertEqual(f"{score:.2f}", "1.00")
if __name__ == "__main__":
unittest.main()
2. 测试函数说明
| 测试函数 | 测试内容 |
|---|---|
test_clean_text |
清洗文本是否正确 |
test_get_ngrams_empty |
空文本是否返回空列表 |
test_get_ngrams_short |
短文本是否返回自身 |
test_get_ngrams_normal |
正常文本是否切分成 2-gram |
test_similarity_same |
相同文本相似度是否为 1 |
test_similarity_different |
不同文本相似度是否较低 |
test_similarity_empty |
空文本相似度是否为 0 |
test_similarity_partial |
部分相似文本相似度是否在 0 到 1 之间 |
test_similarity_symmetric |
相似度是否满足对称性 |
test_output_two_decimal |
输出是否保留两位小数 |
3. 测试覆盖率
使用 coverage 查看覆盖率:
coverage run -m unittest test_paper.py
coverage report

覆盖率结果:
Name Stmts Miss Cover
----------------------------------
main.py 53 24 55%
test_paper.py 32 1 97%
----------------------------------
TOTAL 85 25 71%
五、计算模块部分异常处理说明
| 异常 | 场景 | 设计目标 |
|---|---|---|
FileNotFoundError |
输入文件路径不存在 | 提示文件不存在,避免程序崩溃 |
PermissionError |
文件无权限读取或写入 | 提示权限错误 |
UnicodeDecodeError |
文件编码异常 | 使用 errors="ignore" 容错 |
IndexError / sys.argv 长度不对 |
命令行参数不足 | 提示正确用法 |
ZeroDivisionError |
空文本导致模长为 0 | 在余弦相似度中判断并返回 0.0 |
Exception |
其他未知异常 | 统一捕获,避免异常退出 |
异常测试样例
def test_file_not_found(self):
with self.assertRaises(FileNotFoundError):
open("not_exist_file_12345.txt", "r", encoding="utf-8")
对应场景:当输入文件不存在时,程序应提示错误,而不是直接崩溃。
六、实际 PSP 时间
见本文第一节“实际耗时”表格。
七、总结
本次作业完成了论文查重程序的基本功能:
- 从命令行读取原文、抄袭版论文、答案文件路径。
- 计算两篇论文的相似度。
- 输出保留两位小数的浮点数。
- 编写了 10 个单元测试,全部通过。
- 使用 GitHub 管理源代码,至少提交两次。
- 对异常情况做了处理,避免程序异常退出。
通过本次作业,我熟悉了 Python 文件读写、命令行参数、余弦相似度算法、单元测试和 GitHub 使用流程。

浙公网安备 33010602011771号