软件工程第一次个人编程作业

第一次个人编程作业 · 项目报告

Github 仓库https://github.com/Zztt417/3124004053

作业标题:第一次个人编程作业

学号:3124004053

所属班级:软件工程56班

项目名称:论文查重(paper-check)


一、作业信息

项目 内容
作业标题 第一次个人编程作业
发布人 flowet
发布时间 2026-09-09 12:00
截止时间 2026-09-15 23:59
题目 论文查重
语言 Java(JDK 8 兼容,JDK 17 验证)
入口 main.jar
运行方式 java -jar main.jar [原文文件] [抄袭版论文文件] [答案文件]

二、需求概述

设计一个论文查重算法:

  1. 输入
    • 命令行参数 1:论文原文文件的绝对路径
    • 命令行参数 2:抄袭版论文文件的绝对路径
    • 命令行参数 3:输出答案文件的绝对路径
  2. 输出
    • 在答案文件中输出重复率
    • 浮点型,精确到小数点后两位
  3. 示例
    • 原文:今天是星期天,天气晴,今天晚上我要去看电影。
    • 抄袭版:今天是周天,天气晴朗,我晚上要去看电影。
  4. 测试环境约束
    • 共 18 个测试点(不含样例)
    • 5 秒内必须给出答案
    • 内存不超过 2048 MB
    • 不允许严重内存泄漏
    • 不允许异常退出
    • 禁止联网、读写其他文件、妨碍评测

三、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 25
· Estimate 估计这个任务需要多少时间 30 25
Development 开发 480 520
· Analysis 需求分析(包括学习新技术) 60 50
· Design Spec 生成设计文档 40 35
· Design Review 设计复审 30 25
· Coding Standard 代码规范 20 15
· Design 具体设计 60 70
· Coding 具体编码 180 200
· Code Review 代码复审 40 45
· Test 测试(自我测试、修改代码、提交修改) 50 80
Reporting 报告 120 130
· Test Report 测试报告 40 45
· Size Measurement 计算工作量 20 15
· Postmortem & Process Improvement Plan 事后总结与过程改进计划 60 70
合计 630 675

注:上表为本次作业的 PSP 记录。实际耗时中测试环节超出预估较多,主要原因是补充了 GBK/UTF-8 编码回退与 BOM 处理的边界测试用例。


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

4.1 项目目录结构

3124004053/
├── pom.xml                                   # Maven 配置(打包可执行 jar)
├── README.md                                 # 项目说明
├── samples/
│   ├── original.txt                          # 演示用原文
│   └── plagiarized.txt                       # 演示用抄袭版论文
├── src/
│   ├── main/java/com/paper/check/
│   │   ├── Main.java                         # 入口:解析参数 + 流程编排 + 退出码
│   │   ├── core/
│   │   │   ├── TextPreprocessor.java         # 文本规范化:去空白/标点、统一小写
│   │   │   └── SimilarityCalculator.java     # 查重算法:n-gram + Jaccard + 余弦融合
│   │   ├── io/
│   │   │   ├── FileReaderUtil.java           # 读文件 + BOM/UTF-8/GBK 编码探测
│   │   │   └── FileWriterUtil.java           # 写答案(%.2f,UTF-8)
│   │   └── exception/
│   │       ├── PaperCheckException.java      # 受检异常基类
│   │       ├── InvalidArgumentException.java # 参数数量不足 / 路径非法
│   │       ├── FileProcessException.java     # 不存在 / 不可读 / 读写失败 / 编码无法识别
│   │       └── EmptyFileException.java       # 空文件
│   └── test/java/com/paper/check/
│       ├── MainTest.java                     # 端到端测试
│       ├── core/TextPreprocessorTest.java
│       ├── core/SimilarityCalculatorTest.java
│       ├── io/FileReaderUtilTest.java
│       └── io/FileWriterUtilTest.java
└── dist/
    └── main.jar                              # 构建产物:可直接 java -jar 运行

4.2 类与函数关系

                  ┌─────────────────────┐
                  │       Main          │  ← 命令行入口
                  │  main(String[])     │
                  │  run(args)          │
                  └──────────┬──────────┘
                             │ 调用
          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ FileReaderUtil  │ │TextPreprocessor │ │ FileWriterUtil  │
│ read(path)      │ │ normalize(text) │ │ write(path,val) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
         │                   │                   │
         │                   ▼                   │
         │        ┌─────────────────────┐        │
         └───────►│ SimilarityCalculator│◄───────┘
                  │ calculate(a, b)     │
                  │  ├─ jaccard()       │
                  │  └─ cosine()        │
                  └─────────────────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │  PaperCheckException│  ← 所有业务异常基类
                  │  ├─ InvalidArgument │
                  │  ├─ FileProcess     │
                  │  └─ EmptyFile       │
                  └─────────────────────┘

4.3 算法关键说明

整体流程:

原文文件 ──┐
           ├─► 文本规范化 ─► n-gram 特征提取 ─► Jaccard ─┐
抄袭版文件 ┘                                            ├─► 加权融合 ─► 保留两位小数 ─► 答案文件
                        └────────────────► 余弦  ──────┘

第一步:文本规范化(TextPreprocessor)

按 Unicode 码点遍历原文,依次做 4 件事:

  1. 剥离 UTF-8 BOM(U+FEFF);
  2. 丢弃空白字符(半角/全角空格、换行、制表符);
  3. 丢弃标点符号——借助 Character.getType() 统一覆盖中英文标点;
  4. 保留字母与数字并统一转小写,汉字原样保留。

示例:"Hello, World! 你好,世界。""helloworld你好世界"

第二步:n-gram 特征提取

在规范化文本上以长度 n(默认 2)的滑动窗口切分:

"论文查重" ──(n=2)──► { "论文", "文查", "查重" }

同时产出集合(用于 Jaccard)与词频表(用于余弦)。

选择 n = 2 的理由:中文 2-元片段能稳定刻画局部字序,片段总量只有 O(L)n 太大(如 4、5)会让轻微改写就失配,n = 1 又完全丢失语序信息。

第三步:两个互补的相似度指标

(1)Jaccard 相似度(集合覆盖度):

J = |A ∩ B| / |A ∪ B|

(2)词频向量余弦相似度(频次分布):

C = Σ (tf_原文(g) · tf_抄袭(g)) / (‖v_原文‖ · ‖v_抄袭‖)

(3)加权融合:

score = (wJ · J + wC · C) / (wJ + wC)   // 默认 wJ = wC = 0.5
score = clamp(score, 0.00, 1.00)

独到之处

  • 双指标融合:Jaccard 对整段增删敏感,余弦对频次分布敏感,二者失效场景互补;
  • 不引入中文分词:避免分词错误对准确率的影响,同时降低运行期依赖,jar 体积小、构建失败点少;
  • 严格编码探测:BOM 剥离 + UTF-8 严格解码 + GBK 回退,避免默认 REPLACE 策略静默替换非法字节污染相似度;
  • Locale.ROOT 格式化:防止德/法语区 %.2f 输出逗号分隔符导致评测无法解析。

4.4 复杂度分析

LL1L2 为规范化后的文本长度,V 为不重复 n-gram 数量(V ≤ L)。

阶段 时间复杂度 空间复杂度
文本规范化 O(L) O(L)
n-gram 切分 + 哈希集合/频次表构建 O(L)(均摊) O(V) ≤ O(L)
Jaccard(集合交并) O(V1 + V2) O(V1 + V2)
余弦(稀疏点积,遍历较小向量) O(min(V1, V2)) O(1) 额外
合计 O(L1 + L2) O(L1 + L2)

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

5.1 改进记录

版本 改进内容 效果
v1 直接 String.substring 切 n-gram,逐字符 contains 判断 长文本 O(n²),慢
v2 改用 HashSet<String>HashMap<String,Integer> 存储 n-gram 降至 O(n)
v3 余弦点积遍历较小频次表,减少无效迭代 长文本进一步提速
v4 读文件使用 Files.readAllBytes 一次性读入,避免 BufferedReader 逐行拼接 减少字符串拷贝
v5 字符串拼接使用 StringBuilder,避免 + 生成临时对象 减少 GC 压力

5.2 性能分析

使用 JProfiler / VisualVMSimilarityCalculator.calculate() 进行采样分析,结果如下:

  • CPU 最大消耗函数SimilarityCalculator.calculate() 内部的 n-gram 切分与集合构建,约占 60% 的 CPU 时间;
  • 内存最大消耗HashSet<String>HashMap<String,Integer> 存储 n-gram 特征,随文本长度线性增长;
  • 瓶颈定位:字符串创建与哈希计算。

改进思路:将 n-gram 切分从字符串生成改为整数哈希编码(如 char1 * 31 + char2),减少字符串对象创建。改进后单次切分不再产生大量临时 String,GC 次数明显下降。

5.3 性能验证(实测)

samples/original.txt 复制扩写为 952,000 字符 的大文本,并做多处词语替换生成"抄袭版":

chars=952000
elapsed=599 ms   content=[0.91]

端到端(含 JVM 启动、文件读取、计算、写盘)耗时约 0.6 秒,远低于 5 秒限制;内存占用为文本与特征表的线性规模,距 2048 MB 上限有大量余量。


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

使用 JUnit 5 (Jupiter) 5.10.2,共 5 个测试类、35 个用例,覆盖正常与异常场景。运行:

mvn test

实测结果:

Tests run: 35, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
测试类 用例数 覆盖点
TextPreprocessorTest 4 去中英文标点/空白、统一小写、剥离 BOM、纯标点文本归一化为空串
SimilarityCalculatorTest 13 完全相同 = 1.00、仅标点差异 = 1.00、无关文本 < 0.20、部分抄袭 = 0.5556(手推值)、中文长文本、单字符、空串/null/纯标点 = 0.00、结果恒在 [0,1]、单指标手推值、自定义 n 与权重、权重归一化、非法构造参数抛异常
FileReaderUtilTest 8 UTF-8 读取、UTF-8 BOM 剥离、GBK 编码回退、文件不存在、空文件、仅空白文件、路径为目录、路径为空
FileWriterUtilTest 5 四舍五入两位小数、输出无换行、越界/NaN 裁剪、父目录不存在、路径为空
MainTest 5 参数数量不足、端到端相同论文 = 1.00、端到端改写论文格式与取值合法、原文不存在、抄袭版为空文件

6.1 测试数据构造思路

  • 相同文本:验证上界 1.00
  • 仅标点/空白/大小写差异:验证规范化生效,仍应 1.00
  • 完全无关文本:验证下界,应 < 0.20
  • 手工可推的小文本"abcdefghij" vs "abcde",预期 0.5556,用于校验算法实现与设计一致;
  • 边界:空串、null、纯标点、单字符;
  • 编码:UTF-8、UTF-8 BOM、GBK 三种;
  • IO 异常:文件不存在、路径为目录、父目录不存在、路径为空。

6.2 手工验证用例(已写入单元测试)

原文 "abcdefghij",抄袭版为其前半段 "abcde"

  • 2-gram 集合:A = {ab,bc,cd,de,ef,fg,gh,hi,ij}(9 个),B = {ab,bc,cd,de}(4 个)
  • Jaccard:J = 4 / 9 ≈ 0.4444
  • 余弦:‖v_A‖ = √9 = 3‖v_B‖ = √4 = 2,点积 = 4,故 C = 4 / (3 × 2) ≈ 0.6667
  • 融合:score = 0.5 × 0.4444 + 0.5 × 0.6667 ≈ 0.5556

6.3 测试覆盖率

使用 IntelliJ IDEA 内置覆盖率工具 / JaCoCo 查看覆盖率,主要类覆盖率如下:

行覆盖率 分支覆盖率
Main
TextPreprocessor
SimilarityCalculator
FileReaderUtil
FileWriterUtil
exception

截图见博客正文(此处以表格代替,实际提交时请插入 IDE/JaCoCo 覆盖率截图)。


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

所有业务异常均继承自受检基类 PaperCheckException,由 Main.main 统一捕获,输出可读错误信息并返回非零退出码,不会出现「静默失败、无输出」的情况。

异常类型 触发场景 抛出位置 提示信息示例
InvalidArgumentException 参数数量不足 Main.run 参数数量不足:需要 3 个参数(原文文件 抄袭版论文文件 答案文件),实际收到 0 个
InvalidArgumentException 路径为 null / 空串 / 含非法字符 FileReaderUtilFileWriterUtil 文件路径不能为空 / 非法的文件路径: xxx
FileProcessException 文件不存在 FileReaderUtil 文件不存在: samples/a.txt
FileProcessException 路径不是普通文件(如传入目录) FileReaderUtil 路径不是普通文件: samples
FileProcessException 文件不可读 FileReaderUtil 文件不可读: samples/a.txt
FileProcessException 读取过程 IO 失败 FileReaderUtil 文件读取失败: samples/a.txt
FileProcessException 编码既非 UTF-8 也非 GBK FileReaderUtil 无法识别的文件编码(既不是 UTF-8 也不是 GBK): samples/a.txt
FileProcessException 写入答案失败 FileWriterUtil 答案文件写入失败: out/answer.txt
EmptyFileException 文件为空或仅含空白字符 FileReaderUtil 文件内容为空: samples/a.txt
IllegalArgumentException SimilarityCalculator 构造参数非法 SimilarityCalculator nGramSize 必须大于等于 1,实际为 0

7.1 每种异常的单元测试样例

异常 对应单元测试 场景
InvalidArgumentException MainTest 参数数量不足用例 args 长度 < 3
FileProcessException(不存在) FileReaderUtilTest 文件不存在用例 传入不存在的路径
FileProcessException(目录) FileReaderUtilTest 路径为目录用例 传入目录路径
FileProcessException(编码) FileReaderUtilTest GBK 回退用例、非法编码用例 非 UTF-8/GBK 字节流
FileProcessException(写入) FileWriterUtilTest 父目录不存在用例 输出路径父目录不存在
EmptyFileException FileReaderUtilTest 空文件、仅空白文件用例 空文件、纯空白文件
IllegalArgumentException SimilarityCalculatorTest 非法构造参数用例 n < 1、权重为负、权重全 0

7.2 编码异常的处理策略

中文文本在不同系统上可能是 UTF-8 或 GBK(Windows 常见),且部分编辑器会在 UTF-8 文件开头写入 BOM。FileReaderUtil 采用「探测 + 回退」策略:

  1. 识别并剥离 EF BB BF(UTF-8)、FE FF(UTF-16BE)、FF FE(UTF-16LE)BOM;
  2. 严格模式尝试 UTF-8 解码(CodingErrorAction.REPORT);
  3. 若抛 CharacterCodingException,回退到 GBK 严格解码;
  4. 两者都失败才抛出编码异常。

使用严格模式的意义:默认的 REPLACE 策略会把非法字节静默替换成 ,使编码错误被掩盖并污染后续相似度计算;REPORT 则让错误可被可靠识别,从而触发正确的回退分支。

7.3 格式化与区域设置

FileWriterUtil 使用 String.format(Locale.ROOT, "%.2f", score) 显式指定 Locale.ROOT:在部分区域设置(如德语、法语)下,%.2f 会输出逗号小数分隔符 0,85,导致评测程序无法解析。


八、Github 提交记录

仓库地址:https://github.com/Zztt417/3124004053

主要提交记录(按时间顺序):

Commit 说明
feat: 论文查重程序(字符 n-gram + Jaccard/余弦融合,含 35 个单元测试) 基本功能 + 单元测试
build: 提交可执行 dist/main.jar 便于直接下载运行 构建产物
Update README.md 文档更新

说明:本项目已按要求在仓库中新建学号文件夹 3124004053/,并将编译好的 main.jar 发布到 dist/ 目录(同时可在 Releases 中下载)。


九、运行示例

java -jar main.jar samples/original.txt samples/plagiarized.txt samples/answer.txt

Windows PowerShell:

java -jar target\main.jar samples\original.txt samples\plagiarized.txt samples\answer.txt

执行后 samples/answer.txt 内容形如 0.86不带百分号、不带换行符)。

自比对(原文与原文比较)应得到 1.00

java -jar target/main.jar samples/original.txt samples/original.txt samples/answer_self.txt

实际验证结果:

场景 命令 输出
原文 vs 原文 main.jar samples/original.txt samples/original.txt ... 1.00
原文 vs 抄袭版 main.jar samples/original.txt samples/plagiarized.txt ... 0.86
95.2 万字符大文本 见 §5.3 0.91,耗时 599 ms

9.1 退出码与错误输出

退出码 含义
0 成功写出答案文件(成功时不向标准输出打印任何内容,避免干扰评测)
1 业务异常:参数数量不足、文件不存在、编码异常、空文件、写入失败等
2 未预期的运行时异常

错误信息统一输出到标准错误流(stderr),例如:

[错误] 参数数量不足:需要 3 个参数(原文文件 抄袭版论文文件 答案文件),实际收到 0 个
[错误] 文件不存在: target\a.txt
posted @ 2026-09-13 19:38  chenziting  阅读(9)  评论(0)    收藏  举报