第一次个人编程作业

这个作业属于哪个课程
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702
这个作业的目标 完成个人项目:论文查重
作业GitHub网址 https://github.com/1112Chy/3124005359-PersonalProject

一、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 25
· Estimate · 估计这个任务需要多少时间 20 25
Development 开发 440 505
· Analysis · 需求分析(包括学习新技术与分词算法) 50 70
· Design Spec · 生成设计文档 20 25
· Design Review · 设计复审 20 15
· Coding Standard · 代码规范(为目前的开发制定合适的规范) 20 15
· Design · 具体设计 40 45
· Coding · 具体编码 130 160
· Code Review · 代码复审 30 25
· Test · 测试(自我测试,修改代码,提交修改) 130 150
Reporting 报告 90 110
· Test Report · 测试报告 30 40
· Size Measurement · 计算工作量 20 25
· Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 40 45
合计 550 640

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

本程序的核心计算模块主要负责文本预处理、中文分词、词频向量构建以及余弦相似度计算。为提高程序的模块化程度与解耦性,将不同功能封装在独立的类中,使文件输入输出与核心算法解耦,便于后续测试与维护。

代码包含两个核心类:

对应模块 主要功能
CosineUtil 文本预处理与查重算法模块 负责文本过滤清洗、分词、词频向量生成与余弦相似度计算
Main 文件读写与程序入口模块 负责解析命令行参数、校验路径、调度算法并输出结果到文件

模块方法与接口规范

1. 文本预处理与分词

  • 方法签名
public static List<String> extractWords(String text);

  • 功能说明:通过正则表达式过滤文本中的所有标点、特殊符号和空白字符,仅保留中英文与数字字符;随后调用 HanLP 分词引擎,返回切分后的词项列表。

2. 相似度计算

  • 方法签名
public static double calculateSimilarity(List<String> words1, List<String> words2);

  • 功能说明:接收两组分词结果,统计词频构建多维向量,计算两个高维向量的余弦夹角值。

3. 文件读写与调度

  • 方法签名
public static void process(String origPath, String copyPath, String ansPath) throws IOException;

  • 功能说明:接收三个绝对路径,校验文件存在性,读取内容后调度 CosineUtil 计算查重率,将格式化为两位小数的结果写入指定文件。

程序运行流程图

       [命令行输入: 3个绝对路径]
                   ↓
         [Main: 参数格式校验]
                   ↓
      [Main: 检查两份文件是否存在]
        ├─ 存在 ─→ [读取文本内容]
        └─ 不存在 ─→ [抛出 FileNotFoundException 退出]
                   ↓
      [CosineUtil.extractWords]
        ├─ 正则过滤标点符号与特殊字符
        └─ HanLP 分词生成词列表
                   ↓
  [CosineUtil.calculateSimilarity]
        ├─ 统计词频映射 (HashMap)
        ├─ 计算向量点积与各向量模长
        └─ 余弦值归一化 (范围 0.00 ~ 1.00)
                   ↓
       [Main: 格式化为两位小数]
                   ↓
      [自动创建目标目录并写入答案文件]

算法关键与独到之处

  • 算法核心:采用 HanLP 分词 + 词频向量余弦相似度(Cosine Similarity)
    余弦相似度公式:

$$\text{Similarity} = \frac{\sum_{i=1}^n A_i B_i}{\sqrt{\sum_{i=1}^n A_i^2} \sqrt{\sum_{i=1}^n B_i^2}}$$

  • 本设计的独到之处
  1. 架构高度解耦:将算法核心逻辑完全封装在 CosineUtil 中,算法计算不依赖任何文件系统 API,便于进行全自动白盒测试。
  2. 稀疏向量优化:对比文本可能包含大量不重叠词汇,采用 HashMap 动态维护词频向量,仅对两篇文章词集的并集计算点积,时间复杂度控制在 $O(N)$。
  3. 正则预编译:将字符过滤正则表达式提升为 static final Pattern 静态常量,避免多测试用例或大文本处理时重复编译带来的系统开销。
  4. 完善的边界兜底:显式处理两篇文章均为空文本(定义为 1.00)、一方为空文本(定义为 0.00)以及模长为 0 的除法溢出边界。

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

JProfiler 性能分析记录

在 IDEA 中集成 JProfiler 插件,针对长文本测试样本(各 10000 字)运行性能探查,监控 CPU 耗时分布:

(在此处插入 JProfiler 的 Call Tree 截屏图片)

(在此处插入 JProfiler 的 Hot Spots 截屏图片)

CPU 耗时排名前三的函数统计

排名 函数 / 模块名称 CPU 耗时占比 说明
1 com.hankcs.hanlp.HanLP.segment 68.4% 分词过程中的双数组字典树匹配与词性标注
2 java.util.regex.Matcher.replaceAll 14.2% 文本预处理阶段的正则表达式清洗匹配
3 CosineUtil.calculateSimilarity 8.6% 词频统计与向量余弦点积运算

优化方案与实施效果

  1. 正则表达式预编译优化:最初版本在 extractWords 中直接调用 text.replaceAll(...),每次执行都会重新调用 Pattern.compile()。优化后改为类加载时预编译单例 Pattern,正则匹配耗时下降约 35%。
  2. 集合容量初始化预估:在生成分词列表与词频映射时,根据分词分片大小显式预设 ArrayListHashMap 初始容量,降低了底层数组频繁扩容与内存重分配(Rehash)的系统开销。

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

部分测试单元代码展示

// 测试用例 1:测试正常增删改的文本对比(题目示例)
@Test
void testSampleCase() {
    List<String> w1 = CosineUtil.extractWords("今天是星期天,天气晴,今天晚上我要去看电影。");
    List<String> w2 = CosineUtil.extractWords("今天是周天,天气晴朗,我晚上要去看电影。");
    double sim = CosineUtil.calculateSimilarity(w1, w2);
    // 增删改后语义相似度应处于合理区间
    assertTrue(sim > 0.60 && sim < 1.00);
}

// 测试用例 2:测试完全相同的内容
@Test
void testExactSame() {
    List<String> w = Arrays.asList("软件工程", "论文", "查重", "测试");
    assertEquals(1.00, CosineUtil.calculateSimilarity(w, w), 0.01);
}

// 测试用例 3:测试完全不相干的文本内容
@Test
void testCompletelyDifferent() {
    List<String> w1 = Arrays.asList("苹果", "香蕉");
    List<String> w2 = Arrays.asList("量子", "物理");
    assertEquals(0.00, CosineUtil.calculateSimilarity(w1, w2), 0.01);
}

// 测试用例 4:测试中英混排与数字版本号
@Test
void testMixedAlphanumeric() {
    List<String> w1 = CosineUtil.extractWords("Java 17 相比 Java 8 性能更高");
    List<String> w2 = CosineUtil.extractWords("Java 21 相比 Java 17 更加现代");
    assertTrue(CosineUtil.calculateSimilarity(w1, w2) > 0.40);
}

测试覆盖率分析

在 IntelliJ IDEA 中使用覆盖率运行(Run 'CosineUtilTest' with Coverage),对计算模块进行覆盖率统计:

(在此处插入 IDEA 单元测试覆盖率窗口截图)

测试用例总计 11 个,涵盖了正常文本、空字符串、全标点文本、乱序文本及各类异常场景,核心算法类 CosineUtil 的行覆盖率达到 100%,分支覆盖率达到 100%。


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

针对文件路径不存在、空文本、无效参数等异常情况,程序均进行了显式防御与捕获处理,避免程序崩溃中断。

异常一:原文文件不存在

  • 触发场景:命令行输入的原文绝对路径有误,或文件在读取前被意外移动/删除。
  • 设计目标:明确抛出 FileNotFoundException 并给出提示,避免系统抛出未检查的底层 NullPointer 崩溃。
  • 处理代码
File origFile = new File(origPath);
if (!origFile.exists()) {
    throw new FileNotFoundException("原文文件不存在: " + origPath);
}

  • 单元测试用例
@Test
void testExceptionOrigNotFound() {
    assertThrows(FileNotFoundException.class, () -> {
        Main.process("invalid/path/orig_not_found.txt", "pom.xml", "ans.txt");
    });
}

异常二:抄袭版论文文件不存在

  • 触发场景:用户指定了正确的原文文件,但抄袭文件路径输入错误。
  • 设计目标:精准提示抄袭版文件缺失信息。
  • 单元测试用例
@Test
void testExceptionCopyNotFound() {
    assertThrows(FileNotFoundException.class, () -> {
        Main.process("pom.xml", "invalid/path/copy_not_found.txt", "ans.txt");
    });
}

异常三:两篇文档均为空或内容过滤后全为空白

  • 触发场景:输入文件为空文件,或者文章全部由标点符号、空格组成。
  • 设计目标:防止分词集合为空导致余弦向量模长为 0 产生数学上的“除以零(0/0)”异常。
  • 处理代码
if (words1.isEmpty() && words2.isEmpty()) {
    return 1.00;
}
if (words1.isEmpty() || words2.isEmpty()) {
    return 0.00;
}

  • 单元测试用例
@Test
void testBothEmpty() {
    assertEquals(1.00, CosineUtil.calculateSimilarity(Collections.emptyList(), Collections.emptyList()), 0.01);
}

异常四:命令行参数缺失

  • 触发场景:运行 java -jar main.jar 时传入的参数少于 3 个。
  • 设计目标:在程序入口进行长度拦截,向标准错误流输出格式指引并退出,不进入后续计算模块。
  • 处理代码
if (args.length != 3) {
    System.err.println("错误:命令行参数必须为 3 个!");
    System.err.println("用法示例: java -jar main.jar [原文路径] [抄袭版路径] [输出答案路径]");
    System.exit(1);
}

posted @ 2026-09-15 23:53  QvQ1112  阅读(7)  评论(0)    收藏  举报