软件工程-作业2

个人项目:论文查重系统

这个作业属于哪个课程 班级链接
这个作业要求在哪里 作业链接
这个作业的目标 算法设计与实现、工程化能力、文档与报告

GitHub 项目地址shiyao.com


一、PSP表格

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

二、计算模块设计

1. 接口设计与实现

类结构

  • Main: 程序入口,处理命令行参数
  • FileUtils: 文件读写工具类
  • PaperCheckService: 查重算法
  • ApplicationExceptionHandler: 启动类异常处理
  • CommandLineArgsChecker: 启动前参数检查

Main->CommandLineArgsChecker(检查参数)->FileUtils(读)->PaperCheckService(查重计算)->FileUtils(写)->结束

算法关键:词频向量的余弦相似度


三、性能改进

PaperCheckService: 查重算法中,一开始想用滑动窗口+二分查找来查重,但是对与复杂文本难以适用于是改用词频向量的余弦相似度

1.滑动窗口+二分查找:

class LCSChecker {
    // 使用二分查找 + 滑动窗口找最长公共子串的长度
    public static int binarySearchLCS(String s1, String s2) {
        // 确保 s1 是较短的字符串,减少哈希集合的大小,提高匹配效率
        if (s1.length() > s2.length()) {
            String temp = s1;
            s1 = s2;
            s2 = temp;
        }

        int low = 0, high = s1.length(); // 二分查找范围
        int bestLength = 0; // 记录最长公共子串的长度

        // 二分查找最长公共子串
        while (low <= high) {
            int mid = (low + high) / 2; // 取中间长度
            if (hasCommonSubstring(s1, s2, mid)) { // 判断是否存在该长度的公共子串
                bestLength = mid;  // 更新最长子串长度
                low = mid + 1;      // 尝试更长的子串
            } else {
                high = mid - 1;     // 尝试更短的子串
            }
        }
        return bestLength;
    }
}

2.词频向量的余弦相似度

@Service
public class PaperCheckService {

    // 停用词表
    private static final Set<String> STOP_WORDS = new HashSet<>(Arrays.asList(
            "的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "与", "上", "这", "你"
    ));

    /**
     * 计算两篇文本的相似度
     * @param original    原始文本
     * @param plagiarized 待检测文本
     * @return 相似度得分(0.0~1.0)
     */
    public static double computeSimilarity(String original, String plagiarized) {
        // 处理空文本边界情况
        if (original.isEmpty() && plagiarized.isEmpty()) return 1.0;  // 两文本均为空视为完全相似
        if (original.isEmpty() || plagiarized.isEmpty()) return 0.0;  // 任一文本为空视为无相似

        // 文本预处理:分词 -> 过滤停用词 -> 过滤单字
        List<String> originalTerms = processText(original);
        List<String> plagiarizedTerms = processText(plagiarized);

        // 构建联合词频向量(Key: 词语, Value: int[2]数组,0位存原文词频,1位存待检测文本词频)
        Map<String, int[]> termFrequency = new HashMap<>();
        buildTermFrequencyVector(originalTerms, termFrequency, 0);  // 处理原文
        buildTermFrequencyVector(plagiarizedTerms, termFrequency, 1);  // 处理待检测文本

        // 计算并返回余弦相似度
        return calculateCosineSimilarity(termFrequency);
    }

    /**
     * 文本预处理流水线
     * @param text 原始文本
     * @return 处理后的词语列表(小写,已过滤停用词和单字)
     * @author zyh
     * @date 2025/03/06
     */
    private static List<String> processText(String text) {
        return ToAnalysis.parse(text).getTerms().stream()  // 分词器进行中文分词
                .map(Term::getName)                        // 提取分词结果
                .filter(word -> !STOP_WORDS.contains(word)) // 过滤停用词
                .filter(word -> word.length() > 1)          // 过滤单字
                .collect(Collectors.toList());
    }

    /**
     * 构建词频统计向量
     * @param terms        词语列表
     * @param frequencyMap 词频统计Map
     * @param index        统计位置(0表示原文,1表示待检测文本)
     * @author zyh
     * @date 2025/03/06
     */
    private static void buildTermFrequencyVector(List<String> terms,
                                                 Map<String, int[]> frequencyMap,
                                                 int index) {
        terms.forEach(term -> {
            frequencyMap.putIfAbsent(term, new int[2]);  // 初始化词频数组
            frequencyMap.get(term)[index]++;             // 对应位置词频+1
        });
    }

    /**
     * 计算余弦相似度公式:cos=(A·B)/(||A||*||B||)
     * @param frequencyMap 词频统计表
     * @return 余弦相似度值(范围0.0~1.0)
     * @author zyh
     * @date 2025/03/06
     */
    private static double calculateCosineSimilarity(Map<String, int[]> frequencyMap) {
        double dotProduct = 0.0;  // 点积(分子)
        double normA = 0.0;       // 向量A的模长平方
        double normB = 0.0;       // 向量B的模长平方

        for (int[] vector : frequencyMap.values()) {
            // 计算点积
            dotProduct += vector[0] * vector[1];
            // 分别累加模长平方
            normA += Math.pow(vector[0], 2);
            normB += Math.pow(vector[1], 2);
        }

        // 处理除零情况
        if (normA == 0 || normB == 0) return 0.0;

        // 计算并返回最终相似度
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }
}

3.Intellij Profiler分析:

四、单元测试

1.单元测试代码

class PaperCheckServiceTest {

    /**
     * 测试完全相同的文本
     * 预期结果:相似度应为1.0
     */
    @Test
    void testExactMatch() {
        assertEquals(1.0, PaperCheckService.computeSimilarity("今天天气很好", "今天天气很好"), 0.01);
    }

    /**
     * 测试完全不同的文本
     * 预期结果:相似度应为0.0
     */
    @Test
    void testCompletelyDifferent() {
        assertEquals(0.0, PaperCheckService.computeSimilarity("今天天气很好", "计算机科学"), 0.01);
    }

    /**
     * 测试空文本处理(双空)
     * 预期结果:相似度应为1.0
     */
    @Test
    void testBothEmpty() {
        assertEquals(1.0, PaperCheckService.computeSimilarity("", ""), 0.01);
    }

    /**
     * 测试空文本与正常文本
     * 预期结果:相似度应为0.0
     */
    @Test
    void testOneEmpty() {
        assertEquals(0.0, PaperCheckService.computeSimilarity("", "深度学习"), 0.01);
    }

    /**
     * 测试单字词过滤
     * 数据构造:原文含单字词,对比文本无单字词
     * 预期结果:相似度为0.0(有效词被过滤后无交集)
     */
    @Test
    void testSingleCharacterFiltered() {
        assertEquals(0.0, PaperCheckService.computeSimilarity("A 数据", "数据挖掘"), 0.01); // 处理后"数据" vs "数据""挖掘"
    }

    /**
     * 测试词序不影响结果
     * 数据构造:相同词汇不同顺序
     * 预期结果:相似度应为1.0
     */
    @Test
    void testWordOrderIrrelevant() {
        assertEquals(1.0, PaperCheckService.computeSimilarity("机器学习 深度学习", "深度学习 机器学习"), 0.01);
    }

    /**
     * 测试部分重复内容
     * 数据构造:两文本有 50% 词汇重叠
     * 预期结果:相似度约为 0.5(实际值取决于词频)
     */
    @Test
    void testPartialOverlap() {
        String text1 = "自然语言处理 文本分析 算法";
        String text2 = "文本分析 机器学习 算法";
        double similarity = PaperCheckService.computeSimilarity(text1, text2);
        assertTrue(similarity > 0.4 && similarity < 0.6);
    }

    /**
     * 测试长文本重复
     * 数据构造:长文本与自身+少量新增内容对比
     * 预期结果:相似度接近 1.0
     */
    @Test
    void testLongText() {
        String base = "分布式系统的核心是容错性和一致性。";
        String text1 = base.repeat(100);
        String text2 = text1 + "新增补充内容。";
        assertTrue(PaperCheckService.computeSimilarity(text1, text2) > 0.95);
    }

    /**
     * 测试特殊符号处理
     * 数据构造:含特殊符号的文本与纯净文本对比
     * 预期结果:相似度应为 1.0(符号被过滤)
     */
    @Test
    void testSpecialSymbols() {
        assertEquals(1.0, PaperCheckService.computeSimilarity("数据!@#", "数据"), 0.01);
    }

    /**
     * 测试全停用词导致空向量
     * 数据构造:两文本均为停用词
     * 预期结果:相似度应为 0.0
     */
    @Test
    void testAllStopWords() {
        assertEquals(0.0, PaperCheckService.computeSimilarity("这是的", "就和在"), 0.01);
    }
}

2.覆盖率

五、异常处理

1.异常设计代码

public class ExceptionTest {
    @Test
    void testReadFile_FileNotExist() {
        //场景:文件不存在
        String path = "non_existent.txt";
        String content = FileUtil.readFile(path);
        assertNull(content);  // 预期返回 null
        // 控制台应输出:"文件读错误: non_existent.txt"
    }

    @Test
    void testWriteFile_InvalidPath() {
        //场景:写入路径不可用(如目录不存在)
        String path = "/invalid_directory/result.txt";
        FileUtil.writeFile(path, "test content");
        // 控制台应输出:"文件写错误: /invalid_directory/result.txt"
    }
    @Test
    void testCheckArgs_InvalidArgumentCount() {
        //场景:参数数量不匹配
        String[] args = {"path1.txt", "path2.txt"};  // 仅2个参数
        Exception exception = assertThrows(RuntimeException.class, () ->
                new CommandLineArgsChecker().run(args));
        System.out.println("捕获异常: " + exception.getMessage());
        // 控制台应输出:"捕获异常: 命令错误,请按照格式输入:java -jar main.jar <原文路径> <抄袭版路径> <答案路径>"
        assertTrue(exception.getMessage().contains("命令错误"));
    }
}

2.设计目标

1.testReadFile_FileNotExist

  • 目标
    1. 文件不存在时返回 null,避免程序崩溃
    2. 打印错误路径(文件读错误: non_existent.txt),便于定位问题
  • 场景:文件不存在

2.testWriteFile_InvalidPath

  • 目标
    1. 写入失败时静默处理,避免程序终止
    2. 记录错误路径(文件写错误: /invalid_directory/result.txt
  • 场景:写入路径不可用(如目录不存在)

3.testCheckArgs_InvalidArgumentCount

  • 目标
    1. 参数不足时立即抛出异常,阻止后续无效操作
    2. 提示正确命令格式(java -jar main.jar <原文路径> <抄袭版路径> <答案路径>
  • 场景:参数数量不匹配
posted @ 2025-03-06 19:51  十曜  阅读(85)  评论(0)    收藏  举报