第一次个人编程作业——论文查重

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.pymain.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 实现)依然有开销。

image

四、计算模块部分单元测试展示

  1. 测试代码展示
    以下是 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% 以上。
image

五、计算模块部分异常处理说明

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

posted on 2026-09-12 16:16  空白QAQ  阅读(15)  评论(0)    收藏  举报