噢!查重原來是這樣實現的啊!

2022-10-22 06:00:57

前言

專案中有一個查重的需求,就類似論文查重這種的需求,我的組長已經寫好了這個 Demo 了,我也挺感興趣的,所以也看了看是如何實現的,看完後,感慨一聲,噢!原來是這樣實現的啊!現在呢,就記錄下我從中學到的知識!

需求

輸入:需要查重的內容,通常是非常長的文字,對於論文來說,可能上萬字。

輸出:顯示重複的句子,將重複句子標紅,以及整體內容的重複率。

標紅是次要矛盾,查重是主要矛盾,需要先解決。

發揮想象

我們想象一下,純人工查重的辦法。工作人員拿到一篇論文,閱讀這篇論文(假設該工作人員的大腦是超強大腦,工作人員對論文庫中的論文非常熟悉,基本能倒背如流的程度),每閱讀一句就與大腦中的論文進行對比,如果發現重複的內容太多了(即重複的句子很多),那麼計算下重複的內容大概佔全文的多少,進而得出整篇論文的重複率。

很明顯,人工查重,效率肯定是不高的。

如何通過程式碼實現?

已有資源:

  • 一篇待查重的論文,假設論文內容兩萬字。
  • 論文資料庫中大量的論文資料,假設資料庫中的每篇論文的內容也兩萬字左右。

思路:將輸入的論文內容與論文資料庫中存在的論文內容進行一一對比。

思考:

  • 如何對比?是一句一句進行對比,還是一段一段的進行對比?
  • 對比的時候,如何才能說明對比的內容是重複的?也就是說判斷重複的標準是什麼?

接觸新領域:

自然語言處理(NLP),自然語言處理任務中,我們經常需要判斷兩篇檔案是否相似、計算兩篇檔案的相似程度。

文字相似度演演算法

對於如何說明對比的內容是重複的,那麼這裡就涉及到文字相似度演演算法了。通過查詢資料,我瞭解到文字相似度演演算法有挺多的。

掘金-如何計算兩個字串之間的文字相似度?

掘金-文字相似度計算之餘弦定理

下面我列舉了幾種:

  • Jaccard 相似度演演算法
  • Sorensen Dice 相似度係數
  • Levenshtein
  • 漢明距離(海明距離)(Hamming Distance)
  • 餘弦相似性

對於這些文字相似度的演演算法,主要就是對文字進行分詞,然後再對分好的詞進行相關的計算,得出兩個文字的相似度。

所以,對於兩個文字,計算相似度的思路是:分詞->通過某種演演算法計算得到相似度

斷句

當然,這些演演算法,都是兩個文字進行的,這兩個文字可以是句子,也可以是段落,還可以是超長文字。假設我們直接是超長文字,直接使用相似度演演算法去匹配相似度,那麼可能會誤判,畢竟超長文字,分詞出來的詞語,相同的數量肯定是很多的,所以重複性也就會越高。

所以,首先要解決的問題就是,對於超長的文字,我們該如何進行中文斷句?

經過了解,得知 BreakIterator 這個類可以完成這件事。

BreakIterator:https://docs.oracle.com/javase/7/docs/api/java/text/BreakIterator.html

CSDN-Java國際化:BreakIterator

分詞

分詞,將一個句子中的詞語進行劃分,分出有意義的詞語。這裡主要使用 IK 分詞器。

實現

準備工作

Maven依賴

<!-- IK分詞器 -->
<dependency>
    <groupId>com.janeluo</groupId>
    <artifactId>ikanalyzer</artifactId>
    <version>2012_u6</version>
</dependency>
<!-- 漢語言處理包 Han Natural Language Processing -->
<dependency>
    <groupId>com.hankcs</groupId>
    <artifactId>hanlp</artifactId>
    <version>portable-1.5.4</version>
</dependency>
<!-- 阿帕奇 集合工具 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>
<!-- 糊塗工具包 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.10</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

Sentence 類

把句子抽象出來,寫成一個 Sentence 類去代表句子。

@Data
public class Sentence {
    /**
     * 文字
     */
    private String text;
    
    /**
     * 相似度
     */
    private Double similar;
    
    /**
     * 是否重複,0否,1是,預設0,重複標準就是,當相似度大於60%時,就認為該句子是重複的
     */
    private Integer duplicatesState = 0;

    /**
     * 與該句子最相似的句子
     */
    private Sentence maxSimilarSentence;

    /**
     * 重複句子下標,可能存在多個重複句子,所以使用集合記錄
     */
    private List<Integer> duplicatesIndex = new ArrayList<>();
}

策略模式

由於這裡有多種演演算法,考慮可以使用策略模式,來選擇不同的演演算法實現。

public interface SimDegreeAlgorithm {

    /**
     * 計算兩個句子的相似度
     * @param a
     * @param b
     * @return double
     **/
    double getSimDegree(String a, String b);
}
/**
 * @author god23bin
 * @description Jaccard 相似度演演算法,集合的交集與集合的並集的比例.
 */
public class Jaccard implements SimDegreeAlgorithm {
    @Override
    public double getSimDegree(String a, String b) {

    }
}
/**
 * @author god23bin
 * @description 餘弦相似性演演算法
 * 怎麼用它來計算兩個字串之間的相似度呢?
 * 首先我們將字串向量化(向量就是並集中的每個字元在各自中出現的頻率),之後就可以在一個平面空間中,求出他們向量之間夾角的餘弦值即可。
 */
public class CosSim implements SimDegreeAlgorithm {
    @Override
    public double getSimDegree(String a, String b) {

    }
}
/**
 * @author god23bin
 * @description 相似度演演算法的策略
 */
public class SimDegreeStrategy {

    private SimDegreeAlgorithm simDegreeAlgorithm;

    public SimDegreeStrategy(SimDegreeAlgorithm simDegreeAlgorithm) {
        this.simDegreeAlgorithm = simDegreeAlgorithm;
    }

    public double getSimDegree(String a, String b) {
        return simDegreeAlgorithm.getSimDegree(a, b);
    }
}

本簡單實現中,將選擇使用餘弦相似性演演算法來作為文字相似度演演算法的實現。

斷句

寫一個工具類來實現斷句。簡單說明一下,如何通過 BreakIterator 這個類實現斷句。

  1. 呼叫 getSentenceInstance() 就可以獲取能判斷句子邊界的範例物件。
  2. 通過範例物件呼叫 setText() 方法設定需要判斷的句子字串。
  3. 通過範例物件呼叫 first()next() 方法判斷邊界點。
  4. 根據邊界點進行分割字串。
public class SentenceUtil {

    /**
     * 將長文字進行斷句
     * @param content 長文字
     * @return
     */
    public static List<Sentence> breakSentence(String content) {
        // 獲取範例物件
        BreakIterator iterator = BreakIterator.getSentenceInstance(Locale.CHINA);
        // 設定文字,待斷句的長文字
        iterator.setText(content);
        // 儲存斷好的句子
        List<Sentence> list = new ArrayList<>();
        // 斷句的邊界
        int firstIndex;
        int lastIndex = iterator.first();
        // lastIndex 不等於 -1 (BreakIterator.DONE的值為 -1),說明還沒斷完,還沒結束
        while (lastIndex != BreakIterator.DONE) {
            firstIndex = lastIndex;
            lastIndex = iterator.next();

            if (lastIndex != BreakIterator.DONE) {
                Sentence sentence = new Sentence();
                sentence.setText(content.substring(firstIndex, lastIndex));
                list.add(sentence);
            }
        }
        return list;
    }
}

分詞

寫一個工具類來實現分詞,使用 IK 分詞器對文字進行分詞。

public class IKUtil {

    /**
     * 以List的形式返回經過IK分詞器處理的文字分詞的結果
     * @param text 需要分詞的文字
     * @return
     */
    public static List<String> divideText(String text) {
        if (null == text || "".equals(text.trim())) {
            return null;
        }
        // 分詞結果集
        List<String> resultList = new ArrayList<>();
        // 文字串 Reader
        StringReader re = new StringReader(text);
        // 智慧分詞: 合併數詞和量詞,對分詞結果進行歧義判斷
        IKSegmenter ik = new IKSegmenter(re, true);
        // Lexeme 詞元物件
        Lexeme lex = null;
        try {
            // 分詞,獲取下一個詞元
            while ((lex = ik.next()) != null) {
                // 獲取詞元的文字內容,存入結果集中
                resultList.add(lex.getLexemeText());
            }
        } catch (IOException e) {
            System.out.println("分詞IO異常:" + e.getMessage());
        }
        return resultList;
    }
}

餘弦相似性演演算法

邏輯

整個演演算法的邏輯是這樣的,那麼我們一一實現。

@Override
public double getSimDegree(String a, String b) {
    if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
        return 0f;
    }
    // 將句子進行分詞

    // 計算句子中詞的詞頻

    // 向量化

    // a、b 一維向量

    // 分別計算三個引數,再結合公式計算

}

統計詞頻

分詞上面已經實現,那現在是需要對句子中分好的詞進行詞頻的統計,分詞工具返回的是一個 List<String> 集合,我們可以通過雜湊表對集合中的詞語的出現次數進行統計,就是我們要的詞頻了。

public static Map<String, Integer> getWordsFrequency(List<String> words) {
    Map<String, Integer> wordFrequency = new HashMap<>(16);
    // 統計詞的出現次數,即詞頻
    for (String word : words) {
        wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);
    }
    return wordFrequency;
}

向量化

向量化,我們看看 @呼延十 大佬是如何說的:

字串向量化怎麼做呢?我舉一個簡單的例子:

A: 呼延十二
B: 呼延二十三

他們的並集 [呼,延,二,十,三]

向量就是並集中的每個字元在各自中出現的頻率。
A 的向量:[1,1,1,1,0]
B 的向量:[1,1,1,1,1]

掘金-如何計算兩個字串之間的文字相似度?-餘弦相似性

所以

兩個句子是這樣的:
句子1:你笑起來真好看,像春天的花一樣!
句子2:你贊起來真好看,像夏天的陽光!

進行分詞,分詞結果及頻率:
[你, 笑起來, 真, 好看, 像, 春天, 的, 花, 一樣],出現頻率都是1
[你, 贊, 起來, 真, 好看, 像, 夏天, 的, 陽光],出現頻率都是1

它們的並集:
[你,笑起來,贊,起來,真,好看,像,春天,夏天,的,花,一樣,陽光]

它們的向量:
         [你,笑起來,贊,起來,真,好看,像,春天,夏天,的,花,一樣,陽光]
句子1向量:[1,   1,    0,  0,  1,  1,   1,  1,   0,   1, 1,   1,   0 ]
句子2向量:[1,   0,    1,  1,  1,  1,   1,  0,   1,   1, 1,   0,   1 ]

程式碼錶示:

// 向量化,先並集,然後遍歷在並集中對應詞語,在自己的分詞集合中對應詞語出現次數,組成的數就是向量
Set<String> union = new HashSet<>();
union.addAll(aWords);
union.addAll(bWords);
// a、b 一維向量
int[] aVector = new int[union.size()];
int[] bVector = new int[union.size()];
List<String> collect = new ArrayList<>(union);
for (int i = 0; i < collect.size(); ++i) {
    aVector[i] = aWordsFrequency.getOrDefault(collect.get(i), 0);
    bVector[i] = bWordsFrequency.getOrDefault(collect.get(i), 0);
}

計算餘弦相似度

最後,計算餘弦相似度,結合公式計算。

/**
 * 分別計算三個引數
 * @param aVec a 一維向量
 * @param bVec b 一維向量
 */
public static double similarity(int[] aVec, int[] bVec) {
    int n = aVec.length;
    double p1 = 0;
    double p2 = 0f;
    double p3 = 0f;
    for (int i = 0; i < n; i++) {
        p1 += (aVec[i] * bVec[i]);
        p2 += (aVec[i] * aVec[i]);
        p3 += (bVec[i] * bVec[i]);
    }
    p2 = Math.sqrt(p2);
    p3 = Math.sqrt(p3);
    // 結合公式計算
    return (p1) / (p2 * p3);
}

程式碼

CosSim

public class CosSim implements SimDegreeAlgorithm {

    /**
     * 計算兩個句子的相似度:餘弦相似度演演算法
     * @param a 句子1
     * @param b 句子2
     **/
    @Override
    public double getSimDegree(String a, String b) {
        if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
            return 0f;
        }
        // 將句子進行分詞
        List<String> aWords = IKUtil.divideText(a);
        List<String> bWords = IKUtil.divideText(b);
        // 計算句子中詞的詞頻
        Map<String, Integer> aWordsFrequency = getWordsFrequency(aWords);
        Map<String, Integer> bWordsFrequency = getWordsFrequency(bWords);
        // 向量化,先並集,然後遍歷在並集中對應詞語,在自己的分詞集合中對應詞語出現次數,組成的數就是向量
        Set<String> union = new HashSet<>();
        union.addAll(aWords);
        union.addAll(bWords);
        // a、b 一維向量
        int[] aVector = new int[union.size()];
        int[] bVector = new int[union.size()];
        List<String> collect = new ArrayList<>(union);
        for (int i = 0; i < collect.size(); ++i) {
            aVector[i] = aWordsFrequency.getOrDefault(collect.get(i), 0);
            bVector[i] = bWordsFrequency.getOrDefault(collect.get(i), 0);
        }
        // 分別計算三個引數,再結合公式計算
        return similarity(aVector, bVector);
    }

    public static Map<String, Integer> getWordsFrequency(List<String> words) {
        Map<String, Integer> wordFrequency = new HashMap<>(16);
        // 統計詞的出現次數,即詞頻
        for (String word : words) {
            wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);
        }
        return wordFrequency;
    }

    /**
     * 分別計算三個引數
     * @param aVec a 一維向量
     * @param bVec b 一維向量
     **/
    public static double similarity(int[] aVec, int[] bVec) {
        int n = aVec.length;
        double p1 = 0;
        double p2 = 0f;
        double p3 = 0f;
        for (int i = 0; i < n; i++) {
            p1 += (aVec[i] * bVec[i]);
            p2 += (aVec[i] * aVec[i]);
            p3 += (bVec[i] * bVec[i]);
        }
        p2 = Math.sqrt(p2);
        p3 = Math.sqrt(p3);
        // 結合公式計算
        return (p1) / (p2 * p3);
    }
}

回顧思考

思考:

  • 如何對比?是一句一句進行對比,還是一段一段的進行對比?
  • 對比的時候,如何才能說明對比的內容是重複的?也就是說判斷重複的標準是什麼?

通過文字相似度演演算法,我們可以得到兩個句子的相似度。那麼相似度多少,我們才能認為它重複了呢?這個就由我們來決定了,在這裡,當相似度達到60%以上,那麼就認為當前句子是重複的

現在,整體的查重邏輯應該是比較明瞭了:

我們可以拿到長文字,對長文字進行斷句,得到句子集合,將這個句子集合與資料庫中的資料(也進行斷句,得到句子集合)進行相似度計算,記錄相似度大於標準的句子,即記錄重複句子及重複句子的數量,這樣我們就能夠判斷,這長文字裡面到底有多少個句子是重複的,進而得出重複率

分析文字工具類

我們可以再封裝一下,寫一個分析文字工具類 AnalysisUtil

public class AnalysisUtil {
    
    public static BigDecimal getAnalysisResult(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
        int simSentenceCnt = getSimSentenceCnt(sentencesA, sentencesB, algorithm);
        BigDecimal analysisResult = null;
        if (CollectionUtil.isNotEmpty(sentencesA)) {
            analysisResult = BigDecimal.valueOf((double) simSentenceCnt / sentencesA.size()).setScale(4, BigDecimal.ROUND_HALF_UP);
        } else {
            analysisResult = new BigDecimal(0);
        }
        return analysisResult;
    }
    
    /**
     * 返回 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的過程中,通過對 A 中資料進行相關操作實現)。
     * @param sentencesA 原始文字集合,即斷好的句子集合
     * @param sentencesB 模式文字集合,即斷好的句子集合
     * @param algorithm 相似度演演算法
     **/
    public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
        return null;
    }
}

計算相似的句子數量

    /**
     * 返回 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的過程中,通過對 A 中資料進行相關操作實現)。
     * @param sentencesA 原始文字集合,即斷好的句子集合
     * @param sentencesB 模式文字集合,即斷好的句子集合
     * @param algorithm 相似度演演算法
     **/
    public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
        // 當前句子相似度
        double simDegree = 0f;
        // 相似的句子數量
        int simSentenceCnt = 0;
        // 計算相似度的策略
        SimDegreeStrategy simDegreeStrategy = new SimDegreeStrategy(algorithm);
        for (Sentence sentence1 : sentencesA) {
            // 當前句子匹配到的最大的相似度
            double maxSimDegree = 0f;
            // 記錄 B 裡的,與 A 中最大相似度的那個句子
            Sentence temp = null;
            for (Sentence sentence2 : sentencesB) {
                // 計算相似度
                simDegree = simDegreeStrategy.getSimDegree(sentence1.getText(), sentence2.getText());
                // 列印資訊
                printSim(sentence1, sentence2, simDegree, algorithm);
                // 相似度大於60,認為文字重複
                if (simDegree * 100 > 60) {
                    sentence1.setDuplicatesState(1);
                    // 記錄該句子在 B 中的位置
                    sentence1.getDuplicatesIndex().add(sentencesB.indexOf(sentence2));
                }
                // 記錄最大的相似度
                if (simDegree * 100 > maxSimDegree) {
                    maxSimDegree = simDegree * 100;
                    temp = sentence2;
                }
            }
            // 如果當前句子匹配到的最大相似度是大於60%的,那麼說明該句子在 B 中至少有一個句子是相似的,即該句子是重複的
            if (maxSimDegree > 60) {
                ++simSentenceCnt;
            }
            sentence1.setSimilar(maxSimDegree);
            sentence1.setMaxSimilarSentence(temp);
        }
        return simSentenceCnt;
    }

完整程式碼

public class AnalysisUtil {

    /**
     * 計算出與專案庫內容重複的句子在當前內容下所佔的比例
     * @param sentencesA 待查重的句子集合
     * @param sentencesB 專案庫中的專案內容句子集合
     * @param algorithm 相似度演演算法
     * @return java.math.BigDecimal
     **/
    public static BigDecimal getAnalysisResult(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
        int simSentenceCnt = getSimSentenceCnt(sentencesA, sentencesB, algorithm);
        BigDecimal analysisResult = null;
        if (CollectionUtil.isNotEmpty(sentencesA)) {
            analysisResult = BigDecimal.valueOf((double) simSentenceCnt / sentencesA.size()).setScale(4, BigDecimal.ROUND_HALF_UP);
        } else {
            analysisResult = new BigDecimal(0);
        }
        return analysisResult;
    }

    /**
     * 根據相似度演演算法,分析句子集合,返回 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的過程中,通過對 A 中資料進行相關操作實現)。
     * @param sentencesA 原始文字集合,即斷好的句子集合
     * @param sentencesB 模式文字集合,即斷好的句子集合
     * @param algorithm 相似度演演算法
     * @return int
     **/
    public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
        // 當前句子相似度
        double simDegree = 0f;
        // 相似的句子數量
        int simSentenceCnt = 0;
        // 計算相似度的策略
        SimDegreeStrategy simDegreeStrategy = new SimDegreeStrategy(algorithm);
        for (Sentence sentence1 : sentencesA) {
            // 當前句子匹配到的最大的相似度
            double maxSimDegree = 0f;
            // 記錄 B 裡的,與 A 中最大相似度的那個句子
            Sentence temp = null;
            for (Sentence sentence2 : sentencesB) {
                // 計算相似度
                simDegree = simDegreeStrategy.getSimDegree(sentence1.getText(), sentence2.getText());
                // 列印資訊
                printSim(sentence1, sentence2, simDegree, algorithm);
                // 相似度大於60,認為文字重複
                if (simDegree * 100 > 60) {
                    sentence1.setDuplicatesState(1);
                    // 記錄該句子在 B 中的位置
                    sentence1.getDuplicatesIndex().add(sentencesB.indexOf(sentence2));
                }
                // 記錄最大的相似度
                if (simDegree * 100 > maxSimDegree) {
                    maxSimDegree = simDegree * 100;
                    temp = sentence2;
                }
            }
            // 如果當前句子匹配到的最大相似度是大於60的,那麼說明該句子在 B 中至少有一個句子是相似的,即該句子是重複的
            if (maxSimDegree > 60) {
                ++simSentenceCnt;
            }
            // 記錄句子的相似度以及與哪條相似
            sentence1.setSimilar(maxSimDegree);
            sentence1.setMaxSimilarSentence(temp);
        }
        return simSentenceCnt;
    }
    
    private static void printSim(Sentence sentence1, Sentence sentence2, double simDegree, SimDegreeAlgorithm algorithm) {
        BigDecimal bigDecimal = new BigDecimal(simDegree);
        DecimalFormat decimalFormat = new DecimalFormat("0.00%");
        String format = decimalFormat.format(bigDecimal);
        System.out.println("----------------------------------------------------------------");
        System.out.println(algorithm.getClass().getSimpleName());
        System.out.println("句子1:" + sentence1.getText());
        System.out.println("句子2:" + sentence2.getText());
        System.out.println("相似度:" + format);
    }
}

測試

測試兩個句子。

public static void testLogic() {
    String content = "你笑起來真好看,像春天的花一樣!";
    String t = "你贊起來真好看,像夏天的陽光!";
    List<Sentence> sentencesA = SentenceUtil.breakSentence(content);
    List<Sentence> sentencesB = SentenceUtil.breakSentence(t);
    BigDecimal analysisResult = AnalysisUtil.getAnalysisResult(sentencesA, sentencesB, new CosSim());
    System.out.println("重複率:" + analysisResult);
}

輸出結果:

句子1:你笑起來真好看,像春天的花一樣!
句子2:你贊起來真好看,像夏天的陽光!
相似度:55.56%
重複率:0.0000
public static void testLogic() {
    String content = "你笑起來真好看,像春天的花一樣!";
    String t = "你笑起來真好看,像夏天的花一樣!";
    List<Sentence> sentencesA = SentenceUtil.breakSentence(content);
    List<Sentence> sentencesB = SentenceUtil.breakSentence(t);
    BigDecimal analysisResult = AnalysisUtil.getAnalysisResult(sentencesA, sentencesB, new CosSim());
    System.out.println("相似度:" + analysisResult);
}

輸出結果:

句子1:你笑起來真好看,像春天的花一樣!
句子2:你笑起來真好看,像夏天的花一樣!
相似度:88.89%
重複率:1.0000

總結

思路:將輸入的論文內容與論文資料庫中存在的論文內容進行一一對比。

思考:

  • 如何對比?是一句一句進行對比,還是一段一段的進行對比?
  • 對比的時候,如何才能說明對比的內容是重複的?也就是說判斷重複的標準是什麼?

查重的基本思路就是,把待查重的內容進行短句,然後一條一條句子與資料庫中的進行對比,計算相似度。當然,這裡的實現是比較簡單粗暴的,兩層 for 迴圈,外層遍歷帶查重的句子,內層遍歷對比的句子,時間複雜度為 $O(n^2)$ 。

進一步的想法,就是使用多執行緒,這個後續再更新吧。

目前還沒想到還能如何進一步優化。如果螢幕前的你有什麼寶貴的建議或者想法,非常歡迎留下你的評論~~~

最後的最後

由本人水平所限,難免有錯誤以及不足之處, 螢幕前的靚仔靚女們 如有發現,懇請指出!

最後,謝謝你看到這裡,謝謝你認真對待我的努力,希望這篇部落格對你有所幫助!

你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!