这个作业属于哪个课程 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.001.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 模块关系

flowchart TD A[命令行参数] --> B[main.main] B --> C[read_file] C --> D[原文字符串] C --> E[抄袭文字符串] D --> F[calculate_similarity] E --> F F --> G[normalize_text] G --> H[字符 n-gram Jaccard] G --> I[字符频率余弦相似度] G --> J[词语 Jaccard 相似度] H --> K[加权组合] I --> K J --> K K --> L[限制到 0.00~1.00] L --> M[写入答案文件]

2.4 关键函数流程图

calculate_similarity 的计算流程如下:

flowchart TD A[接收两段文本] --> B{是否为 None} B -->|是| C[转换为空字符串] B -->|否| D[保留原文本] C --> E[统一标点和空白] D --> E E --> F{两段文本是否都为空} F -->|是| G[返回 1.0] F -->|否| H{是否有一段为空} H -->|是| I[返回 0.0] H -->|否| J[提取二元组和三元组] J --> K[计算 n-gram Jaccard] E --> L[统计字符频率] L --> M[计算余弦相似度] E --> N[分词并计算词语 Jaccard] K --> O[0.50*n-gram] M --> P[0.30*cosine] N --> Q[0.20*word] O --> R[加权求和] P --> R Q --> R R --> S[限制结果范围并返回]

2.5 算法关键

本项目没有只依赖单一字符串匹配,而是组合三种互补特征:

  1. 字符 n-gram Jaccard,相似度权重 50%

    对文本提取二元组和三元组,分别计算集合的 Jaccard 相似度,再按 0.60.4 加权。该方法能够识别连续字符片段的保留情况。

    ngram_similarity = 0.6 * bigram_jaccard + 0.4 * trigram_jaccard
    
  2. 字符频率余弦相似度,权重 30%

    将文本表示为字符频率向量,计算两个向量的夹角相似度。即使文本中有少量增删改,整体字符分布仍然相近时,该指标可以提供补充判断。

  3. 词语 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 性能改进思路

  1. 将文本规范化集中放在 calculate_similarity 开始处,后续特征提取都使用规范化后的文本。
  2. 使用集合计算 n-gram 的交集和并集,避免逐字符嵌套比较。
  3. 使用字典保存字符频率,字符统计复杂度为 $O(n)$。
  4. 对空文本提前返回,避免不必要的分词和向量计算。
  5. 性能采样时先预热一次 jieba,排除词典首次加载对计算模块本身的干扰。
  6. 使用 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 截图:

屏幕截图 2026-09-14 221309

3.4 消耗最大的函数

按累计耗时排序时,计算模块的主要耗时集中在:

  • calculate_similarity:负责整体流程和三种相似度的组合。
  • _compute_word_similarity:调用 jieba 进行中文分词。
  • jieba.cutjieba.__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%

屏幕截图 2026-09-14 214817

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 项,已经覆盖计算模块的主要正常路径、边界条件和降级路径。测试可以自动运行,适合在每次代码提交后执行。

仍有两个可继续提高的方向:

  1. 增加命令行参数数量错误、输出目录无权限等 main.py 异常分支测试。
  2. 在 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 源代码管理

本项目按功能划分提交:

  1. 基本功能提交

    • 完成命令行参数处理。
    • 完成文件读取和答案文件输出。
    • 完成基础相似度算法。
    • 编译/运行和基础测试通过后提交。
  2. 扩展功能提交

    • 增加中文分词和多指标加权。
    • 增加编码兼容、异常处理和边界测试。
    • 增加性能采样脚本和覆盖率报告。
    • 回归测试通过后提交。

七、总结

本项目的计算模块使用函数式接口组织,核心入口清晰,算法由字符片段、字符频率和词语三个层次组成。通过 40 项自动化测试验证了主要功能和异常分支,并使用 coverage.py 得到 68% 的总体覆盖率。性能方面使用 cProfile 和 SnakeViz 完成了函数级采样,短文本单次计算耗时约 0.00085s,满足题目对运行时间和资源占用的要求。

后续可以继续增加命令行异常分支测试、CI 自动测试,以及基于更大文本集的准确率对比实验。