个人项目——C++实现论文查重(简易版)

发布时间 2023-09-19 09:06:16作者: 忆长思

本次项目GitHub地址:https://github.com/Focuspresent/Paper_Review

这个作业属于哪个课程 https://edu.cnblogs.com/campus/jmu/ComputerScience21
这个作业的要求 https://edu.cnblogs.com/campus/jmu/ComputerScience21/homework/13034
这个作业的目标 完成一次编程练习

前言

题目需求

题目:论文查重

描述如下:

设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。

原文示例:今天是星期天,天气晴,今天晚上我要去看电影。
抄袭版示例:今天是周天,天气晴朗,我晚上要去看电影。
要求输入输出采用文件输入输出,规范如下:

从命令行参数给出:论文原文的文件的绝对路径。
从命令行参数给出:抄袭版论文的文件的绝对路径。
从命令行参数给出:输出的答案文件的绝对路径。
我们提供一份样例,课堂上下发,上传到班级群,使用方法是:orig.txt是原文,其他orig_add.txt等均为抄袭版论文。

注意:答案文件中输出的答案为浮点型,精确到小数点后两位

开发环境

  • 编程语言: C++11
  • 编程环境: Vscode(Vscode怎么了你)
  • 第三方库: cppjieba
  • 其他软件: cmake,mingw32-make

算法及其流程

简单的采用余弦相似度来计算文本相似度

  1. 将文件内容作为整体读入字符串
  2. 调用分词库将字符串分词,并去掉标点符号,得到两个列表
  3. 将两个列表进行去重,并做标识记录,生成哈希表
  4. 分别遍历列表,用哈希表做两次词频统计,也就是映射完后的向量
  5. 由多维向量计算余弦相似度

编程实现

接口的设计与实现

整体架构

函数说明

  • main(int,char*) -> int: 主体部分,由命令行输入参数调用,在计算并写入之前,会检测参数个数,路径以及文件内容的合法性
  • put_error(string) -> void: 错误输出,并由此退出程序
  • check_path(string&) -> bool: 路径检测,失败则调用退出函数
  • read_file(string&) -> string: 输入路径,并打开文件,将文本内容读入到字符串并返回,文件打开失败调用退出函数
  • write_file(string&,string) -> void: 输入路径,并打开文件,将后一个字符串写到文件中并换行,文件打开失败调用退出函数
  • spilt(string&) -> vector<string>: 调用分词库,将字符串分词为列表,并去掉标点符号,返回
  • count(string&, string&) -> string: 调用两次spilt,将两个列表映射成多维向量,由向量计算余弦相似度,最后将答案格式化并转为字符串

算法关键

关键在于count函数的实现,本来其实要多写几个函数,倒不如再多写几个function指针
1.调用分词函数,并声明向量

    vector<string> l1=spilt(text1),l2=spilt(text2);
    vector<int> v1,v2;

2.将两个列表进行合并,以下是合并的函数,记录大小为零会退出

    auto Union=[&](vector<string>& l){
        for(auto& n: l){
            if(us.count(n)) continue;
            um[n]=index++;
            us.insert(n);
        }
    };

3.分别遍历,将列表映射成向量,以下是转化的函数

auto ltov=[&](vector<string>& l,vector<int>& v){
        if(!index) {
            put_error("File Size Empty");
        }
        v.resize(index);
        for(auto& n: l){
            if(!us.count(n)) continue;
            v[um[n]]++;
        }
    };
  1. 用两个向量计算余弦相似度,简单的遍历以下就行,以下是计算的函数,由于涉及除法,检测分母
auto getSim=[&](vector<int>& v1, vector<int>& v2)->double{
        double ans=0,m1=0,m2=0;
        for(int i=0;i<v1.size();i++){
            ans+=v1[i]*v2[i];
            m1+=v1[i]*v1[i];
            m2+=v2[i]*v2[i];
        }
        if(!m1||!m2){
            put_error("Zero Denominator");
        }
        return ans/(sqrt(m1)*sqrt(m2));
    };
  1. 函数调用,先合并,否则记录大小为零会直接退出,再将列表转为向量,最后计算余弦相似度,并将其格式化
    Union(l1);Union(l2);
    ltov(l1,v1);ltov(l2,v2);
    //格式化
    char ans[4];
    sprintf(ans,"%3.2f",getSim(v1,v2));

性能分析

虽然没找到啥好用的插件,但是编译器自带的gprof暂且还可以用,先在编译选项上加上pg选项,然后在运行可执行文件之后,会生成out文件,然后采用以下命令

gprof target.exe gmon.out //可以在命令行中看到相关信息
gprof target.exe gmon.out > report.txt //可以将相关信息保留在目标文本中

不过麻烦的是,由于有三方库的介入,生成的文本较多

可以看出性能瓶颈除去三方库的函数,就在于spilt函数,其实也挺正常的,毕竟spilt就是调用第三方库的函数

性能改进

我觉得没啥好改的,除去第三方库及其调用的,关键的count函数也只是多次一次遍历就可以完成

单元测试

路径检测测试

void check_path_test(){
    //相对路径
    string s1=".\\test",s2="saxaxa",s3="E:\\hah\\hha.txt";
    cout<<check_path(s1)<<endl;
    //错误路径
    cout<<check_path(s2)<<endl;
    //绝对路径
    cout<<check_path(s3)<<endl;
}

打印输出

0
0
1

分割函数测试

void spilt_test(){
    //打印函数
    auto print=[&](string& s){
        for(auto& w: spilt(s)){
            cout<<w<<" ";
        }
        cout<<endl;
    };
    //
    string s1="你说的对但是原神是由米哈游自主研发的一款全新开放世界冒险游戏游戏发生在一个被称作提瓦特的幻想世界在这里被神选中的人将被授予神之眼导引元素之力你将扮演一位名为旅行者的神秘角色在自由的旅行中邂逅性格各异能力独特的同伴们和他们一起击败强敌找回失散的亲人的同时逐步发掘原神的真相";
    string s2="我认为, 识之律者女士万岁,发生了会如何,不发生又会如何。 识之律者女士万岁,发生了会如何,不发生又会如何。 要想清楚,识之律者女士万岁,到底是一种怎么样的存在。 而这些并不是完全重要,更加重要的问题是, 了解清楚识之律者女士万岁到底是一种怎么样的存在,是解决一切问题的关键。 这种事实对本人来说意义重大,相信对这个世界也是有一定意义的。 既然如此, 就我个人来说,识之律者女士万岁对我的意义,不能不说非常重大。 带着这些问题,我们来审视一下识之律者女士万岁。 要想清楚,识之律者女士万岁,到底是一种怎么样的存在。 所谓识之律者女士万岁,关键是识之律者女士万岁需要如何写。";
    print(s1);print(s2);
}

打印输出

你 说 的 对 但是 原神 是 由 米哈 游 自主 研发 的 一款 全新 开放 世界 冒险游戏 游戏 发生 在 一个 被称作 提 瓦特 的 幻想 世界 在 这里 被 神 选中 的 人 将 被 授予 神之 
眼 导引 元素 之力 你 将 扮演 一位 名为 旅行者 的 神秘 角色 在 自由 的 旅行 中 邂逅 性格 各异 能力 独特 的 同伴 们 和 他们 一起 击败 强敌 找回 失散 的 亲人 的 同时 逐
步 发掘 原神 的 真相
我 认为   识之律者 女士 万岁 发生 了 会 如何 不 发生 又 会 如何   识之律者 女士 万岁 发生 了 会 如何 不 发生 又 会 如何   要 想 清楚 识之律者 女士 万岁 到底 是 一种 
怎么样 的 存在   而 这些 并 不是 完全 重要 更加 重要 的 问题 是   了解 清楚 识之律者 女士 万岁 到底 是 一种 怎么样 的 存在 是 解决 一切 问题 的 关键   这种 事实 对  
本人 来说 意义 重大 相信 对 这个 世界 也 是 有 一定 意义 的   既然如此   就 我 个人 来说 识之律者 女士 万岁 对 我 的 意义 不能不 说 非常 重大   带 着 这些 问题 我们 
来 审视 一下 识之律者 女士 万岁   要 想 清楚 识之律者 女士 万岁 到底 是 一种 怎么样 的 存在   所谓 识之律者 女士 万岁 关键 是识 之律者 女士 万岁 需要 如何 写   

计算函数测试

void count_test(){
    string s1="生活中,若你说的对出现了,我们就不得不考虑它出现了的事实。 既然如何, 一般来说, 那么, 这种事实对本人来说意义重大,相信对这个世界也是有一定意义的。 我们都知道,只要有意义,那么就必须慎重考虑。";
    string s2="一般来说, 现在,解决你说的对的问题,是非常非常重要的。 所以, 阿卜·日·法拉兹曾经说过,学问是异常珍贵的东西,从任何源泉吸收都不可耻。我希望诸位也能好好地体会这句话。 美华纳在不经意间这样说过,勿问成功的秘诀为何,且尽全力做你应该做的事吧。带着这句话,我们还要更加慎重的审视这个问题: ";
    cout<<count(s1,s2)<<endl;
}

打印输出

0.46

主函数测试

利用_execl(...)可以调用程序来测试

int main(){
    int ch;
    cin>>ch;
    switch(ch){
        case 0:{
            //错误路径
            _execl("..\\main.exe","main_test_0","wad","xaw","../../data/target.txt",NULL);
            break;
        }
        case 1:{
            //文本为空
            _execl("..\\main.exe","main_test_1","E:\\CODE\\data\\paper.txt","E:\\CODE\\data\\copy_paper2.txt","E:\\CODE\\data\\target.txt",NULL);
            break;
        }
        case 2:{
            //正确
            _execl("..\\main.exe","main_test_2","E:\\CODE\\data\\paper.txt","E:\\CODE\\data\\copy_paper1.txt","E:\\CODE\\data\\target.txt",NULL);
            break;
        }
    }
    return EXIT_SUCCESS;
}

打印输出

$ ./main_test
0
Parameter Quantity Error
$ ./main_test
1
File Size Empty
$ ./main_test
2

异常处理

输入参数错误

    if(argc!=4){
        put_error("Parameter Quantity Error");
    }

路径出错

    if(!check_path(orig_path)||!check_path(origadd_path)||!check_path(target_path)){
        put_error("File Path Error");
    }

文本内容为空

    if(!paper.size()||!copied_paper.size()){
        put_error("File Size Empty");
    }

读取或写文件出错

    if(!fin.is_open()){
        put_error("File Open Error");
    }
    if(!fout.is_open()){
        put_error("File Open Error");
    }

去完标点符号后,文本内容为空

    if(!index) {
        put_error("File Size Empty");
    }

计算余弦时分母为零

    if(!m1||!m2){
        put_error("Zero Denominator");
    }

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 15
Estimate 估计这个任务需要多少时间 10 5
Development 开发 60 120
Analysis 需求分析(包括学习新技术) 60 30
Design Spec 生成设计文档 0 0
Design Review 设计复审 0 0
Coding Standard 代码规范(为目前的开发制定合适的规范) 0 0
Design 具体设计 40 30
Coding 具体编码 30 30
Code Review 代码复审 0 0
Test 测试 120 180
Reporting 报告 60 120
Test Repor 测试报告 30 60
Size Measurement 计算工作量 10 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 0 0
合计 450 610

写到最后

c++/java确实麻烦了点(可惜没有轮子哥),不如python的difflib库