| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 完成论文查重作业项目 |
| GitHub链接: | https://github.com/doki475/doki/tree/main/3124004449 |
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 40 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 40 |
| Development | 开发 | 300 | 400 |
| · Analysis | · 需求分析 (包括学习新技术) | 240 | 320 |
| · Design Spec | · 生成设计文档 | 60 | 50 |
| · Design Review | · 设计复审 | 30 | 30 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
| · Design | · 具体设计 | 60 | 80 |
| · Coding | · 具体编码 | 300 | 350 |
| · Code Review | · 代码复审 | 45 | 40 |
| · Test | · 测试 (自我测试, 修改代码, 提交修改) | 180 | 240 |
| Reporting | 报告 | 105 | 115 |
| · Test Report | · 测试报告 | 45 | 50 |
| · Size Measurement | · 计算工作量 | 15 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 45 | 50 |
| 合计 | 620 | 720 |
论文查重系统:计算模块设计、性能分析与测试
一、项目概述
本项目实现一个论文查重程序。程序从命令行接收三个文件的绝对路径:原文文件、抄袭版文件和答案文件,计算两篇文本的相似度,并将结果以保留两位小数的浮点数写入答案文件。
输出范围为 0.00 到 1.00。其中 1.00 表示两篇文本完全相同,0.00 表示没有检测到相似内容。
二、计算模块接口的设计与实现
2.1 模块组织
项目采用按职责划分的模块结构,没有为了使用类而强行增加类层次。计算模块主要由三个部分组成:
paper-check/
├── main.py # 命令行入口、文件输入输出、异常处理
├── checker/
│ ├── __init__.py
│ ├── algorithm.py # 相似度算法与计算接口
│ └── preprocessor.py # 文本预处理和 n-gram 提取
├── tests/
│ └── test_core.py # 单元测试
├── profile_checker.py # cProfile 性能采样脚本
└── profile_output/
├── short_text.prof # cProfile 原始数据
└── short_text_stats.txt # 函数耗时统计表
核心接口为:
calculate_similarity(text1, text2) -> float
该函数接收两段文本,返回 [0, 1] 范围内的相似度。接口不依赖文件系统,因此可以单独进行单元测试,也可以被命令行入口调用。
2.2 函数职责
| 函数 | 所属模块 | 职责 |
|---|---|---|
read_file |
main.py |
按多种编码读取文本文件 |
main |
main.py |
解析命令行参数,组织输入、计算和输出流程 |
normalize_text |
preprocessor.py |
统一标点并压缩空白字符 |
extract_char_ngrams |
preprocessor.py |
提取连续字符 n-gram 集合 |
jaccard_similarity |
algorithm.py |
计算两个集合的 Jaccard 相似度 |
char_frequency_vector |
algorithm.py |
生成字符频率向量 |
cosine_similarity |
algorithm.py |
计算两个频率向量的余弦相似度 |
_compute_word_similarity |
algorithm.py |
使用 jieba 进行词语级比较,缺少 jieba 时使用字符特征降级 |
calculate_similarity |
algorithm.py |
组合多个指标,输出最终重复率 |
2.3 模块关系
2.4 关键函数流程图
calculate_similarity 的计算流程如下:
2.5 算法关键
本项目没有只依赖单一字符串匹配,而是组合三种互补特征:
-
字符 n-gram Jaccard,相似度权重 50%
对文本提取二元组和三元组,分别计算集合的 Jaccard 相似度,再按
0.6和0.4加权。该方法能够识别连续字符片段的保留情况。ngram_similarity = 0.6 * bigram_jaccard + 0.4 * trigram_jaccard -
字符频率余弦相似度,权重 30%
将文本表示为字符频率向量,计算两个向量的夹角相似度。即使文本中有少量增删改,整体字符分布仍然相近时,该指标可以提供补充判断。
-
词语 Jaccard 相似度,权重 20%
中文文本优先使用 jieba 精确模式分词,再比较词集合。如果运行环境没有安装 jieba,则降级为字符四元组和 skip-gram 特征,保证核心功能仍然可用。
最终公式为:
$$
similarity = 0.50 \times ngram + 0.30 \times cosine + 0.20 \times word
$$
2.6 算法的独到之处
- 同时考虑连续片段、字符分布和词语层面,减少单一指标的误判。
- 对中文标点进行统一处理,避免全角标点和半角标点造成无意义差异。
- 对空文本、完全相同文本和
None输入设置了明确行为。 - jieba 不是算法唯一依赖,缺少第三方分词库时仍然有可用的降级方案。
- 计算模块与文件输入输出解耦,便于测试、性能采样和后续替换算法。
三、计算模块性能改进与性能分析
3.1 性能改进记录
本次性能改进和验证实际投入约 80 分钟,主要包括算法实现、测试、性能采样和结果分析。
| 阶段 | 实际时间 | 工作内容 |
|---|---|---|
| 初始算法实现 | 35 分钟 | 完成 n-gram、余弦相似度和词语相似度 |
| 计算流程优化 | 20 分钟 | 复用预处理结果,避免重复处理,处理边界条件 |
| 性能采样与分析 | 15 分钟 | 使用 cProfile 采样并查看 SnakeViz 图表 |
| 回归测试 | 10 分钟 | 执行全部单元测试并检查命令行流程 |
3.2 性能改进思路
- 将文本规范化集中放在
calculate_similarity开始处,后续特征提取都使用规范化后的文本。 - 使用集合计算 n-gram 的交集和并集,避免逐字符嵌套比较。
- 使用字典保存字符频率,字符统计复杂度为 $O(n)$。
- 对空文本提前返回,避免不必要的分词和向量计算。
- 性能采样时先预热一次 jieba,排除词典首次加载对计算模块本身的干扰。
- 使用
cProfile记录函数级调用次数和耗时,用pstats生成统计表,用 SnakeViz 生成交互式 Icicle 冰柱图。
3.3 性能采样方法
采样脚本为 profile_checker.py,测试数据采用题目中的单句中文文本:
原文:今天是星期天,天气晴,今天晚上我要去看电影。
抄袭版:今天是周天,天气晴朗,我晚上要去看电影。
执行命令:
python profile_checker.py
使用 SnakeViz 查看图表:
snakeviz profile_output/short_text.prof
本次实际采样结果:
similarity = 0.56
elapsed = 0.00085s
function calls = 808
说明:题目要求使用 VS 2017/JProfiler 的性能分析工具自动生成图表。本项目为 Python 项目,因此使用 Python 官方
cProfile采集数据,并使用 SnakeViz 生成等效的交互式 Icicle 冰柱图。该方式能够展示函数调用层次、调用次数和累计耗时。
SnakeViz 截图:

3.4 消耗最大的函数
按累计耗时排序时,计算模块的主要耗时集中在:
calculate_similarity:负责整体流程和三种相似度的组合。_compute_word_similarity:调用 jieba 进行中文分词。jieba.cut和jieba.__cut_DAG:完成中文词语切分。cosine_similarity:生成并计算字符频率向量。
在短文本场景下,单次调用耗时非常小,统计表中的数值会因四舍五入显示为 0.000。因此应同时参考调用次数、累计耗时和 SnakeViz 图中的面积,不能只看三位小数的显示结果。jieba 首次加载词典的耗时不属于单次查重计算,因此采样脚本先执行了一次预热调用。
四、单元测试设计与展示
4.1 测试结果
项目使用 Python unittest 自动执行测试:
python -m unittest discover tests -v
当前测试结果:
Ran 40 tests in 0.481s
OK
测试覆盖率使用 coverage.py 生成:
python -m coverage run --source=checker,main -m unittest discover tests
python -m coverage report -m
python -m coverage html -d coverage_html
当前覆盖率结果:
| 文件 | 覆盖率 |
|---|---|
checker/__init__.py |
100% |
checker/preprocessor.py |
100% |
checker/algorithm.py |
81% |
main.py |
33% |
| 总计 | 68% |

4.2 测试代码示例
以下测试验证完全相同文本:
def test_identical_texts(self):
text = "今天是星期天,天气晴,今天晚上我要去看电影。"
sim = calculate_similarity(text, text)
self.assertAlmostEqual(sim, 1.0, places=2)
以下测试验证一段文本为空的情况:
def test_one_empty_one_not(self):
sim = calculate_similarity("", "hello world")
self.assertAlmostEqual(sim, 0.0)
以下测试验证增删改后的文本仍能得到较高相似度:
def test_minor_modification(self):
original = "今天是星期天,天气晴,今天晚上我要去看电影。"
modified = "今天是周天,天气晴朗,我晚上要去看电影。"
sim = calculate_similarity(original, modified)
self.assertGreater(sim, 0.5)
4.3 测试用例设计思路
测试采用白盒测试为主,同时覆盖边界值和典型输入:
| 测试类别 | 测试内容 |
|---|---|
| 正常路径 | 完全相同文本、部分相似文本、中文混合英文文本 |
| 边界值 | 两段都为空、只有一段为空、文本短于 n-gram 长度 |
| 算法分支 | Jaccard 空集合、余弦空向量、skip-gram 降级逻辑 |
| 修改场景 | 少量修改、大量增加、大量删除、句子重排 |
| 输入格式 | 中文标点、换行、多余空白、特殊字符 |
| 文件场景 | UTF-8、GBK、UTF-16 文件,不存在的输入文件 |
| 输出场景 | 输出范围、两位小数、文件写入流程 |
测试数据不是只使用一个固定样例,而是分别构造短文本、完全不同文本、部分重叠文本和大规模增删文本,以验证不同代码分支。
4.4 测试设计评价
当前测试共 40 项,已经覆盖计算模块的主要正常路径、边界条件和降级路径。测试可以自动运行,适合在每次代码提交后执行。
仍有两个可继续提高的方向:
- 增加命令行参数数量错误、输出目录无权限等
main.py异常分支测试。 - 在 CI 中自动执行测试和覆盖率检查,避免只在本地运行。
五、异常处理设计
5.1 异常处理目标
异常处理的目标不是隐藏错误,而是让程序在错误输入下及时停止,并给出能够定位问题的错误信息,同时避免生成错误答案文件。
5.2 异常场景说明
| 异常场景 | 处理方式 | 设计目标 |
|---|---|---|
| 命令行参数不是 3 个 | 输出使用方法并返回非零状态码 | 防止输入文件和输出文件位置错位 |
| 输入文件不存在 | 捕获 FileNotFoundError |
明确告诉用户哪个路径不可用 |
| 文件编码无法识别 | 抛出 ValueError |
防止乱码文本进入算法 |
| 文件读取失败 | 捕获 IOError |
处理权限、磁盘和文件系统错误 |
| 算法运行异常 | 捕获异常并输出 traceback | 保留错误上下文,方便调试 |
| 输出文件写入失败 | 捕获 IOError |
防止用户误以为答案已经成功生成 |
None 文本输入 |
在计算接口中转为空字符串 | 保证计算函数接口稳定 |
| 空文本输入 | 两空返回 1.0,一空返回 0.0 |
明确边界语义,避免除零错误 |
5.3 异常测试样例
文件不存在:
def test_read_file_not_found(self):
from main import read_file
with self.assertRaises(FileNotFoundError):
read_file("/nonexistent/file.txt")
对应场景:用户传入了错误的原文路径或抄袭版路径,程序应在读取阶段停止,而不是继续计算。
空输入:
def test_none_inputs(self):
self.assertAlmostEqual(calculate_similarity(None, None), 1.0)
self.assertAlmostEqual(calculate_similarity(None, "hello"), 0.0)
对应场景:上层调用者没有提供文本对象时,计算模块仍返回确定结果,不发生异常退出。
编码处理:
def test_read_file_gbk(self):
content = read_file(gbk_file_path)
self.assertEqual(content, "GBK测试")
对应场景:Windows 环境下文本文件可能使用 GBK 编码,程序需要能够读取常见中文编码。
六、GitHub 源代码管理
本项目按功能划分提交:
-
基本功能提交
- 完成命令行参数处理。
- 完成文件读取和答案文件输出。
- 完成基础相似度算法。
- 编译/运行和基础测试通过后提交。
-
扩展功能提交
- 增加中文分词和多指标加权。
- 增加编码兼容、异常处理和边界测试。
- 增加性能采样脚本和覆盖率报告。
- 回归测试通过后提交。
七、总结
本项目的计算模块使用函数式接口组织,核心入口清晰,算法由字符片段、字符频率和词语三个层次组成。通过 40 项自动化测试验证了主要功能和异常分支,并使用 coverage.py 得到 68% 的总体覆盖率。性能方面使用 cProfile 和 SnakeViz 完成了函数级采样,短文本单次计算耗时约 0.00085s,满足题目对运行时间和资源占用的要求。
后续可以继续增加命令行异常分支测试、CI 自动测试,以及基于更大文本集的准确率对比实验。
浙公网安备 33010602011771号