項目 | 內容 |
---|---|
這個作業屬於哪個課程 | (信息安全1812)的鏈接 |
這個作業要求在哪里 | (個人項目作業)作業要求的鏈接 |
這個作業的目標 | 設計一個論文查重算法,給出一個原文文件和一個在這份原文上經過了增刪改的抄襲版論文的文件,在答案文件中輸出其重復率。 |
1、前言
1.1、開發環境
-
編程語言:Java 14
-
IDE:Intellij IDEA 2020.1
-
項目構建工具:maven
-
單元測試:JUnit-4.12
-
性能分析工具:JProfiler 9.2
-
依賴的外部 jar 包:漢語言處理包
<dependency> <groupId>com.hankcs</groupId> <artifactId>hanlp</artifactId> <version>portable-1.5.4</version> </dependency>
1.1、整體流程
1.2、類
- MainPaperCheck:main 方法所在的類
- HammingUtils:計算海明距離的類
- SimHashUtils:計算 SimHash 值的類
- TxtIOUtils:讀寫 txt 文件的工具類
- ShortStringException:處理文本內容過短的異常類
1.3、核心算法
-
simhash+海明距離
-
具體可參考:
2、接口的設計和實現
2.1、讀寫 txt 文件的模塊
類:TxtIOUtils
包含了兩個靜態方法:
1、readTxt:讀取txt文件
2、writeTxt:寫入txt文件
實現:都是調用 Java.io 包提供的接口,比較簡單,這里省略。
2.2、SimHash 模塊(核心模塊)
類:SimHashUtils
包含了兩個靜態方法:
1、getHash:傳入String,計算出它的hash值,並以字符串形式輸出,(使用了MD5獲得hash值)
2、getSimHash:傳入String,計算出它的simHash值,並以字符串形式輸出,(需要調用 getHash 方法)
getSimHash 是核心算法,主要流程如下:
1、分詞(使用了外部依賴 hankcs 包提供的接口)
List<String> keywordList = HanLP.extractKeyword(str, str.length());//取出所有關鍵詞
2、獲取 hash 值
String keywordHash = getHash(keyword);
if (keywordHash.length() < 128) {
// hash值可能少於128位,在低位以0補齊
int dif = 128 - keywordHash.length();
for (int j = 0; j < dif; j++) {
keywordHash += "0";
}
}
3、加權、合並
for (int j = 0; j < v.length; j++) {
// 對keywordHash的每一位與'1'進行比較
if (keywordHash.charAt(j) == '1') {
//權重分10級,由詞頻從高到低,取權重10~0
v[j] += (10 - (i / (size / 10)));
} else {
v[j] -= (10 - (i / (size / 10)));
}
}
4、降維
String simHash = "";// 儲存返回的simHash值
for (int j = 0; j < v.length; j++) {
// 從高位遍歷到低位
if (v[j] <= 0) {
simHash += "0";
} else {
simHash += "1";
}
}
2.3、海明距離模塊
- 類:HammingUtils
包含了兩個靜態方法:
1、getHammingDistance:輸入兩個 simHash 值,計算出它們的海明距離 distance
for (int i = 0; i < simHash1.length(); i++) {
// 每一位進行比較
if (simHash1.charAt(i) != simHash2.charAt(i)) {
distance++;
}
}
2、getSimilarity:輸入兩個 simHash 值,調用 getHammingDistance 方法得出海明距離 distance,在由 distance 計算出相似度。
return 0.01 * (100 - distance * 100 / 128);
2.4、main 主模塊
- main 方法的主要流程:
- 從命令行輸入的路徑名讀取對應的文件,將文件的內容轉化為對應的字符串
- 由字符串得出對應的 simHash值
- 由 simHash值求出相似度
- 把相似度寫入最后的結果文件中
- 退出程序
3、接口部分的性能改進
3.1、性能分析
-
Overview
-
方法的調用情況
-
從分析圖可以看到:
調用次數最多的是com.hankcs.hanlp包提供的接口, 即分詞、取關鍵詞與計算詞頻花費了最多的時間。
所以在性能上基本沒有什么需要改進的。
4、單元測試
4.1、讀寫 txt 文件的模塊 的測試
-
基本思路:
1、測試正常讀取
2、測試正常寫入
3、測試錯誤讀取
4、測試錯誤寫入
public class TxtIOUtilsTest {
@Test
public void readTxtTest() {
// 路徑存在,正常讀取
String str = TxtIOUtils.readTxt("D:/test/orig.txt");
String[] strings = str.split(" ");
for (String string : strings) {
System.out.println(string);
}
}
@Test
public void writeTxtTest() {
// 路徑存在,正常寫入
double[] elem = {0.11, 0.22, 0.33, 0.44, 0.55};
for (int i = 0; i < elem.length; i++) {
TxtIOUtils.writeTxt(elem[i], "D:/test/ans.txt");
}
}
@Test
public void readTxtFailTest() {
// 路徑不存在,讀取失敗
String str = TxtIOUtils.readTxt("D:/test/none.txt");
}
@Test
public void writeTxtFailTest() {
// 路徑錯誤,寫入失敗
double[] elem = {0.11, 0.22, 0.33, 0.44, 0.55};
for (int i = 0; i < elem.length; i++) {
TxtIOUtils.writeTxt(elem[i], "User:/test/ans.txt");
}
}
}
-
測試結果:
-
代碼覆蓋率:
4.2、SimHash 模塊 的測試
public class SimHashUtilsTest {
@Test
public void getHashTest(){
String[] strings = {"余華", "是", "一位", "真正", "的", "作家"};
for (String string : strings) {
String stringHash = SimHashUtils.getHash(string);
System.out.println(stringHash.length());
System.out.println(stringHash);
}
}
@Test
public void getSimHashTest(){
String str0 = TxtIOUtils.readTxt("D:/test/orig.txt");
String str1 = TxtIOUtils.readTxt("D:/test/orig_0.8_add.txt");
System.out.println(SimHashUtils.getSimHash(str0));
System.out.println(SimHashUtils.getSimHash(str1));
}
}
-
測試結果:
-
代碼覆蓋率:
(SimHashUtils 的 getHash 方法的異常 catch 測試不了)
4.3、海明距離模塊 的測試
- 部分代碼:
public class HammingUtilsTest {
@Test
public void getHammingDistanceTest() {
String str0 = TxtIOUtils.readTxt("D:/test/orig.txt");
String str1 = TxtIOUtils.readTxt("D:/test/orig_0.8_add.txt");
int distance = HammingUtils.getHammingDistance(SimHashUtils.getSimHash(str0), SimHashUtils.getSimHash(str1));
System.out.println("海明距離:" + distance);
System.out.println("相似度: " + (100 - distance * 100 / 128) + "%");
}
}
-
測試結果:
-
代碼覆蓋率:
4.4、主測試 MainTest
部分測試代碼:
public class MainTest {
@Test
public void origAndAllTest(){
String[] str = new String[6];
str[0] = TxtIOUtils.readTxt("D:/test/orig.txt");
str[1] = TxtIOUtils.readTxt("D:/test/orig_0.8_add.txt");
str[2] = TxtIOUtils.readTxt("D:/test/orig_0.8_del.txt");
str[3] = TxtIOUtils.readTxt("D:/test/orig_0.8_dis_1.txt");
str[4] = TxtIOUtils.readTxt("D:/test/orig_0.8_dis_10.txt");
str[5] = TxtIOUtils.readTxt("D:/test/orig_0.8_dis_15.txt");
String ansFileName = "D:/test/ansAll.txt";
for(int i = 0; i <= 5; i++){
double ans = HammingUtils.getSimilarity(SimHashUtils.getSimHash(str[0]), SimHashUtils.getSimHash(str[i]));
TxtIOUtils.writeTxt(ans, ansFileName);
}
}
}
-
測試結果:
-
結果文件:
-
5、異常處理
5.1、設計與實現
當文本長度太短時,HanLp無法取得關鍵字,需要拋出異常。
try{
if(str.length() < 200) throw new ShortStringException("文本過短!");
}catch (ShortStringException e){
e.printStackTrace();
return null;
}
實現了一個處理這個異常的類:ShortStringException(繼承了Exception)
public ShortStringException(String message) {
super(message);
}
5.2、測試
-
測試結果:
6、PSP 表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 60 | 60 |
· Estimate | · 估計這個任務需要多少時間 | 60 | 60 |
Development | 開發 | 1120 | 1290 |
· Analysis | · 需求分析 (包括學習新技術) | 300 | 360 |
· Design Spec | · 生成設計文檔 | 70 | 60 |
· Design Review | · 設計復審 | 30 | 30 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 30 | 30 |
· Design | · 具體設計 | 90 | 90 |
· Coding | · 具體編碼 | 300 | 360 |
· Code Review | · 代碼復審 | 60 | 60 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 240 | 300 |
Reporting | 報告 | 240 | 250 |
· Test Repor | · 測試報告 | 60 | 70 |
· Size Measurement | · 計算工作量 | 60 | 60 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 120 | 120 |
· 合計 | 1420 | 1600 |