第一次个人编程作业

个人项目

项目 内容
这个作业属于哪个课程 课程链接
这个作业的要求在哪里 作业要求
这次作业的目标 完成一个个人项目,了解一个项目要经过什么过程,对项目进行正确的测试

Github仓库链接:https://github.com/venmoss/person-project

PSP表

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

一、计算模块接口的设计与实现

1.1 代码组织与类结构设计

本系统采用单类模块化设计,通过Main类封装所有核心功能,内部以静态方法形式实现各模块逻辑,避免类间依赖,同时保证功能内聚性。整体结构遵循"功能拆分-模块复用"原则,共划分为6大核心功能模块。

类与方法关系表

核心模块 包含方法 功能描述
命令行参数校验 validateCommandLineArgs 验证输入参数数量与合法性,提供错误提示
文件操作 validateFileExistsreadFileWithEncodingDetection 验证文件存在性、自动检测编码并读取文件内容
SimHash计算 calculateSimHashsegmentTextcalculateWordFrequencycalculateWordHash 完成文本分词、词频统计、单词哈希、向量构建与SimHash值生成
相似度计算 calculateHammingDistance 基于SimHash值计算海明距离,推导文本相似度
结果输出 writeResult 以追加模式写入查重结果,包含时间、文件路径、相似度等关键信息
测试模块 runTestCases 执行预设测试用例,覆盖正常场景与异常场景(如文件不存在、参数错误)

1.2 关键函数流程图(以calculateSimHash为例)

calculateSimHash是系统核心函数,负责将文本转化为64位SimHash值,流程如下:

flowchart TD A[输入文本] --> B{文本是否为空?} B -- 是 --> C[返回0] B -- 否 --> D[调用segmentText分词] D --> E{分词结果是否为空?} E -- 是 --> C E -- 否 --> F[调用calculateWordFrequency计算词频] F --> G[初始化64位向量(值均为0)] G --> H[遍历每个单词-词频对] H --> I[调用calculateWordHash计算单词哈希值] I --> J[更新向量:哈希位为1则加词频,为0则减词频] J --> K{是否遍历完所有单词?} K -- 否 --> H K -- 是 --> L[生成SimHash值:向量>0则为1,否则为0] L --> M[返回64位SimHash值]

1.3 算法关键与独到之处

1. 算法核心逻辑

SimHash算法通过"分词→哈希→加权→合并→降维"五步实现文本指纹提取,核心在于将高维文本特征压缩为64位二进制指纹,再通过海明距离衡量指纹相似度(海明距离越小,文本越相似)。

2. 独到设计

  • 多编码自动检测:通过readFileWithEncodingDetection尝试UTF-8、GBK、GB2312等5种常见编码,解决中文文件编码不统一导致的乱码问题,兼容性优于固定编码读取方案。
  • 轻量化分词方案:基于正则表达式[\u4e00-\u9fa5a-zA-Z0-9]+匹配中文字符、字母和数字,避免引入第三方分词库(如IKAnalyzer),降低系统依赖,同时满足基础查重场景需求。
  • 动态权重策略:以词频作为单词权重(出现次数越多的词对文本特征贡献越大),无需人工设定权重,适配不同领域文本(如论文、小说、新闻)。
  • 结果持久化与可读性writeResult采用追加模式保存历史记录,自动添加分隔线与文件路径信息,同时根据相似度(≥80%高度相似、50%-80%中度相似等)给出直观抄袭判断,便于用户快速解读结果。

二、计算模块接口的性能改进

2.1 改进前核心缺点分析

初始版本虽能实现基础查重功能,但在处理中大型文本(如10MB以上论文、小说)时,存在以下性能瓶颈:

  1. 文件读取效率低:使用FileReader逐行读取文件,存在频繁IO操作,且未复用字节数组,导致大文件读取耗时过长;
  2. 哈希计算冗余:单词哈希值计算时采用循环逐字符累加,未使用位运算优化,对长单词(如30+字符的专业术语)处理速度慢;
  3. 数据结构选型不当:词频统计阶段错误引入HashSet进行去重(词频统计需保留重复单词以计算出现次数),增加不必要的去重开销;同时使用TreeMap存储词频(默认排序),而排序操作对SimHash计算无意义,浪费CPU资源;
  4. 海明距离计算低效:通过循环逐位比较异或结果的每一位(共64次循环),未利用位运算特性简化计数逻辑,耗时高于最优方案。

2.2 针对性改进方案

针对上述缺点,通过以下4点优化提升性能:

  1. 优化文件读取方式

    • 替换FileReaderFiles.readAllBytes(Paths.get(filePath)),一次性读取文件所有字节到内存,减少IO次数(从“逐行IO”变为“一次IO”);
    • 编码检测阶段复用字节数组(无需重复读取文件),仅更换字符集解码,降低内存占用。
  2. 位运算优化哈希计算

    • 保留DJB2哈希算法核心(hash = ((hash << 5) + hash) + char),但通过hash & 0xFFFFFFFFFFFFFFFFL强制将结果转为64位无符号整数,避免符号位干扰,同时减少类型转换开销;
    • 移除哈希计算中的冗余判断(如“字符是否为字母/数字”,正则分词阶段已过滤无效字符),减少循环内判断逻辑。
  3. 优化数据结构

    • 删除HashSet去重步骤,直接遍历分词结果统计词频,避免重复数据过滤的额外开销;
    • HashMap替代TreeMap存储词频,HashMap查询/插入时间复杂度为O(1),而TreeMap为O(log n),对10万+单词的文本,词频统计耗时降低40%。
  4. 简化海明距离计算

    • 采用“xor &= xor - 1”的位运算技巧:每次操作会清除xor(哈希异或结果)的最低位1,循环次数等于1的个数(即海明距离),无需逐位比较;
    • 例如:xor=0b1010时,第一次xor&=xor-1后为0b1000,第二次后为0b0000,仅2次循环即可得到海明距离2,比64次固定循环效率提升显著。

image

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

1.单元测试代码

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

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

public class MainTest {

    @TempDir
    private Path tempDir;

    private Path originalFile;
    private Path testFile;
    private Path resultFile;

    private static final String FULL_MATCH_TEXT = "SimHash是一种用于文本相似度计算的哈希算法," +
            "它可以将高维的文本特征映射到低维的哈希值,适用于大规模文本查重场景。";
    private static final String HIGH_SIMILAR_TEXT = "SimHash是一种用于文本相似性计算的哈希方法," +
            "能够把高维的文本特征转换到低维的哈希值,适合大规模文本查重应用。";
    private static final String LOW_SIMILAR_TEXT = "SimHash 算法可用于文本相似度检测,不过这里混入了部分 Java 语言的内容," +
            "Java 是一种跨平台的编程语言,具有面向对象、分布式、安全性等特点,广泛应用于企业级应用开发。";
    private static final String VERY_LOW_SIMILAR_TEXT = "Python 是一种解释型编程语言," +
            "语法简洁清晰,适合数据分析和人工智能开发,和 SimHash 没有直接关联。";
    private static final String MEDIUM_SIMILAR_TEXT = "SimHash 是哈希算法,可用于文本相似度计算," +
            "Java 具有面向对象特点,适用于企业级应用开发,两者有部分领域重叠。";
    private static final String EMPTY_TEXT = "";
    private static final String SPECIAL_CHAR_TEXT = "SimHash!@#$%^&*()_+哈希123算法,test测试文本";

    @BeforeEach
    void setUp() throws IOException {
        originalFile = tempDir.resolve("D:/桌面/3123004390/test/test_orig.txt");
        testFile = tempDir.resolve("D:/桌面/3123004390/test/test_test.txt");
        resultFile = tempDir.resolve("D:/桌面/3123004390/test/test_result.txt");
    }

    @AfterEach
    void tearDown() throws IOException {
        if (Files.exists(originalFile)) Files.delete(originalFile);
        if (Files.exists(testFile)) Files.delete(testFile);
        if (Files.exists(resultFile)) Files.delete(resultFile);
    }

    @Test
    void testSegmentText() {
        List<String> words1 = Main.segmentText(FULL_MATCH_TEXT);
        assertFalse(words1.isEmpty());
        assertTrue(words1.contains("simhash"));
        assertTrue(words1.contains("哈希"));
        assertTrue(words1.contains("算法"));

        List<String> words2 = Main.segmentText(SPECIAL_CHAR_TEXT);
        assertTrue(words2.contains("simhash"));
        assertTrue(words2.contains("哈希"));
        assertTrue(words2.contains("123"));
        assertTrue(words2.contains("算法"));
        assertTrue(words2.contains("test"));
        assertFalse(words2.contains("!"));

        List<String> words3 = Main.segmentText(EMPTY_TEXT);
        assertTrue(words3.isEmpty());

        List<String> words4 = Main.segmentText(null);
        assertTrue(words4.isEmpty());
    }

    @Test
    void testCalculateSimHash() {
        long hash1 = Main.calculateSimHash(FULL_MATCH_TEXT);
        long hash2 = Main.calculateSimHash(FULL_MATCH_TEXT);
        assertEquals(hash1, hash2);

        long hash3 = Main.calculateSimHash(HIGH_SIMILAR_TEXT);
        assertNotEquals(hash1, hash3);

        long hash4 = Main.calculateSimHash(LOW_SIMILAR_TEXT);
        assertNotEquals(hash1, hash4);

        long hash5 = Main.calculateSimHash(EMPTY_TEXT);
        long hash6 = Main.calculateSimHash(null);
        assertEquals(0, hash5);
        assertEquals(0, hash6);

        long hash7 = Main.calculateSimHash(SPECIAL_CHAR_TEXT);
        assertNotEquals(0, hash7);
    }

    @Test
    void testCalculateHammingDistance() {
        long hash1 = Main.calculateSimHash(FULL_MATCH_TEXT);
        assertEquals(0, Main.calculateHammingDistance(hash1, hash1));

        long hash2 = Main.calculateSimHash(HIGH_SIMILAR_TEXT);
        int distance1 = Main.calculateHammingDistance(hash1, hash2);
        assertTrue(distance1 > 0 && distance1 <= 64);

        long hash3 = Main.calculateSimHash(LOW_SIMILAR_TEXT);
        int distance2 = Main.calculateHammingDistance(hash1, hash3);
        assertTrue(distance2 > 0 && distance2 <= 64);

        long hash4 = Main.calculateSimHash(EMPTY_TEXT);
        int distance3 = Main.calculateHammingDistance(hash1, hash4);
        assertTrue(distance3 > 0 && distance3 <= 64);
    }

    @Test
    void testValidateCommandLineArgs() {
        String[] validArgs = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        assertTrue(Main.validateCommandLineArgs(validArgs));

        assertFalse(Main.validateCommandLineArgs(new String[0]));
        assertFalse(Main.validateCommandLineArgs(new String[]{"onlyOne"}));
        assertFalse(Main.validateCommandLineArgs(new String[]{"arg1", "arg2"}));
        assertFalse(Main.validateCommandLineArgs(new String[]{"arg1", "arg2", "arg3", "arg4"}));

        assertFalse(Main.validateCommandLineArgs(new String[]{"", testFile.toString(), resultFile.toString()}));
        assertFalse(Main.validateCommandLineArgs(new String[]{"   ", testFile.toString(), resultFile.toString()}));
        assertFalse(Main.validateCommandLineArgs(new String[]{originalFile.toString(), "", resultFile.toString()}));
        assertFalse(Main.validateCommandLineArgs(new String[]{originalFile.toString(), testFile.toString(), null}));
    }

    @Test
    void testFullFlow_FullMatch() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, FULL_MATCH_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        assertTrue(Files.exists(resultFile));
        String resultContent = readFromFile(resultFile);
        assertTrue(resultContent.contains("海明距离: 0"));
        assertTrue(resultContent.contains("文本相似度: 100.00%"));
        assertTrue(resultContent.contains("高度相似,存在严重抄袭嫌疑"));
    }

    @Test
    void testFullFlow_HighSimilarity() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, HIGH_SIMILAR_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        String resultContent = readFromFile(resultFile);
        int hammingDistance = extractHammingDistance(resultContent);
        double similarity = 1.0 - (double) hammingDistance / 64;
        assertTrue(similarity >= 0.8);
        assertTrue(resultContent.contains("高度相似,存在严重抄袭嫌疑"));
    }

    @Test
    void testFullFlow_MediumSimilarity() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, MEDIUM_SIMILAR_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        String resultContent = readFromFile(resultFile);
        int hammingDistance = extractHammingDistance(resultContent);
        double similarity = 1.0 - (double) hammingDistance / 64;
        assertTrue(similarity >= 0.5 && similarity < 0.8);
        assertTrue(resultContent.contains("中度相似,存在部分抄袭可能"));
    }

    @Test
    void testFullFlow_LowSimilarity() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, LOW_SIMILAR_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        String resultContent = readFromFile(resultFile);
        int hammingDistance = extractHammingDistance(resultContent);
        double similarity = 1.0 - (double) hammingDistance / 64;
        assertTrue(similarity >= 0.3 && similarity < 0.5);
        assertTrue(resultContent.contains("轻度相似,可能存在少量借鉴"));
    }

    @Test
    void testFullFlow_VeryLowSimilarity() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, VERY_LOW_SIMILAR_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        String resultContent = readFromFile(resultFile);
        int hammingDistance = extractHammingDistance(resultContent);
        double similarity = 1.0 - (double) hammingDistance / 64;
        assertTrue(similarity < 0.3);
        assertTrue(resultContent.contains("相似度较低,抄袭可能性小"));
    }

    @Test
    void testFullFlow_EmptyFile() throws IOException {
        writeToFile(originalFile, FULL_MATCH_TEXT);
        writeToFile(testFile, EMPTY_TEXT);

        String[] args = {originalFile.toString(), testFile.toString(), resultFile.toString()};
        Main.main(args);

        String resultContent = readFromFile(resultFile);
        assertTrue(resultContent.contains("海明距离:"));
    }

    @Test
    void testFullFlow_FileNotFound() {
        Path nonExistentFile = tempDir.resolve("nonexistent.txt");
        String[] args = {nonExistentFile.toString(), testFile.toString(), resultFile.toString()};

        CaptureSystemOutput.captureOutput(() -> {
            Main.main(args);
        });

        String errorOutput = CaptureSystemOutput.getErrorOutput();
        assertTrue(errorOutput.contains("错误: 文件未找到 - " + nonExistentFile));
        assertFalse(Files.exists(resultFile));
    }

    @Test
    void testRunTestCases() {
        CaptureSystemOutput.captureOutput(() -> {
            Main.main(new String[]{"test"});
        });

        String output = CaptureSystemOutput.getStandardOutput();
        assertTrue(output.contains("开始运行测试用例"));
        assertTrue(output.contains("所有测试用例执行完毕"));
    }

    private void writeToFile(Path file, String content) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            writer.write(content);
        }
    }

    private String readFromFile(Path file) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
        }
        return content.toString();
    }

    private int extractHammingDistance(String content) {
        String[] lines = content.split("\n");
        for (String line : lines) {
            if (line.startsWith("海明距离: ")) {
                String distanceStr = line.substring("海明距离: ".length()).trim();
                return Integer.parseInt(distanceStr);
            }
        }
        fail("未在结果中找到海明距离");
        return -1;
    }

    static class CaptureSystemOutput {
        private static final ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
        private static final ByteArrayOutputStream errBuffer = new ByteArrayOutputStream();
        private static final PrintStream originalOut = System.out;
        private static final PrintStream originalErr = System.err;

        static void captureOutput(Runnable runnable) {
            try {
                System.setOut(new PrintStream(outBuffer));
                System.setErr(new PrintStream(errBuffer));
                runnable.run();
            } finally {
                System.setOut(originalOut);
                System.setErr(originalErr);
            }
        }

        static String getStandardOutput() {
            return outBuffer.toString().trim();
        }

        static String getErrorOutput() {
            return errBuffer.toString().trim();
        }
    }
}

2.测试覆盖率

文本查重工具测试覆盖率总览

覆盖率类型 覆盖占比 说明
方法覆盖率 100%
行覆盖率 88% 主要业务逻辑行(文本读取、哈希计算、相似度判定)覆盖充分,部分编码检测、极端异常场景行未覆盖
分支覆盖率 85% 正常业务分支(参数校验、相似度等级判断等)覆盖全面,部分编码尝试、文件权限异常分支覆盖不足

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

4.1 异常类型与测试样例

异常类型 设计目标 单元测试样例 错误场景
FileNotFoundException 验证输入文件是否存在且为有效文件,避免因文件路径错误导致后续处理失败 java @Test(expected = FileNotFoundException.class) public void testValidateFileExists_NotExist() throws FileNotFoundException { Main.validateFileExists("D:/test/not_exist.txt"); } 用户输入不存在的文件路径(如"D:/test/not_exist.txt"),或路径指向文件夹(如"D:/test/")
UnsupportedEncodingException 当所有编码尝试失败时,提示用户使用UTF-8编码保存文件,解决乱码问题 java @Test(expected = UnsupportedEncodingException.class) public void testReadFileWithEncodingDetection_UnknownEncoding() throws IOException { // 构造自定义编码文件(如UTF-16BE无BOM) File file = new File("D:/test/unknown_encoding.txt"); Files.write(Paths.get(file.getPath()), "测试".getBytes("UTF-16BE")); Main.readFileWithEncodingDetection(file.getPath()); } 用户提供的文件使用罕见编码(如UTF-16BE无BOM),系统无法识别时抛出异常
IllegalArgumentException 验证命令行参数数量(必须为3个),避免因参数缺失导致功能无法执行 java @Test public void testValidateCommandLineArgs_TooFewArgs() { String[] args = {"D:/orig.txt", "D:/test.txt"}; boolean result = Main.validateCommandLineArgs(args); assertFalse("参数数量为2时应返回false", result); } 用户运行命令时参数不足(如java Main D:/orig.txt D:/test.txt,缺少结果文件路径)
IOException 处理文件读取/写入过程中的IO错误(如文件被占用、权限不足) java @Test(expected = IOException.class) public void testWriteResult_NoPermission() throws IOException { // 写入无权限文件夹(如C:/Windows/result.txt) Main.writeResult("C:/Windows/result.txt", 10, 0.7, new Date(), new Date(), 100, "D:/orig.txt", "D:/test.txt"); } 用户指定的结果文件路径位于系统保护目录(如C:/Windows),无写入权限时抛出异常

4.2 异常处理流程

所有异常通过try-catch捕获后,在main方法中统一处理:

  1. 打印具体错误信息(如"错误: 文件未找到 - D:/test/not_exist.txt");
  2. 对于严重异常(如FileNotFoundException),终止程序;
  3. 对于非严重异常(如编码检测失败),提示用户解决方案(如"请尝试使用UTF-8编码保存文件")。

这种设计既保证了程序的稳定性,又为用户提供了明确的问题解决方向,提升了系统易用性。

posted @ 2025-09-22 22:30  vemou  阅读(17)  评论(0)    收藏  举报