软件工程第二次作业

论文查重程序

Github仓库链接:https://github.com/Mimipap111/TheSecondHomework

这个作业属于哪个课程 23级计科12班
这个作业要求在哪里 个人项目—第2次作业
这个作业的目标 提高我的项目开发能力,同时学会使用JProfiler性能测试工具和实现单元测试优化,继续熟悉Git操作

一、PSP表格(预估时间)

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

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

1. 代码组织结构与模块关系

1.1类的数量

代码中只有一个类,即 Main 类。

1.2函数的数量及功能

共有 12 个函数,各函数功能如下:

  • main:程序的主入口,负责根据命令行参数协调各模块执行,若为测试模式则调用测试相关函数,否则进行论文查重流程。
  • checkArguments:验证命令行参数的合法性,包括参数数量和参数是否为空等情况。
  • verifyFileAvailability:检查指定路径的文件是否存在且为可访问的文件。
  • extractFileContent:读取文件内容,会尝试多种字符编码(如 UTF - 8、GBK 等)以正确提取文件内容。
  • generateDocumentFingerprint:生成文档的 SimHash 指纹,内部会调用分词、词频计算、哈希计算等函数。
  • tokenizeContent:对文本进行分词处理,提取有意义的字符序列(如中文、英文、数字等)。
  • calculateTokenWeights:计算文本中每个分词的权重,以词频作为权重依据。
  • computeTokenHash:计算单个分词的哈希值。
  • computeDifferenceScore:计算两个文档指纹的汉明距离,以此确定差异程度。
  • saveAnalysisResult:将论文查重的结果(如相似度、处理时长等)保存到指定的输出文件。
  • executeTestScenarios:在测试模式下,执行批量的测试用例。
  • processTestcase:处理单条测试用例,验证算法在特定测试文本上的表现。
1.3 程序采用面向对象与过程式结合的方式组织代码,核心模块及调用关系如下:
  • 参数处理模块checkArguments(命令行参数合法性校验),负责验证输入的原文路径、待检路径、输出路径等参数是否符合要求。
  • 文件操作模块verifyFileAvailability(文件可用性验证)、extractFileContent(文件内容提取,含编码自动检测),承担文件存在性检查与内容读取的 IO 操作。
  • 文本处理模块tokenizeContent(文本分词),利用正则表达式提取有意义的字符序列,为后续指纹生成做准备。
  • 指纹生成模块generateDocumentFingerprint(文档指纹生成),内部调用calculateTokenWeights(词频权重计算)、computeTokenHash(词语哈希计算)等方法,基于 SimHash 算法生成文档指纹。
  • 相似度计算模块computeDifferenceScore(指纹差异分数计算),通过计算两个文档指纹的汉明距离来确定相似度。
  • 结果处理模块saveAnalysisResult(分析结果保存),将查重结果、处理时长等信息写入指定输出文件。
  • 测试执行模块executeTestScenarios(测试场景执行)、processTestcase(单测试用例处理),支持批量测试用例的执行与结果验证。
  • 异常处理模块:通过捕获FileNotFoundException(文件未找到)、UnsupportedEncodingException(编码不支持)、IOException(IO 异常)等,统一处理文件操作、内容提取等过程中的业务异常。
1.4. 依赖关系说明
  • 强依赖extractFileContent 必须依赖 verifyFileAvailability 完成文件可用性校验,确保能正确读取文件;generateDocumentFingerprint 必须依赖 tokenizeContentcalculateTokenWeightscomputeTokenHash 来获取分词、词频权重和词语哈希,以生成有效的文档指纹;computeDifferenceScore 依赖 generateDocumentFingerprint 生成的文档指纹来计算差异分数;saveAnalysisResult 依赖前面各模块得到的差异分数、相似度等结果数据,才能完成结果保存。
  • 弱依赖executeTestScenarios 仅在启动测试模式(命令行参数为 verify)时被 main 调用,不影响正常的论文查重流程;各种异常类(FileNotFoundExceptionUnsupportedEncodingExceptionIOException 等)作为全局异常类型,被所有涉及文件操作、内容处理的模块依赖,用于统一捕获和抛出错误,简化错误定位。

2. 核心算法设计

采用改进版 SimHash 算法实现文本相似度计算,流程如下:
·文本预处理:使用正则表达式[\u4e00-\u9fa5a-zA-Z0-9]+提取有意义的字符序列(中文、英文、数字),过滤标点符号与特殊字符
·分词与权重计算: 将预处理后的文本拆分为词语列表,以词频作为权重(出现次数越多权重越高)
·指纹生成
对每个词语计算 64 位哈希值(使用 DJB2 哈希算法变体)
构建 64 维向量空间,根据哈希值每一位的 0/1 状态,累加 / 减去对应词语的权重
对向量空间进行二值化处理(>0 为 1,≤0 为 0),生成最终文档指纹
·相似度计算:通过计算两个指纹的汉明距离(不同位的数量),转换为相似度:相似度 = 1 - 汉明距离/64

3. 关键函数流程图(generateDocumentFingerprint)

a38c6c3be86b5fe626d0e75106b5476

4. 算法独到之处

·多编码自适应:自动检测 UTF-8、GBK、GB2312 等多种编码格式,解决中文文本乱码问题
·权重优化:引入词频作为权重因子,使重要词语(出现次数多)在指纹中占更大比重
·测试友好:支持verify模式批量执行测试用例,便于算法调优与回归测试
·结果可视化:输出包含时间戳、处理时长、相似度等级的详细报告,而非仅返回数值

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

1. 性能优化时间记录

优化项 分析瓶颈耗时(分钟) 实施优化耗时(分钟) 验证效果耗时(分钟) 总计(分钟)
正则表达式预编译 10 15 8 33
指纹生成循环优化 12 20 10 42
编码检测策略调整 8 12 6 26
结果写入IO优化 6 10 5 21
合计 36 57 29 122

2. 性能瓶颈分析

初始版本中,以下函数存在性能问题:
tokenizeContent():正则匹配效率低,占总耗时的 38%
generateDocumentFingerprint():向量计算循环嵌套过深,占总耗时的 42%
extractFileContent():编码检测采用暴力尝试,占总耗时的 15%

3. 优化思路与实施

·正则匹配优化:
将Matcher对象局部变量改为方法内复用,减少对象创建开销
优化正则表达式,移除不必要的捕获组,提高匹配速度
·指纹生成优化:
将 64 次位运算循环展开为局部变量计算,减少数组访问次数
哈希计算改用位运算替代乘法操作:hash = (hash << 5) + hash + c → 更快的hash = hash * 33 + c等价实现
·编码检测优化:
调整编码尝试顺序,将常见编码(UTF-8、GBK)放在前面
增加 BOM 头检测,优先识别带 BOM 的 UTF-8 文件

4. 性能分析

函数 优化前耗时占比 优化后耗时占比 提升幅度
tokenizeContent() 38% 22% 42%
generateDocumentFingerprint() 42% 30% 29%
extractFileContent() 15% 8% 47%
其他函数 5% 5% -

以下性能分析来自于JProfiler
e73ae06b9aa905b83badeed9fff3cee

5. 主要消耗函数分析

优化后,generateDocumentFingerprint()仍为最耗时函数(30%),原因是:
需遍历所有词语(O (n) 复杂度)
每个词语需进行 64 位哈希计算(O (1) 但常数较大)
向量累加操作涉及大量数组访问

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

  1. 单元测试代码
    import org.junit.Test;
    import static org.junit.Assert.*;
    import java.util.List;
    import java.io.File;

    public class MainTest {

    // 测试参数验证逻辑
    @Test
    public void testCheckArguments() {
        // 正常参数
        String[] validArgs = {"/path/orig.txt", "/path/copy.txt", "/path/ans.txt"};
        assertTrue("正常参数应验证通过", Main.checkArguments(validArgs));
        // 参数数量不足
        String[] shortArgs = {"/path/orig.txt", "/path/copy.txt"};
        assertFalse("参数数量不足应验证失败", Main.checkArguments(shortArgs));

        // 空参数
        String[] emptyArgs = {"/path/orig.txt", "", "/path/ans.txt"};
        assertFalse("空参数应验证失败", Main.checkArguments(emptyArgs));
    }

    // 测试文本分词功能
    @Test
    public void testTokenizeContent() {
        // 混合文本测试
        String text = "今天是2023年10月,Weather很好!Go to park?";
        List<String> tokens = Main.tokenizeContent(text);
        assertEquals("分词数量不匹配", 7, tokens.size());
        assertTrue("应包含中文词语", tokens.contains("今天"));
        assertTrue("应包含数字", tokens.contains("2023"));
        assertTrue("应包含英文", tokens.contains("weather"));
        assertFalse("不应包含标点", tokens.contains("!"));
        // 空文本测试
        List<String> emptyTokens = Main.tokenizeContent("   !@#¥%");
        assertTrue("空文本应返回空列表", emptyTokens.isEmpty());
    }

    // 测试哈希差异计算
    @Test
    public void testComputeDifferenceScore() {
        // 完全相同的哈希
        long hash1 = 0b10101010L;
        long hash2 = 0b10101010L;
        assertEquals("相同哈希差异应为0", 0, Main.computeDifferenceScore(hash1, hash2));
        // 一位不同
        long hash3 = 0b10101011L;
        assertEquals("一位不同差异应为1", 1, Main.computeDifferenceScore(hash1, hash3));
        // 所有位不同
        long hash4 = 0b01010101L;
        assertEquals("全不同差异应为8", 8, Main.computeDifferenceScore(hash1, hash4));
    }

    // 测试文件内容提取
    @Test
    public void testExtractFileContent() throws Exception {
        // 创建临时测试文件
        File tempFile = File.createTempFile("test", ".txt");
        tempFile.deleteOnExit();
        // 写入测试内容
        java.nio.file.Files.write(tempFile.toPath(), "测试内容".getBytes("UTF-8"));
        // 验证内容提取
        String content = Main.extractFileContent(tempFile.getAbsolutePath());
        assertEquals("文件内容提取错误", "测试内容", content);
    }

    // 测试指纹生成与相似度计算
    @Test
    public void testSimilarityCalculation() {
        // 完全相同文本
        String text1 = "今天天气很好";
        String text2 = "今天天气很好";
        long hash1 = Main.generateDocumentFingerprint(text1);
        long hash2 = Main.generateDocumentFingerprint(text2);
        double similarity1 = 1.0 - (double)Main.computeDifferenceScore(hash1, hash2)/64;
        assertEquals("相同文本相似度应为1.0", 1.0, similarity1, 0.001);
        // 部分相似文本
        String text3 = "今天天气不错";
        long hash3 = Main.generateDocumentFingerprint(text3);
        double similarity2 = 1.0 - (double)Main.computeDifferenceScore(hash1, hash3)/64;
        assertTrue("部分相似文本相似度应较高", similarity2 > 0.7);
        // 完全不同文本
        String text4 = "明天会下雨";
        long hash4 = Main.generateDocumentFingerprint(text4);
        double similarity3 = 1.0 - (double)Main.computeDifferenceScore(hash1, hash4)/64;
        assertTrue("不同文本相似度应较低", similarity3 < 0.3);
    }
}

2. 测试设计思路

·边界测试:针对空文本、极端参数、边界值(如全 0 哈希)设计用例
·等价类划分:将输入分为有效输入、无效输入、边界输入等等价类
·场景覆盖:覆盖正常处理、异常处理、边界条件等场景
·断言设计:每个测试用例包含多个断言,验证不同维度的正确性

3. 测试覆盖率

方法覆盖率:100%(所有公共方法均被测试)
行覆盖率:92%(部分异常处理分支未完全覆盖)
分支覆盖率:88%(条件判断的各种分支均有覆盖)
877ba6c87c02a4e0edf2f0bff21e8eb
c4e100c2d6e0eee9ea97817c427e2de

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

1. FileNotFoundException
设计目标:当指定路径的文件不存在或不是普通文件时抛出,用于捕获无效文件路径错误
触发条件:
·文件路径不存在
·路径指向目录而非文件
·文件被删除或移动(并发场景)
测试用例:

@Test(expected = FileNotFoundException.class)
public void testFileNotFound() throws Exception {
    Main.verifyFileAvailability("/invalid/path/nonexistent.txt");
}

错误场景:用户输入错误的文件路径,或文件被意外删除
2. UnsupportedEncodingException
设计目标:当文件编码无法被程序支持的编码集识别时抛出
触发条件:
·文件使用罕见编码(如 ISO-2022-JP)
·文件被损坏导致编码识别失败
·二进制文件被当作文本文件处理
测试用例:

@Test(expected = UnsupportedEncodingException.class)
public void testUnsupportedEncoding() throws Exception {
    // 创建一个使用特殊编码的文件
    File tempFile = File.createTempFile("bad", ".txt");
    tempFile.deleteOnExit();
    byte[] invalidBytes = {(byte)0x80, (byte)0x81, (byte)0x82}; // 无法被任何支持的编码识别
    java.nio.file.Files.write(tempFile.toPath(), invalidBytes);
    Main.extractFileContent(tempFile.getAbsolutePath());
}

错误场景:处理非文本文件(如图片、可执行文件)或使用特殊编码的文本
3. IOException
设计目标:捕获文件读写过程中的各种 IO 错误,如权限不足、磁盘满等
触发条件:
·无文件读取权限
·无结果文件写入权限
·磁盘空间不足
·文件被锁定(被其他程序占用)
测试用例:

@Test(expected = IOException.class)
public void testIOException() throws Exception {
    // 在只读目录中尝试写入文件(需要特定测试环境)
    String readOnlyDir = "/proc"; // Linux系统的只读目录
    String outputPath = readOnlyDir + "/test_result.txt";
    Main.saveAnalysisResult(outputPath, 0, 1.0, new Date(), new Date(), 0, "", "");
}

错误场景:在权限受限的环境中运行程序,或磁盘空间不足时
4. 异常处理策略
·防御式编程:所有文件操作前均进行前置检查
·明确提示:异常信息包含具体错误位置和原因
·优雅降级:关键步骤失败时终止程序,非关键步骤失败时记录警告
·资源释放:使用 try-with-resources 确保 IO 资源正确释放

posted @ 2025-09-22 20:20  Azure1204  阅读(34)  评论(0)    收藏  举报