第一次个人编程作业

个人项目作业-论文查重

这个作业属于哪个课程 双学位22级 (广东工业大学)
这个作业要求在哪里 个人项目作业-论文查重
这个作业的目标 开发一个论文查重软件,并通过Git提交到远程代码库,熟悉软件开发流程
其他参考文献 现代软件工程讲义 2 工程师的能力评估和发展
Commit message 和 Change log 编写指南
现代软件工程讲义 2 开发技术 - 单元测试 & 回归测试

链接:https://gitcode.com/m0_75104457/3122000597

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

本项目的代码结构用以下这张类图来表示:

本项目构建的jar包以com.leonard.util为根路径。核心类和接口包括:

  • App类:主程序(main方法)所在的类。
  • SimHasher类:由SimHashChecker类使用,包含SimHash算法的具体实现。
  • similarity包:包含论文查重相关的类和接口
    • analysis包:文本处理相关包
      • WordAnalyzer接口:提供统一的文本处理方法的规范,核心方法为analyze()、splitWords()。对于analyze()方法的其中一个重载,该接口提供了默认的、通用的具体实现逻辑——直接计算各个分词在文本中出现的频率(即词频)。
      • AnsjWordAnalyzer类:WordAnalyzer接口的实现类,使用ansj中文分词工具做文本处理工作。
      • HanLPWordAnalyzer类:WordAnalyzer接口的实现类,使用HanLP中文分词工具做文本处理工作。本项目生成的应用默认使用此分词器。
    • checker包:相似度分析相关包
      • Checker接口:提供统一的分析两个文本相似度的规范,核心方法为checkSimilarity()。
      • SimHashChecker类:Checker接口的实现类,采用SimHash算法来分析两个文本的相似度,其中使用到了SimHasher类。
      • CosineChecker类:Checker接口的实现类,采用余弦相似度算法来分析两个文本的相似度。
    • SimilarityChecker类:主程序使用的类,核心方法为check(),用于接收文本输入,调用相应的Checker的checkSimilarity方法,并将处理结果输出到指定文件中。
    • SimilarityCheckUtils类:论文查重工具类,封装了各类使用不同分词器、不同算法的Checker,通过方法返回给调用方。

算法关键:

  1. App类中的主程序在创建一个SimilarityChecker对象时,向其构造方法传入SimHashChecker或CosineChecker对象(指明要使用基于哪种算法的文本相似度分析器,方便更改),接着再向SimHashChecker的构造方法传入AnsjWordAnalyzer或HanLPWordAnalyzer对象(指明要使用哪种分词工具做文本处理),随后调用check()方法,向该方法传入三个参数:程序从命令行接收的原文文件、待查重论文文件、目标输出文件的路径。
  2. SimilarityChecker.check()方法读取原文文件、待查重论文文件的内容,并传给分析器的checkSimilarity()方法。分析器计算完成后,返回两个文件内容的相似度,check()方法将结果写入到答案文件中。
  3. 分析器执行文本相似度分析逻辑:以SimHashChecker为例,它会调用SimHasher的hammingDistance()方法来计算两个文本的海明距离d,取SimHash值的位数为64,公式为:相似度=1 - d/64。(这里不展开讲述SimHash算法的原理及其Java实现)

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

在改进计算模块的性能上花费了大约三个小时左右。下图分别为IntelliJ IDEA的测试运行结果以及JProfiler的性能分析结果:

性能在空间方面没有问题,主要问题在于耗费的时间。程序占用的内存大小最大为218.7MB,不超过2048MB。从运行结果可以看到,程序处理orig.txt(原文文件)的文本的耗时长达10秒,严重影响程序性能,而处理其它文件的内容耗时基本不超过100ms。程序已经使用到parallelStream来实现大文本的分块并行处理,但性能提升效果仍然不太明显。

后续的测试中程序耗时都是在6~8秒之间,但在IDE被重新打开后第一次运行程序,耗时依然长达10秒。原因有待进一步探究。

经过查阅网上资料后发现,原因在于ansj文本处理库中的ToAnalysis().parse()方法在第一次加载词典时会特别久,但使用自定义词典会对分析结果有所影响。尝试改用HanLPWordAnalyzer后,性能测试结果如下:

可以看到,速度显著提升了十几倍,运行结果也显示程序在处理orig.txt文件的内容时也仅耗时643ms。两种分词工具在程序首次运行时都会自动加载词典,但HanLP更加高效、简单、轻量级,其默认词典大小仅600MB,因此它适用于大多数中文分词场景。

这次改动在提升性能的同时,也难免对处理结果产生了影响,可以看到改动前后程序的输出结果明显不同:只讨论SimHash相似度,在对比orig.txt和orig_0.8_add.txt这两个文件内容的相似度时,使用ansj分词器的结果是0.9375,而使用HanLP分词器的结果却是0.984375。得出结论:对于需要兼顾效率和准确率的中文文本处理场景,采用HanLP较为合适;对于注重准确度的场景,可以选择采用ansj。

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

测试源码:

package com.leonard;

import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import com.leonard.util.similarity.checker.Checker;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import com.leonard.util.similarity.SimilarityCheckUtils;

import static org.junit.jupiter.api.Assertions.*;

public class CheckerTest {
  /** 项目根路径 */
  private static final String root = System.getProperty("user.dir");

  /** 测试文件的文件名前缀 */
  private static final String prefix = "orig_0.8_";

  /** 测试文件所在路径 */
  private static final String dirName = "src/test/resources";

  /** 原文文件名 */
  private static final String originalFileName = "orig.txt";

  /** 原文文件路径 */
  private static final Path originalFilePath = Paths.get(root, dirName, originalFileName);

  /**
   * 获取测试文件的文件名及路径,并提供给测试方法 test()
   *
   * @return 为 test() 方法提供的参数
   * @throws IOException Files.list(Path) 抛出
   */
  static Stream<Arguments> loadTestFiles() throws IOException {
    return Files.list(Paths.get(root, dirName))
        .filter(Files::isRegularFile)
        .filter(path -> path.getFileName().toString().startsWith(prefix))
        .map(path -> Arguments.of(path.getFileName().toString(), path));
  }

  /** 测试开始之前,显示原文的文件名 */
  @BeforeAll
  public static void beforeTest() {
    System.out.println("原文文件: " + originalFileName);
  }

  /**
   * 单元测试
   *
   * @param fileName 测试文件名
   * @param filePath 测试文件所在路径
   * @throws IOException Files.readString(Path) 抛出
   */
  @ParameterizedTest(name = "[{index}] 测试文件: {0}")
  @MethodSource("loadTestFiles")
  public void test(String fileName, Path filePath) throws IOException {
    String originalContent = Files.readString(originalFilePath);

    assertNotSame(originalContent.trim(), "");

    String content = Files.readString(filePath);

    assertNotSame(content.trim(), "");

    // 使用 HanLP 分词器
    double simHashSimilarity =
        testChecker(SimilarityCheckUtils.simHashCheckerWithHanLP(), originalContent, content);
    BigDecimal bd = new BigDecimal(simHashSimilarity);

    // 结果精确到小数点后两位
    simHashSimilarity = bd.setScale(2, RoundingMode.HALF_UP).doubleValue();

    System.out.println("测试文件: " + fileName + ", SimHash 相似度: " + simHashSimilarity);

    System.out.println();
  }

  /**
   * 测试主体,要测试的是 checkSimilarity() 这个方法
   *
   * @param checker 欲使用的文本相似度分析器
   * @param s1 文本1
   * @param s2 文本2
   * @return s1 和 s2 的内容相似度
   */
  private double testChecker(Checker checker, String s1, String s2) {
    long start = System.currentTimeMillis();
    // 测试对象
    double result = checker.checkSimilarity(s1, s2);
    long end = System.currentTimeMillis();

    // 必须 5 秒之内给出答案
    assertTrue(end - start <= 5000);

    // 结果必须是 0 到 1 之间的小数
    assertTrue(result < 1 && result > 0);

    System.out.println(checker.getClass().getName() + " 耗时 " + (end - start) + " ms");
    return result;
  }
}

测试目标:checkSimilarity()方法。

测试思路:读取测试用例中所有待查重论文文件的内容,分别与原文文件的内容作文本相似度分析,并在测试前后分别计时,以计算单次处理所耗费的时间。使用HanLP作为分词器。

测试覆盖率:如下图所示。

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

  • IllegalArgumentException:输入文本为空,通常是文件的内容为空。

    // ExceptionTest.java
    package com.leonard;
    
    import org.junit.Test;
    import com.leonard.util.similarity.SimilarityCheckUtils;
    
    public class ExceptionTest {
      @Test(expected = IllegalArgumentException.class)
      public void testEmptyInput() {
        SimilarityCheckUtils.simHashCheckerWithHanLP().checkSimilarity("", "");
      }
    }
    
    // AnsjWordAnalyzer.java
    public class AnsjWordAnalyzer implements WordAnalyzer {
      //...
      
      @Override
      public List<String> splitWords(String text) {
        if (StringUtils.isBlank(text)) {
          throw new IllegalArgumentException("空白文本");
        }
        
        //...
      }
    
      //...
    }
    
    // HanLPWordAnalyzer.java
    public class HanLPWordAnalyzer implements WordAnalyzer {
      //...
      
      @Override
      public List<String> splitWords(String text) {
        if (StringUtils.isBlank(text)) {
          throw new IllegalArgumentException("空白文本");
        }
        
        //...
      }
    
      //...
    }
    
    // WordAnalyzer.java
    public interface WordAnalyzer {
      //...
      
      default Map<String, Integer> analyze(List<String> words) {
        if (words.isEmpty()) {
          throw new IllegalArgumentException("没有分词,文本可能为空");
        }
        
        //...
      }
      
      //...
    }
    
  • 其它类型异常的处理待开发

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 5 3
Development 开发
· Analysis · 需求分析 (包括学习新技术) 5 8
· Design Spec · 生成设计文档 15 20
· Design Review · 设计复审 5 4
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 5 2
· Design · 具体设计 10 15
· Coding · 具体编码 30 40
· Code Review · 代码复审 10 20
· Test · 测试(自我测试,修改代码,提交修改) 15 35
Reporting 报告
· Test Repor · 测试报告 10 5
· Size Measurement · 计算工作量 5 3
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 35
· 合计 145 190
posted @ 2025-03-14 22:16  嘤狐  阅读(35)  评论(0)    收藏  举报