第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS?page=2 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 完成个人编程作业编码部分 |
| 作业所在地址 | https://github.com/Parker-HJD/Parker-HJD/releases/tag/4207 |
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
| Development | 开发 | 300 | 360 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 40 |
| · Design Spec | · 生成设计文档 | 20 | 20 |
| · Design Review | · 设计复审 | 10 | 10 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 10 | 10 |
| · Design | · 具体设计 | 30 | 40 |
| · Coding | · 具体编码 | 150 | 180 |
| · Code Review | · 代码复审 | 20 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting | 报告 | 60 | 70 |
| · Test Report | · 测试报告 | 30 | 30 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 20 | 30 |
| 合计 | 380 | 460 |
二、计算模块接口的设计与实现
1. 整体结构
本项目只使用单个 Main 类,包含以下核心静态方法:
readFile(String path):读取文本文件内容,返回字符串;使用 try-with-resources 自动关闭文件流。getWords(String text):文本预处理,过滤非中英文字符,分词得到词语列表。calcHash(String word):对单个词语计算 64 位 hash 值。simHash(List<String> words):对词语列表生成 64 位 SimHash 指纹。hammingDistance(long hash1, long hash2):计算两个指纹之间的海明距离。calcSimilarity(long hash1, long hash2):由海明距离换算文本相似度百分比。main(String[] args):程序入口,读取命令行参数,调用各个模块,输出结果写入结果文件。
模块调用流程:
main入口 → 读取两个文本文件 → 文本分词预处理 → 生成两份 SimHash 指纹 → 计算海明距离 → 换算相似度 → 写入输出文件。
2. SimHash 算法关键
- 分词:把文档切分成词语,过滤符号、空白字符;
- 哈希:每个词语映射成 64 位无符号 hash;
- 加权累加:每一位,hash 为 1 则 + 1,为 0 则 - 1;
- 降维:累加后,大于 0 置 1,否则置 0,得到文档 64 位 SimHash 指纹;
- 相似度:比对两个指纹的海明距离,距离越小代表文本越相似。
3. 独到之处
- 使用
try-with-resources管理 IO,自动释放资源,避免文件句柄泄漏; - 正则预编译,只在类加载时编译一次,减少重复开销;
- 完善异常捕获,对文件缺失、空文件、参数数量错误等场景做容错;
- 模块化拆分,每个函数职责单一,方便单元测试。
三、计算模块性能改进
1. 性能改进耗时记录
性能分析与优化,实际耗时 40 分钟。
2. 改进思路
原始版本:getWords()中使用String.replaceAll(),每次调用都会重复编译正则表达式。长文本场景下,分词函数占用 CPU 最高,是性能瓶颈。
优化方案:把正则提取为static final Pattern静态常量,类加载阶段仅编译一次,后续直接复用 Matcher 对象,省去反复编译正则的开销。

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


五、模块部分异常处理说明
问题 1:NoSuchFileException 文件找不到
现象:运行程序抛出异常,提示找不到 txt 文件。
原因:代码里写的文件名和实际文件名不一致;文件放错目录,放在 src 里面而不是项目根目录;Windows 隐藏文件扩展名,实际后缀不对。
处理:修改代码里的文件名,统一为 test.txt;把 orig.txt、test.txt 放到和 pom.xml 同级的项目根目录;增加异常捕获,区分文件不存在和普通 IO 错误,给出中文提示。
问题 2:空指针 NullPointerException
现象:传入空文本,调用列表方法时报空指针崩溃。
原因:分词方法在文本为空时直接返回 null,后续遍历集合时报错。
处理:修改 splitWords,空输入时返回空 ArrayList,不返回 null;calcSimilarity 方法开头增加判空,list 为 null 或者空集合直接返回相似度 0。
问题 3:除零异常 ArithmeticException
现象:两段文本全为空,计算余弦相似度除法时报错。
原因:向量模长 magnitudeA 或 magnitudeB 等于 0,分母为 0。
处理:计算前判断模长,如果任意一方为 0,直接返回 0,跳过除法计算。
问题 4:词频平方计算整数溢出
现象:长文本词频很高时,相似度计算结果异常。
原因:int 相乘超出 int 范围,数值溢出变成负数。
处理:强转为 long 进行乘法计算 (long)countA * countA,避免溢出。
问题 5:单元测试用例失败
现象:JUnit 测试报错,断言不通过。
原因:分词正则只提取汉字,标点、数字字母全部丢弃,预期列表写错;文件读取用例依赖 orig.txt 存在。
处理:修正测试预期结果;新增专门的异常测试用例,校验找不到文件时是否抛出指定异常。
问题 6:最初余弦相似度逻辑错误
现象:两段相同文本算出来相似度不等于 1。
原因:循环遍历词频时,重复累加向量模长;点积计算逻辑写错。
处理:重构 calcSimilarity,分开计算点积、向量 A 模、向量 B 模,修正循环逻辑。
问题 7:命令行参数数组越界
现象:直接运行 main 方法,读取 args [] 时报数组越界。
原因:直接在 IDE 运行时没有传入命令行参数,args 数组为空。
处理:取消命令行参数读取,直接指定 orig.txt 和 test.txt 文件名。
六、总结以及改进计划
本项目基于余弦相似度实现文本查重功能,流程分为读取文本、中文单字分词、统计词频、向量计算、输出相似度。开发中先后解决文件读取、空指针、除零、整数溢出、单元测试校验等问题,搭配异常捕获与防御式编程提升程序健壮性。
异常处理采用先捕获具体异常、再捕获通用 IO 异常的策略;单元测试覆盖正常文本、相同文本、完全无关文本、空文本、文件不存在场景,验证核心函数逻辑正确性。程序最终可以读取 orig.txt 与 test.txt,输出保留四位小数的相似度结果。
现有不足
- 分词仅按单个汉字切割,不支持词语分词,语义能力弱,长句改写后查重结果误差大。
- 仅支持 txt 文件,不支持 docx、pdf 等常见文档格式。
- 没有停用词过滤,“的、是、在” 这类高频虚词会干扰相似度计算。
- 没有结果持久化,相似度只能控制台打印,不能自动保存到文件。
- 缺少命令行参数,文件路径写死在代码内,更换文件需要修改源码。
- 缺少日志,错误信息简单,不利于复杂场景调试。
改进方向
- 分词优化:引入中文分词库(如 HanLP),由单字切分改为词语切分,提升相似度计算贴合语义。
- 增加停用词表,过滤无意义虚词,减少噪声对向量的干扰。
- 扩展文件读取模块,支持更多文档类型;增加将相似度结果写入 result.txt 的功能。
- 支持命令行传入两个文件路径,不用修改代码即可更换待比对文本。
- 优化日志输出,区分普通信息与错误信息;增加进度提示。
- 可拓展功能:支持多文件批量查重、设置相似度阈值,当相似度超过阈值给出高重复提示。

浙公网安备 33010602011771号