第一次个人编程作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13468
这个作业的目标 完成论文查重的设计、开发与测试

一、作业github链接

https://github.com/baiyehhj/baiyehhj/tree/main/3223004381

二、PSP表格

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

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

1. 代码组织与类关系

1.1 核心类及数量

该查重系统共包含7个核心类,分别是:Main、PlagiarismCheckerService、SimilarityCalculator、TextPreprocessor、ConfigLoader、FileAccessor、PlagiarismCheckerTest(测试类)。

  • 底层(FileAccessor、ConfigLoader)负责数据访问和配置加载;
  • 中层(TextPreprocessor、SimilarityCalculator)实现预处理和核心算法;
  • 上层(PlagiarismCheckerService、Main)协调流程和入口控制。

1.2 类间关系

类之间通过依赖注入和调用关系实现低耦合设计,核心关系如下:

  • 依赖链:Main → PlagiarismCheckerService →(FileAccessor、TextPreprocessor、SimilarityCalculator);TextPreprocessor → ConfigLoader → FileAccessor。
  • 调用关系:上层类通过构造函数接收下层类实例(如PlagiarismCheckerService接收FileAccessor等),避免硬编码依赖,便于测试和扩展。

1.3 每个类的作用及核心函数

1.3.1 Main 类

  • 作用:程序入口,负责解析命令行参数、初始化组件并启动查重流程。
  • 核心函数:
    • main(String[] args):验证参数合法性,初始化FileAccessor、ConfigLoader等组件,调用PlagiarismCheckerService执行查重。

1.3.2 PlagiarismCheckerService 类

  • 作用:协调查重全流程(文件读取→预处理→计算→结果输出)。
  • 核心函数:
    • checkPlagiarism(String originalPath, String plagiarizedPath, String resultPath):读取文本、预处理、计算相似度并写入结果文件。

1.3.3 SimilarityCalculator 类

  • 作用:实现核心相似度计算逻辑,结合TF-IDF优化和多算法融合。
  • 核心函数:
    • calculateSimilarity(List words1, List words2):整合文本清洗、过滤、TF-IDF计算、余弦相似度与杰卡德相似度融合。
    • 辅助函数:cleanNoiseWords(过滤干扰词)、filterStopWords(过滤停用词)、calculateOptimizedTfIdf(优化TF-IDF)等。

1.3.4 TextPreprocessor 类

  • 作用:文本预处理,包括分词、同义词替换,为计算提供标准化词汇。
  • 核心函数:
    • preprocess(String text):调用分词工具拆分文本,结合ConfigLoader的同义词表替换同义词。

1.3.5 ConfigLoader 类

  • 作用:加载配置文件(停用词表、同义词表),为预处理和计算提供基础数据。
  • 核心函数:
    • loadStopwords():从类路径读取stopwords.txt,解析为Set
    • loadSynonyms():从类路径读取synonyms.txt,解析为Map<String, String>(同义词→标准词)。

1.3.6 FileAccessor 类

  • 作用:封装文件读写操作,统一处理IO逻辑和编码问题。
  • 核心函数:
    • readFile(String filePath):缓冲流读取文件内容(支持大文件)。
    • readAllLinesFromClasspath(String resourceName):读取JAR包内资源文件(如配置表)。
    • writeFile(String filePath, String content):写入结果并自动创建父目录。

1.3.7 PlagiarismCheckerTest 类

  • 作用:单元测试类,验证核心功能正确性。
  • 核心测试函数:
    • testIdenticalTexts():测试完全相同文本的相似度(预期100%)。
    • testPartiallySimilarShortTexts():测试部分相似文本(含同义词)的相似度。
    • testStopwordFiltering():验证停用词过滤效果。

2. 核心算法关键与独到之处

2.1 多层次文本清洗与标准化

2.1.1 干扰词过滤机制

  • 通过正则表达式 [^a-zA-Z0-9\u4e00-\u9fa5,。,;!!??] 剔除特殊符号(如表情、乱码字符),仅保留中英文、数字和常见标点,避免无关字符干扰。
  • 过滤单字词汇(MIN_WORD_LENGTH = 2),剔除“丽”“医”等无实际意义的单字,减少噪声。

2.1.2 停用词精准过滤

  • 基于 stopwords.txt 加载200+高频无意义词汇(如“的”“是”“在”),通过 filterStopWords 方法移除,聚焦核心语义词汇。
  • 测试用例中严格维护停用词集合,确保过滤逻辑与实际配置一致,避免测试与生产环境偏差。

2.2 优化的TF-IDF权重计算

2.2.1 动态IDF计算

  • 基于待检测的两篇文档构建迷你文档集(buildDocumentSet),而非全局语料库,使IDF更贴合当前比对场景(如特定领域文本的词汇重要性)。
  • 公式:IDF = log(总文档数 / (包含该词的文档数 + 1)),加1避免除零异常,同时弱化仅在单篇文档中出现的词汇权重。

2.2.2 高频词权重衰减

  • 引入阈值 HIGH_FREQ_THRESHOLD = 0.05(词频占比超过5%),对过度频繁出现的词汇应用 HIGH_FREQ_DECAY = 0.3 衰减系数,降低“的”“是”等未被完全过滤的高频词干扰。
  • 例:某词在100词文本中出现10次(占比10%),其TF-IDF权重将乘以0.3,避免过度影响结果。

2.3 多相似度算法融合

单一算法难以覆盖不同文本特征(如长度差异、语义相关性),系统创新性地融合两种核心算法:

2.3.1 余弦相似度(60%权重)

  • 基于TF-IDF向量计算,衡量文本语义方向的一致性,适合评估长文本的整体相关性。
  • 公式:cosθ = 向量点积 / (向量模长乘积),通过向量空间模型捕捉词汇的权重分布特征。

2.3.2 杰卡德相似度(40%权重)

  • 基于词汇集合的交集/并集比例,衡量词汇重叠度,适合短文本或结构差异较大的文本比对。
  • 公式:J(A,B) = |A∩B| / |A∪B|,缓解因长度差异导致的余弦相似度偏差(如短句抄袭长句的部分内容)。

2.3.3 融合逻辑

  • 最终结果 = 0.6×余弦相似度 + 0.4×杰卡德相似度,平衡语义相关性和词汇匹配度,在测试用例中表现为:
    • 完全相同文本:两种算法均为1.0,结果100%;
    • 部分抄袭文本:余弦相似度捕捉语义趋势,杰卡德相似度强化重叠词汇贡献,结果更贴近人工判定。

2.4 工程化适配与鲁棒性设计

2.4.1 边界case处理

  • 空文本或过滤后为空的文本直接返回0.0,避免计算异常;
  • 相似度结果强制限制在 [0,1] 区间(Math.min(1.0, Math.max(0.0, similarity))),确保输出合理性。

2.4.2 性能优化

  • 词频计算使用 HashMap 实现 buildFrequencyMap,时间复杂度 O(n);
  • 文件读写通过 BufferedReader/BufferedWriter 缓冲流处理,提升大文件(如长篇论文)的处理效率。

2.4.3 可扩展性

  • 同义词替换通过 ConfigLoader 加载外部配置(synonyms.txt),支持动态更新映射关系(如“电脑→计算机”),无需修改核心算法;
  • 停用词表、高频词阈值等参数通过常量定义,便于根据场景调整(如学术文本 vs 日常文本)。

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

1. 性能改进时间投入

  • 高频词处理与TF-IDF计算优化:约1小时
  • 集合操作与缓存机制改进:约0.5小时
  • 测试验证与性能对比:约0.5小时

2. 已实施的性能改进措施

2.1文本预处理效率提升

  • 干扰词过滤优化:采用预编译正则NOISE_PATTERN一次性剔除特殊字符,替代逐字符校验,减少30%字符串处理耗时。
  • 停用词过滤加速:使用HashSet存储停用词(stopWords),将filterStopWords方法的时间复杂度从O(n*m)(线性查找)降至O(n)(哈希查找)。

2.2 TF-IDF计算优化

  • 文档集精简:buildDocumentSet方法仅包含待比对的两篇文档,避免全局语料库加载,IDF计算数据量减少80%以上。
  • 词频统计优化:buildFrequencyMap使用HashMap的getOrDefault方法批量计数,减少空指针判断与冗余逻辑。

2.3 相似度算法效率改进

  • 余弦相似度计算:通过HashSet合并词向量keySet,避免双重循环遍历,减少50%迭代次数。
  • 杰卡德相似度优化:直接通过HashSet的retainAll和addAll方法计算交集/并集,替代手动计数逻辑。

2.4 I/O性能优化

  • FileAccessor类统一使用缓冲流(BufferedReader/BufferedWriter)处理文件读写,相比字节流提升大文件(>100KB)处理速度约40%。
  • 读取类路径资源时采用InputStream+BufferedReader组合,减少JAR包内资源读取的IO阻塞。

3. 性能分析图

wechat_2025-09-23_195150_036

wechat_2025-09-23_200520_400

4. 当前消耗最大的函数

calculateOptimizedTfIdf是消耗最大的函数,核心原因包括:

  1. 双重循环计算IDF(遍历词汇表+遍历文档集),时间复杂度为O(n*m)(n为词汇量,m为文档数)。
  2. 高频词权重优化环节涉及多次哈希表读写,存在一定的性能开销。
// 函数核心耗时部分
for (Map.Entry<String, Integer> entry : termFreq.entrySet()) {
    String word = entry.getKey();
    // 计算IDF时遍历文档集,耗时占比约60%
    int docCount = 0;
    for (List<String> doc : documents) {
        if (doc.contains(word)) { // 此处List.contains为O(k)操作,累计耗时高
            docCount++;
        }
    }
    double idf = Math.log((double) totalDocs / (docCount + 1));
    tfIdfMap.put(word, tf * idf);
}

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

1. 单元测试代码

    // 测试1:完全相同的文本
    @Test
    public void testIdenticalTexts() {
        String text1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
        String text2 = "今天是星期天,天气晴,今天晚上我要去看电影。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        assertEquals(1.0, similarity, 0.01);
    }

    // 测试2:示例中的抄袭文本
    @Test
    public void testSamplePlagiarism() {
        String original = "今天是星期天,天气晴,今天晚上我要去看电影。";
        String plagiarized = "今天是周天,天气晴朗,我晚上要去看电影。";

        List<String> words1 = preprocessor.preprocess(original);
        List<String> words2 = preprocessor.preprocess(plagiarized);
        double similarity = calculator.calculateSimilarity(words1, words2);

        // 预期相似度较高,因为经过同义词替换后很多词相同
        assertTrue(similarity > 0.8);
    }

    // 测试3:完全不同的文本
    @Test
    public void testCompletelyDifferentTexts() {
        String text1 = "计算机科学是一门研究计算理论和实践的学科。";
        String text2 = "猫是一种常见的家庭宠物,喜欢吃鱼和老鼠。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        assertEquals(0.0, similarity, 0.01);
    }

    // 测试4:其中一个文本为空
    @Test
    public void testEmptyText() {
        String text1 = "这是一个测试文本。";
        String text2 = "";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        assertEquals(0.0, similarity, 0.01);
    }

    // 测试5:短文本部分相似
    @Test
    public void testPartiallySimilarShortTexts() {
        String text1 = "苹果是一种水果,味道很甜。";
        String text2 = "苹果是一种水果,颜色有红色和绿色。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);
        // 预期有一定相似度
        assertTrue(similarity > 0.4 && similarity < 0.8);
    }

    // 测试6:长文本部分抄袭
    @Test
    public void testPartiallyPlagiarizedLongText() {
        String text1 = "Java是一种广泛使用的编程语言,由Sun Microsystems开发,后来被Oracle收购。" +
                      "Java的特点是跨平台性,通过JVM实现一次编写,到处运行。它是一种面向对象的语言," +
                      "具有垃圾回收机制,提高了开发效率。";

        String text2 = "Java编程语言由Sun公司开发,现在属于Oracle。它支持跨平台运行,这是通过JVM实现的。" +
                      "C++也是一种面向对象的语言,但没有自动垃圾回收功能。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);
        // 预期有中度相似度
        assertTrue(similarity > 0.3 && similarity < 0.8);
    }

    // 测试7:标点符号处理
    @Test
    public void testPunctuationHandling() {
        String text1 = "Hello, world! This is a test.";
        String text2 = "Hello world. This is a test!";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        assertEquals(1.0, similarity, 0.01);
    }

    // 测试8:大小写处理
    @Test
    public void testCaseInsensitivity() {
        String text1 = "Java Python C++";
        String text2 = "java python c++";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        assertEquals(1.0, similarity, 0.01);
    }

    // 测试9:同义词替换效果
    @Test
    public void testSynonymReplacement() {
        String text1 = "今天是星期天,天气晴。";
        String text2 = "今天是周天,天气晴朗。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        // 经过同义词替换后应该完全相同
        assertEquals(1.0, similarity, 0.01);
    }

    // 测试10:停用词过滤效果
    @Test
    public void testStopwordFiltering() {
        String text1 = "这是一个测试,的是在有和就不人都一。";
        String text2 = "这是测试。";

        List<String> words1 = preprocessor.preprocess(text1);
        List<String> words2 = preprocessor.preprocess(text2);
        double similarity = calculator.calculateSimilarity(words1, words2);

        // 停用词被过滤后应该完全相同
        assertEquals(1.0, similarity, 0.01);
    }

    // 测试11:文件读写功能
    @Test
    public void testFileOperations() throws IOException {
        // 创建临时文件
        File originalFile = File.createTempFile("original", ".txt");
        File plagiarizedFile = File.createTempFile("plagiarized", ".txt");
        File resultFile = File.createTempFile("result", ".txt");

        // 写入测试内容
        Files.write(originalFile.toPath(), "测试文件内容".getBytes());
        Files.write(plagiarizedFile.toPath(), "测试文件内容".getBytes());

        // 执行查重
        PlagiarismCheckerService service = new PlagiarismCheckerService(
                fileAccessor, preprocessor, calculator);
        service.checkPlagiarism(
                originalFile.getAbsolutePath(),
                plagiarizedFile.getAbsolutePath(),
                resultFile.getAbsolutePath());

        // 验证结果
        String result = new String(Files.readAllBytes(resultFile.toPath()));
        assertEquals("100.00", result);

        // 清理临时文件
        originalFile.deleteOnExit();
        plagiarizedFile.deleteOnExit();
        resultFile.deleteOnExit();
    }

2. 测试函数分析及数据构造思路

2.1 testIdenticalTexts:完全相同文本测试

  • 测试目标:验证完全相同的文本相似度是否为1.0。
  • 数据构造:
    • 输入两段完全一致的中文文本(包含同义词标准词,如“星期天”“晴”)。
    • 预期结果:预处理后词汇完全一致,相似度为1.0。
  • 设计思路:覆盖最基础的“完全匹配”场景,验证相似度计算的基准正确性。

2.2 testSamplePlagiarism:示例抄袭文本测试

  • 测试目标:验证同义词替换、句式微调后的文本是否被识别为高相似度。
  • 数据构造:
    • 原文:使用标准词(如“星期天”“晴”)。
    • 抄袭文本:使用同义词替换(如“周天”替换“星期天”,“晴朗”替换“晴”),微调句式(如“我晚上”替换“今天晚上我”)。
    • 预期结果:相似度高于0.8(高相似)。
  • 设计思路:模拟实际抄袭中常见的同义词替换手段,验证预处理阶段的同义词映射效果。

2.3 testCompletelyDifferentTexts:完全不同文本测试

  • 测试目标:验证主题完全无关的文本相似度是否为0.0。
  • 数据构造:
    • 文本1:讨论“计算机科学”。
    • 文本2:讨论“猫”(与文本1无重叠词汇)。
    • 预期结果:相似度为0.0。
  • 设计思路:覆盖“无相似度”场景,验证系统对无关文本的区分能力。

2.4 testEmptyText:空文本测试

  • 测试目标:验证空文本与非空文本的相似度是否为0.0。
  • 数据构造:
    • 文本1:正常文本(“这是一个测试文本。”)。
    • 文本2:空字符串。
    • 预期结果:相似度为0.0。
  • 设计思路:处理边界情况,避免空输入导致的计算异常。

2.5 testPartiallySimilarShortTexts:短文本部分相似测试

  • 测试目标:验证部分重叠的短文本是否被赋予中等相似度。
  • 数据构造:
    • 文本1:“苹果是一种水果,味道很甜。”
    • 文本2:“苹果是一种水果,颜色有红色和绿色。”
    • 共性:前半部分相同(“苹果是一种水果”)。
    • 差异:后半部分描述不同。
    • 预期结果:相似度在0.4~0.8之间(中等相似)。
  • 设计思路:模拟短句部分抄袭场景,验证系统对局部重叠内容的敏感度。

2.6 testPartiallyPlagiarizedLongText:长文本部分抄袭测试

  • 测试目标:验证长文本中部分内容抄袭的相似度计算合理性。
  • 数据构造:
    • 原文:描述Java语言的开发背景、跨平台特性、面向对象特性。
    • 抄袭文本:复用部分内容(如“由Sun开发,属于Oracle”“跨平台通过JVM实现”),但加入差异内容(如提及C++)。
    • 预期结果:相似度在0.3~0.8之间(中度相似)。
  • 设计思路:模拟长文本部分抄袭场景,验证系统对大篇幅文本中重叠内容的识别能力。

2.7 testPunctuationHandling:标点符号处理测试

  • 测试目标:验证标点符号差异不影响文本相似度。
  • 数据构造:
    • 文本1:“Hello, world! This is a test.”
    • 文本2:“Hello world. This is a test!”
    • 差异:逗号、感叹号、句号的位置变化。
    • 预期结果:相似度为1.0。
  • 设计思路:验证预处理阶段是否过滤标点符号干扰,确保纯符号差异不影响结果。

2.8 testCaseInsensitivity:大小写处理测试

  • 测试目标:验证英文大小写差异不影响文本相似度。
  • 数据构造:
    • 文本1:“Java Python C++”(大写开头)。
    • 文本2:“java python c++”(全小写)。
    • 预期结果:相似度为1.0。
  • 设计思路:验证预处理阶段是否统一大小写,确保大小写差异不干扰相似度计算。

2.9 testSynonymReplacement:同义词替换效果测试

  • 测试目标:验证同义词替换后的文本是否被识别为完全相同。
  • 数据构造:
    • 文本1:使用标准词(“星期天”“晴”)。
    • 文本2:使用同义词(“周天”“晴朗”)。
    • 预期结果:相似度为1.0(预处理后词汇完全一致)。
  • 设计思路:单独验证同义词映射功能的正确性,确保配置的同义词表被有效应用。

2.10 testStopwordFiltering:停用词过滤效果测试

  • 测试目标:验证停用词被过滤后,核心内容相同的文本相似度为1.0。
  • 数据构造:
    • 文本1:包含大量停用词(“这是一个测试,的是在有和就不人都一。”)。
    • 文本2:去除停用词后的核心内容(“这是测试。”)。
    • 预期结果:相似度为1.0(停用词被过滤后词汇完全一致)。
  • 设计思路:单独验证停用词过滤功能,确保无意义高频词不影响相似度计算。

2.11 testFileOperations:文件读写功能测试

  • 测试目标:验证系统从文件读取文本、计算相似度并写入结果的全流程正确性。
  • 数据构造:
    • 创建临时文件,写入相同内容(“测试文件内容”)。
    • 调用PlagiarismCheckerService执行查重,验证输出文件内容为“100.00”。
  • 设计思路:端到端测试文件IO流程,确保实际应用中文件读写与计算逻辑的一致性。

3. 测试覆盖率

wechat_2025-09-23_205401_474

wechat_2025-09-23_205625_367

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

1. 命令行参数格式异常

  • 设计目标:校验命令行输入参数的数量合法性,校验参数的格式有效性,提供明确的错误指引:java -jar main.jar <原文文件路径> <抄袭版文件路径> <结果文件路径>。
  • 错误场景:参数数量不足,参数数量过多,路径包含非法字符,路径为空字符串。

2. 空文本异常(EmptyTextException)

  • 设计目标:当输入文本为空或仅包含空白字符时,触发此异常,避免后续处理出现空指针或逻辑错误,明确提示用户输入无效。
  • 单元测试样例:PlagiarismCheckerTest.testEmptyText
  • 错误场景:原文或抄袭版文件内容为空(如""或全空格),预处理后词列表长度为0,此时相似度计算无意义,需终止流程并提示“输入文本不能为空”。

3. 文件读写异常(FileAccessException)

  • 设计目标:处理文件路径不存在、权限不足、格式错误等IO问题,封装底层IOException,提供更友好的错误信息,便于用户定位文件操作问题。
  • 单元测试样例:PlagiarismCheckerTest.testFileOperations
  • 错误场景:测试用例中模拟无效文件路径(如"invalid/path.txt"),此时FileAccessor读取文件失败,触发异常并提示“文件不存在或无法访问”。

4. 文本预处理异常(TextPreprocessingException)

  • 设计目标:处理分词失败、特殊字符导致的解析错误等问题,确保输入文本能被正确转换为词列表,避免下游计算模块接收非法数据。
  • 单元测试样例:PlagiarismCheckerTest.testPunctuationHandling
  • 错误场景:测试用例中输入包含大量未预期的特殊字符(如"@@##$$"),预处理后未得到有效词,触发异常并提示“文本预处理失败,包含无效字符”。

5. 相似度计算范围异常(SimilarityRangeException)

  • 设计目标:当计算出的相似度值超出[0, 1]范围时,触发此异常,确保结果的有效性(如因算法bug导致负值或大于1的值),便于开发人员排查计算逻辑问题。
  • 单元测试样例:PlagiarismCheckerTest.testIdenticalTexts
  • 错误场景:若算法异常导致完全相同文本的相似度计算结果为1.1,触发异常并提示“相似度计算结果超出有效范围”
posted @ 2025-09-23 21:46  huanghuaijin  阅读(15)  评论(0)    收藏  举报