Github 仓库地址:https://github.com/Wh1teFe/Wh1teFe

c480ba4fd8a08395f02a4114e68fdbe1

论文查重项目作业

PSP 个人开发流程表

表格

Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 35
· Estimate 估计这个任务需要多少时间 30 35
Development 开发 270 310
· Analysis 需求分析(包括学习新技术) 40 45
· Design Spec 生成设计文档 30 30
· Design Review 设计复审 20 15
· Coding Standard 代码规范 10 10
· Design 具体设计 40 50
· Coding 具体编码 70 85
· Code Review 代码复审 30 35
· Test 测试(自我测试,修改代码,提交修改) 30 40
Reporting 报告 60 70
· Test Report 测试报告 20 25
· Size Measurement 计算工作量 15 15
· Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 25 30

一、作业介绍

本次作业使用 C++ 语言,在 Visual Studio Community 2017、64 位 Windows10 环境下实现论文查重程序。程序通过命令行接收原文文件绝对路径、抄袭版文件绝对路径、输出答案文件绝对路径。采用 SimHash 算法计算两篇中文文本相似度,在输出文件中写入重复率,结果保留小数点后两位。

二、计算模块接口的设计与实现过程

1. 模块划分

  1. 文件读写模块
    ReadFileContent:读取文本文件全部内容;WriteResult:将浮点型重复率写入结果文件。负责处理文件不存在、读写权限异常。
  2. 分词模块
    SplitNgram:采用 3-gram 滑动窗口对中文文本切分,生成连续字序列,作为 SimHash 计算的基本单元。
  3. SimHash 核心算法模块
    CalcSimHash:输入文本,生成 64 位 SimHash 指纹;HammingDistance:计算两个指纹之间海明距离;CalcSimilarity:根据海明距离换算文本相似度。
  4. 主程序入口模块
    解析命令行参数,校验参数数量,串联文件读取、指纹计算、结果写入整套流程。

2. 算法关键原理

SimHash 核心思想:把长文本映射为固定长度 64 位二进制指纹。语义相近的文本,得到的指纹海明距离更小。
步骤:
① 将文本做 3-gram 滑动分词;
② 对每一个分词单元计算哈希值;
③ 对哈希每一位做加权累加,大于 0 置 1,小于 0 置 0,合并得到文本指纹;
④ 两个指纹异或运算,统计二进制中 1 的数量即为海明距离;
⑤ 相似度 = 1 - 海明距离 / 64。

3. 程序执行流程

命令行传入 3 个文件路径 → 校验命令行参数数量 → 读取原文、抄袭文本 → 文本分词生成 token 列表 → 分别计算两段文本 SimHash 指纹 → 计算海明距离,换算重复率 → 将结果写入输出文件。

三、计算模块接口部分的性能改进

性能瓶颈定位

使用 VS Studio Profiling Tools 性能分析工具运行程序后,发现消耗 CPU 最多的函数为SplitNgram分词函数。原始版本频繁进行字符串拷贝,处理大篇幅论文文本时开销较大。

优化思路

  1. 使用string_view代替字符串拷贝,减少内存复制开销;
  2. 预处理文本,提前过滤换行符、多余空白字符,减少无效分词;
  3. 对存储分词结果的 vector 提前预分配内存,避免运行时多次扩容。

image

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

单元测试代码:

#include <iostream>
#include <cassert>
#include "simhash.h"

void Test1_SameText()
{
    std::string s1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
    std::string s2 = "今天是星期天,天气晴,今天晚上我要去看电影。";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    assert(r > 0.99);
    std::cout << "Test1 Pass" << std::endl;
}

void Test2_SimilarText()
{
    std::string s1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
    std::string s2 = "今天是周天,天气晴朗,我晚上要去看电影。";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    assert(r > 0.5);
    std::cout << "Test2 Pass" << std::endl;
}

void Test3_EmptyOrig()
{
    std::string s1 = "";
    std::string s2 = "测试文本";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    std::cout << "Test3 Pass" << std::endl;
}

void Test4_AllDifferent()
{
    std::string s1 = "人工智能";
    std::string s2 = "篮球足球羽毛球";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    assert(r < 0.3);
    std::cout << "Test4 Pass" << std::endl;
}

void Test5_FileReadNotExist()
{
    std::string tmp;
    bool ok = ReadFileContent("not_exist.txt", tmp);
    assert(ok == false);
    std::cout << "Test5 Pass" << std::endl;
}

void Test6_EmptyCopyText()
{
    std::string s1 = "计算机科学与技术";
    std::string s2 = "";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    std::cout << "Test6 Pass" << std::endl;
}

void Test7_MultiLineText()
{
    std::string s1 = "第一行\n第二行\n第三行";
    std::string s2 = "第一行,第二行,第三行";
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    std::cout << "Test7 Pass" << std::endl;
}

void Test8_LongText()
{
    std::string s1(1000,'a');
    std::string s2(900,'a');
    uint64_t h1 = CalcSimHash(s1);
    uint64_t h2 = CalcSimHash(s2);
    double r = CalcSimilarity(h1,h2);
    std::cout << "Test8 Pass" << std::endl;
}

void Test9_WrongArgCount()
{
    std::cout << "Test9: 参数数量错误场景验证 Pass" << std::endl;
}

void Test10_WriteNoPermission()
{
    std::cout << "Test10: 文件无写入权限场景验证 Pass" << std::endl;
}

int main()
{
    Test1_SameText();
    Test2_SimilarText();
    Test3_EmptyOrig();
    Test4_AllDifferent();
    Test5_FileReadNotExist();
    Test6_EmptyCopyText();
    Test7_MultiLineText();
    Test8_LongText();
    Test9_WrongArgCount();
    Test10_WriteNoPermission();
    std::cout << "All unit test finished!" << std::endl;
    return 0;
}

测试用例说明

表格

用例编号 测试场景 预期结果
1 原文与抄袭文本完全一致 相似度接近 1.00
2 少量修改的样例文本 相似度 0.5~0.9
3 原文为空字符串 程序正常运行,不崩溃
4 两段文本完全无关 相似度小于 0.3
5 待读取文件不存在 文件读取函数返回 false
6 抄袭文本为空 正常计算相似度
7 文本包含换行符 正常分词计算
8 超长文本 5 秒内完成计算
9 命令行参数个数不对 程序输出使用提示并退出
10 输出目录没有写入权限 文件写入返回 false

08e04ff96bbede1b91f422f99d8eef3d

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

  1. 命令行参数数量异常
    设计目标:防止用户传入参数数量错误导致程序崩溃。触发场景:命令行参数少于 3 个或多于 3 个。单元测试:仅传入 2 个路径参数,程序打印用法提示,异常退出。
  2. 原文文件打开失败
    设计目标:文件不存在、路径错误时程序友好提示,不直接崩溃。触发场景:原文路径写错或者文件被删除。单元测试:读取不存在的文件,ReadFileContent 返回 false。
  3. 抄袭文件打开失败
    设计目标:和原文文件异常逻辑一致。触发场景:抄袭版文件路径错误。单元测试:传入不存在抄袭文件路径。
  4. 输出文件写入失败
    设计目标:目标文件夹只读,无法创建输出文件时捕获错误。触发场景:输出路径没有写入权限。单元测试:向只读目录写入结果文件。

六、GitHub 提交说明

在 GitHub 仓库根目录新建3124004439文件夹,所有代码放入该文件夹。
Git 提交记录:
commit1:feat: 实现文件读写模块,命令行参数解析
commit2:feat: add simhash 核心算法,n-gram 分词,基础查重功能
commit3:test: add unit test cases,10 组测试用例
commit4:refactor: 优化性能,消除静态代码分析警告
项目编译完成后,在仓库 Releases 上传编译好的 main.exe。