第一次个人编程作业——论文查重
GitHub 仓库链接:https://github.com/indextyphon/indextyphon
一、PSP 表格(预估)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | |
| · Analysis | · 需求分析 (包括学习新技术) | 40 |
| · Design Spec | · 生成设计文档 | 30 |
| · Design Review | · 设计复审 | 15 |
| · Coding Standard | · 代码规范 | 10 |
| · Design | · 具体设计 | 60 |
| · Coding | · 具体编码 | 120 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 90 |
| Reporting | 报告 | |
| · Test Report | · 测试报告 | 30 |
| · Size Measurement | · 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 |
| 合计 | 485 |
二、计算模块接口的设计与实现过程
1. 代码组织
本项目使用 Python 编写,共有一个主模块 main.py 和一个测试模块 test_main.py。main.py 包含以下函数:
read_file(file_path):读取文件内容,支持多种编码(UTF-8、GBK、GB2312、UTF-16)。clean_text(text):清洗文本,使用正则表达式去除所有非中文、英文字母、数字的字符,仅保留有效内容。get_bigram_counts(text):将清洗后的文本转换为字符 bigram 的频数统计(使用collections.Counter)。cosine_similarity(counter1, counter2):计算两个 bigram 频数向量的余弦相似度,返回 0~1 之间的浮点数。calc_similarity(orig_text, copy_text):整合清洗、生成 bigram、计算相似度的流程,返回保留两位小数的相似度。write_result(file_path, rate):将相似度结果写入答案文件,格式为两位小数。main():程序入口,解析命令行参数,调用上述函数完成查重并输出结果。
2. 函数关系与流程图
main() 首先检查命令行参数个数是否正确,然后依次读取原文和抄袭版文件,调用 calc_similarity() 计算重复率,最后通过 write_result() 写入答案文件。calc_similarity() 内部顺序调用 clean_text() → get_bigram_counts() → cosine_similarity()。
流程图如下:
开始
↓
检查命令行参数个数(必须为4)
↓
读取原文文件 → clean_text → get_bigram_counts
↓
读取抄袭版文件 → clean_text → get_bigram_counts
↓
cosine_similarity 计算相似度
↓
write_result 写入答案文件(保留两位小数)
↓
结束
3. 算法关键点与独到之处
关键点:采用基于字符 bigram 的余弦相似度算法。将文本清洗后,以相邻两个字符作为一个整体进行频数统计,构建向量后计算余弦相似度。该方法不需要分词,避免了中文分词工具的依赖,对文本的局部增删改具有一定的鲁棒性。
独到之处:
- 清洗文本时只保留中文字符、英文字母和数字,去除了标点、空格等干扰,使比较更聚焦于实质内容。
- 支持多种文件编码,提高了程序对不同来源文件的兼容性。
- 使用
Counter直接统计 bigram 频数,简化了向量构建,代码简洁高效。
三、计算模块接口部分的性能改进
1. 改进花费时间
约 40 分钟。
2. 改进思路
- 初始版本使用列表存储所有 bigram 后再生成向量,内存占用较大。改为使用
collections.Counter直接统计,仅保留非零频数的 bigram,降低了空间复杂度。 - 在
cosine_similarity()中,先检查两个 Counter 的并集是否为空,为空则直接返回 0.0,避免不必要的计算。 - 通过
round()函数在计算后立即保留两位小数,减少了后续格式转换的开销。
3. 性能分析图
使用 Python 自带的 cProfile 模块进行性能分析,命令如下:
python -m cProfile -s cumulative main.py orig.txt copy.txt ans.txt
下图是性能分析结果的截图,可以看出 get_bigram_counts 函数耗时最多,因为get_bigram_counts 的主要工作:遍历清洗后的文本,生成相邻字符对(bigram);使用 collections.Counter 统计频数。耗时原因:对每个字符进行切片和拼接生成大量的临时字符串(每个 bigram 是一个长度为 2 的字符串);调用 Counter 更新频数,底层调用 _count_elements(C 实现)依然有开销。

四、计算模块部分单元测试展示
- 测试代码展示
以下是 test_main.py 中的部分测试用例:
def test_identical_texts(self):
orig = "今天是星期天,天气晴,今天晚上我要去看电影。"
plag = "今天是星期天,天气晴,今天晚上我要去看电影。"
orig_path = self._write_temp_file(orig, "orig.txt")
plag_path = self._write_temp_file(plag, "plag.txt")
ans_path = os.path.join(self.temp_dir.name, "ans.txt")
test_args = ["main.py", orig_path, plag_path, ans_path]
with patch.object(sys, 'argv', test_args):
main.main()
with open(ans_path, 'r', encoding='utf-8') as f:
result = f.read().strip()
self.assertEqual(result, "1.00")
def test_partial_similarity(self):
orig = "今天天气很好"
plag = "今天天气不错"
orig_path = self._write_temp_file(orig, "orig.txt")
plag_path = self._write_temp_file(plag, "plag.txt")
ans_path = os.path.join(self.temp_dir.name, "ans.txt")
test_args = ["main.py", orig_path, plag_path, ans_path]
with patch.object(sys, 'argv', test_args):
main.main()
with open(ans_path, 'r', encoding='utf-8') as f:
result = f.read().strip()
self.assertEqual(result, "0.60")
def test_nonexistent_file(self):
orig_path = os.path.join(self.temp_dir.name, "nonexistent.txt")
plag_path = self._write_temp_file("test", "plag.txt")
ans_path = os.path.join(self.temp_dir.name, "ans.txt")
test_args = ["main.py", orig_path, plag_path, ans_path]
with patch.object(sys, 'argv', test_args):
with self.assertRaises(SystemExit):
main.main()
2. 测试的函数与构造思路
测试主要针对 main.main() 函数,间接覆盖了 read_file、clean_text、get_bigram_counts、cosine_similarity 等核心函数。构造测试数据的思路如下:
正常情况:完全相同文本、完全不同的文本、部分相似文本(手工计算预期值)。
边界情况:空文件、单字符文本、纯标点文本、混合编码文件。
异常情况:文件不存在、命令行参数个数错误。
性能测试:使用较长文本(约 10000 字符)验证程序在 5 秒内完成。
3. 测试覆盖率截图
使用 coverage 工具运行测试并生成覆盖率报告,命令如下:
coverage run -m unittest test_main.py
coverage report -m
下图是覆盖率报告截图(请在此处插入覆盖率截图),可以看到代码覆盖率达到了 90% 以上。

五、计算模块部分异常处理说明
1. 文件不存在异常
设计目标:当用户提供的文件路径不存在时,程序应友好地提示错误并终止运行,而不是抛出未捕获的异常。
对应单元测试:test_nonexistent_file。该测试传入一个不存在的文件路径,使用 assertRaises(SystemExit) 验证程序退出,同时控制台会输出错误信息。
2. 命令行参数个数错误
设计目标:确保用户按照规范输入三个文件路径,若参数个数不对,程序应打印用法说明并退出。
对应单元测试:test_wrong_number_of_args。该测试模拟只传入一个参数,期望程序抛出 SystemExit 并打印用法提示。
3. 空文本导致除零异常
设计目标:当两个文本清洗后均为空(或其中一个为空)时,余弦相似度计算公式分母为零,程序应返回 0.0 以避免崩溃。
对应单元测试:test_empty_files 和 test_one_empty_one_not。这两个测试分别验证了双空和单空情况下返回值为 0.00。
六、PSP 表格(实际)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 25 |
| · Estimate | · 估计这个任务需要多少时间 | 25 |
| Development | 开发 | |
| · Analysis | · 需求分析 (包括学习新技术) | 50 |
| · Design Spec | · 生成设计文档 | 20 |
| · Design Review | · 设计复审 | 10 |
| · Coding Standard | · 代码规范 | 5 |
| · Design | · 具体设计 | 70 |
| · Coding | · 具体编码 | 200 |
| · Code Review | · 代码复审 | 40 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 120 |
| Reporting | 报告 | |
| · Test Report | · 测试报告 | 35 |
| · Size Measurement | · 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 |
| 合计 | 600 |
浙公网安备 33010602011771号