
簡介一份基于Java的論文查重系統完整源碼包面向計算機相關專業學生、Java開發者與文本相似度檢測初學者。系統核心采用SimHash算法計算原文與待檢測論文之間的相似度輸出重復率結果支持文件輸入輸出和命令行參數指定路徑并包含性能分析與單元測試模塊。資源共65個文件體積約461KB主要包含11個Java源文件、11個class編譯產物、15個xml工程配置、10個txt測試語料、11張png示意圖及1個md說明文檔目錄結構清晰可導入IntelliJ IDEA直接運行。已有110人學習該項目可作為課程設計或畢業設計的參考實現幫助讀者掌握SimHash指紋去重、命令行交互、JUnit 4.12單元測試內置10余個用例以及JProfiler性能瓶頸定位等技能適合算法學習與工程實踐快速上手。1. 基于 Java 的論文查重系統真正值錢的不是相似度算法而是文本處理邊界我見過不少基于 Java 的論文查重系統源碼真正能拿自己的論文集跑一遍還不翻車的沒幾個。原因不是相似度算法寫得爛而是中文論文在變成向量之前很多人根本沒想清楚該切什么、濾什么、留什么。論文查重不是字符串找相同它要把復述、換詞、語序調整之后的句子也撈出來。這要求你先做中文分詞、抽 N-Gram 特征再算余弦相似度或 SimHash。這篇博文寫給打算自己搭一套查重工具的人不管是課程設計、公司內部文檔去重還是想理解商業查重系統的工作方式Java 生態里都有現成的分詞器可借力剩下的麻煩基本集中在工程邊界和參數上。2. Java 論文查重的地基文本預處理、N-Gram 與相似度算法選型2.1 中文分詞為什么繞不開字符串直接比對在論文場景下崩得很快論文正文是連續的中文文本“本文提出一種基于深度學習的檢測方法”在字符層面上沒有天然空格。直接對原字符串做包含匹配只能抓住逐字相同的片段只要作者把“提出”改成“給出了”字符匹配立刻失效。所以行業里的通用做法是先用中文分詞器把句子切成詞序列再在詞序列上抽特征。Java 生態里常見的選擇是 HanLP 或 IK Analyzer。下面這段代碼以 HanLP 的StandardTokenizer為例做最基礎的中文分詞并過濾掉停用詞import com.hankcs.hanlp.tokenizer.StandardTokenizer; import com.hankcs.hanlp.seg.common.Term; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; public class TextSplitter { private static final SetString STOP_WORDS new HashSet(Arrays.asList( 的, 了, 和, 在, 是, 我, 有, 與, 及, 等, 中, 也, 不 )); public static ListString tokenize(String text) { ListString result new ArrayList(); ListTerm terms StandardTokenizer.segment(text); for (Term term : terms) { String word term.word; if (word.length() 2) continue; // 過濾單字 if (STOP_WORDS.contains(word)) continue; // 過濾停用詞 result.add(word); } return result; } }這里有兩個參數值得說明word.length() 2會過濾掉“了”“的”這類單字但也會過濾掉英文單詞“AI”“RPC”這種有實義的兩字符縮寫。如果你處理的是計算機類論文建議把長度條件改成同時保留“非中文長度 2”的詞。停用詞表我習慣控制在 100 個以內只濾掉最沒區分度的虛詞停用詞過多會把“提出”“方法”這種論文高頻實義動詞也濾掉反而讓抄襲句變得看不見。2.2 三種相似度算法的選型對比余弦、SimHash、編輯距離有了詞序列后算相似度有三條常見路線余弦相似度、SimHash、編輯距離。它們的定位完全不同直接決定了系統能回答“哪一篇和哪一篇像”還是“這一段是不是抄了某一句”。算法特征粒度適合場景主要風險余弦相似度詞頻/特征向量段落級或全文級比對短文本特征稀疏結果不穩定SimHash64 位哈希指紋海量文檔粗篩長文特征被稀釋無法定位具體句編輯距離字符級標題、摘要、單句比較長文本算力開銷為 O(n*m)不可擴展論文查重源碼里一般會把 SimHash 和余弦相似度組合起來用。SimHash 先把文檔壓成一個只有 64 位的指紋把相似度比較變成漢明距離比較適合在幾十萬篇資料庫里先排除掉明顯無關的文檔。剩下候選對再用余弦相似度做精確復核。如果直接用編輯距離遍歷整個語料庫時間矩陣很快會撐爆內存。2.3 為什么還要 N-Gram換詞描述后連續碎片才是證據很多抄襲不是整句復制而是把句子主干換掉一半比如“本系統采用 Java 語言開發”改成“開發本系統時使用了 Java 技術”。分詞后兩組詞列表相似度可能只有 0.3但它們在字面連續片段的維度上有大量重疊。所以我會在詞特征之外疊加 N-Gram 特征。N-Gram 是把詞序列按固定窗口滑動的組合N2 叫 bigramN3 叫 trigram。以下代碼生成詞級別的 bigrampublic static SetString ngrams(ListString tokens, int n) { SetString grams new HashSet(); for (int i 0; i tokens.size() - n; i) { grams.add(String.join(, tokens.subList(i, i n))); } return grams; }用Set而不是List保存 N-Gram是因為同一句話里重復出現的“系統 提出”“系統 提出”只算一個特征避免因為一句話反復說“系統提出”把相似度拉高。N 的取值也很關鍵N2 時召回高少量無關文本也可能誤傷N3 更精確但對短句會丟失特征。論文正文我一般取 N2并把詞向量和 bigram 向量合并后一起參與余弦計算。3. 用 Java 手寫查重核心分詞、向量化與余弦相似度完整實現3.1 最小工程結構和 Maven 依賴拿到一個“基于 Java 的論文查重系統源碼”最關心的核心模塊其實只有三個部分文本讀取、特征提取、相似度計算。為了讓這個路徑可復現我建議工程結構拆成四個包reader負責讀文件vectorizer負責清洗和分詞similarity放余弦與 SimHashcli放命令行入口。Maven 里不用引入重量級搜索引擎只放一個分詞器依賴就夠了dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependencyportable-1.8.4是 HanLP 的一個輕量發布版自帶核心詞典不需要額外下載模型文件。如果你的項目想要更細顆粒度的自定義詞典可以換成帶data的完整版但那種版本體積大、啟動慢不適合放進課程設計或工具型源碼里。3.2 把文本清洗、分詞和向量生成封裝成一個工具類文本清洗不能只做一次trim()。論文都是 Word 文檔導出的文本里面有無處不在的換行、全角空格、彎引號、編號“1.2.3”、參考文獻里的[12]。這些噪聲不清理干凈后續跑出來的相似度會被大量無效字符拉偏。以下代碼把清洗、去停用詞、加 bigram 特征做成一個TextVectorizer工具類import com.hankcs.hanlp.HanLP; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class TextVectorizer { private static final SetString STOP_WORDS new HashSet(Arrays.asList( 的, 了, 和, 在, 是, 我, 有, 與, 及, 等, 中, 也, 不, 個, 我們, 他們, 通過, 進行, 都, 就 )); public static MapString, Integer vectorize(String text) { String normalized text.toLowerCase() .replaceAll([\\p{Punct}\\p{IsPunctuation}\\s], ); ListString tokens new ArrayList(); for (Term term : HanLP.segment(normalized)) { String word term.word; if (word.length() 2 || STOP_WORDS.contains(word)) { continue; } tokens.add(word); } MapString, Integer vec new HashMap(); for (String token : tokens) { vec.merge(token, 1, Integer::sum); } for (int i 0; i tokens.size() - 1; i) { String bigram tokens.get(i) tokens.get(i 1); vec.merge(bigram, 1, Integer::sum); } return vec; } }參數說明正則[\\p{Punct}\\p{IsPunctuation}\\s]會匹配中英文標點和所有空白符統一替換成單個空格。HanLP.segment在 2.x 版本里是標準入口返回分詞結果。vec.merge(key, 1, Integer::sum)等價于“有這個 key 就加 1沒有就放 1”用這一句代替了先containsKey再put的寫法。bigram 權重默認等同于詞權重如果發現查重結果對長句過于敏感可以把 bigram 的merge權重改成 0.5但要記得最后歸一化。3.3 余弦相似度用稀疏向量避免內存爆炸兩篇論文的特征向量如果都建模成完整的String[]一篇 5 萬字的論文特征數量可能到四五千直接做密集數組比較沒有問題。但當你把整個語料庫幾十萬篇兩兩比較時內存開銷就上去了。因此我一般用MapString, Integer表示稀疏向量只保存出現過的特征。余弦相似度的實現如下public class CosineSimilarity { public static double cosine(MapString, Integer v1, MapString, Integer v2) { if (v1.isEmpty() || v2.isEmpty()) return 0.0; MapString, Integer smaller v1.size() v2.size() ? v1 : v2; MapString, Integer larger smaller v1 ? v2 : v1; double dot 0; for (Map.EntryString, Integer entry : smaller.entrySet()) { Integer other larger.get(entry.getKey()); if (other ! null) { dot entry.getValue() * other; } } double norm norm(v1) * norm(v2); return norm 0 ? 0.0 : dot / Math.sqrt(norm); } private static double norm(MapString, Integer vector) { double sum 0; for (int value : vector.values()) { sum value * value; } return sum; } }這段代碼會先遍歷較小的向量通過larger.get(key)做常數級查詢。這樣點積計算次數是min(v1.size(), v2.size())而不是兩個特征集合的笛卡爾積。norm方法里沒有直接開方而是把兩個范數的乘積先算出來再統一開方減少一次Math.sqrt調用。這個差異在單次比較中無所謂但用 for 循環跑一萬對文檔時能省出可感知的時間。3.4 跑通最小命令行示例兩篇 txt 論文到底重多少有了向量化和余弦計算最小可運行入口就只剩下讀文件和打印結果。下面這個PaperChecker類接收兩個文本文件路徑輸出相似度分數和特征數量import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; public class PaperChecker { public static void main(String[] args) throws Exception { if (args.length 2) { System.out.println(用法: java PaperChecker 論文1.txt 論文2.txt); return; } String text1 Files.readString(Path.of(args[0])); String text2 Files.readString(Path.of(args[1])); MapString, Integer v1 TextVectorizer.vectorize(text1); MapString, Integer v2 TextVectorizer.vectorize(text2); double score CosineSimilarity.cosine(v1, v2); System.out.printf(相似度: %.4f%n, score); System.out.println(特征數量: v1.size() / v2.size()); } }編譯并運行mvn -q compile exec:java \ -Dexec.mainClasscom.example.checker.PaperChecker \ -Dexec.argsdoc1.txt doc2.txt如果手邊沒有 Maven也可以先mvn dependency:copy-dependencies拿到 HanLP 的 jar然后用javac -cp直接編譯運行。看到輸出相似度: 0.0000和特征數量: 0 / 0時先檢查文本文件編碼是不是 UTF-8以及是否全是難以讀入的 PDF 復制亂碼。這個最小系統已經能回答“兩篇文章像不像”了但要扔進真實論文場景還需要處理閾值、性能和漏報。4. Java 論文查重的工程化閾值設置、SimHash 分桶與誤判排錯4.1 查重閾值怎么定才不會被論文答辯懟回來查重系統輸出一個 0 到 1 的相似度分數后緊接著的問題就是“多高算抄襲”。這個閾值不能照搬商業系統因為商業平臺有獨立語料庫和語義模型而你的系統特征集合是自己定的。我一般按下面的經驗區間設置初值相似度區間判定建議處理動作0.00 - 0.20正常不提示0.20 - 0.40待觀察標黃人工抽查重合句子0.40 - 0.60中風險標紅生成詳細重復片段報告0.60 - 1.00高風險阻斷提交強制人工復核請注意這個表的前提是“同一學科領域的論文”。跨領域比較時“本文”“實驗”“結果分析”這些高頻詞會拉高基礎相似度所以閾值必須按目錄分組后分別校準。工程化上建議把結果落庫方便之后回看閾值調整前后的變化CREATE TABLE check_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, source_doc_id BIGINT NOT NULL, target_doc_id BIGINT NOT NULL, similarity DECIMAL(5,4) NOT NULL, decision VARCHAR(16) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );把每次查重的 source、target、score 和 decision 都存下來是調閾值最關鍵的一步。沒有歷史記錄你就無法回答“為什么上周這個分數還是綠這周變成黃了”。4.2 語料變大后別再兩兩比較SimHash 分桶與粗篩精算你的源碼如果只有幾千篇論文暴力兩兩比較還扛得住。但到幾萬篇甚至幾十萬篇時O(n^2)的比較次數會直接把 CPU 打滿。行業里普遍的做法是在余弦精算之前加一層 SimHash 粗篩。SimHash 的核心是先把特征映射成 64 位哈希按詞頻加權累加最后把正負號轉成 0/1 指紋public class SimHash { public static String hash(MapString, Integer vec) { int[] v new int[64]; for (Map.EntryString, Integer entry : vec.entrySet()) { long h entry.getKey().hashCode(); int weight entry.getValue(); for (int i 0; i 64; i) { int bit (int) ((h i) 1L); v[i] (bit 1) ? weight : -weight; } } StringBuilder sb new StringBuilder(); for (int i 0; i 64; i) { sb.append(v[i] 0 ? 1 : 0); } return sb.toString(); } }這段代碼里的是無符號右移避免hashCode()返回負數導致高位錯誤。實際生產環境我不會用 Java 自帶的hashCode它分布不夠均勻建議換 Guava 的Hashing.murmur3_32()。拿到 64 位指紋后把指紋切分 8 段每段作為一個桶的 key 存入HashMapString, ListDocFingerprint。查詢時拿新文檔的每一段去對應桶里找候選然后只對候選做余弦精算。這樣配合閾值能讓重查的規模從“全量兩兩比較”變成“桶內局部比較”。4.3 四個誤判來源參考文獻、代碼塊、表格和同義改寫真實論文場景里最影響口碑的往往不是算法不強而是誤判太蠢。我梳理過常見的四類問題誤判場景現象處理方式參考文獻不同論文的作者、年份、期刊名高度重合預處理階段剝離參考文獻段落或者單獨建引用庫代碼塊變量名、方法名相同但實現思路不同識別縮進或代碼圍欄跳過代碼區域表格數字結構相同、數值相似數值列單獨做歸一化不參與正文向量同義改寫“提出”改為“給出”整體語義相同疊加 N-Gram 與句法順序特征參考文獻誤判是新手最容易踩的坑。很多論文的參考文獻列表是從別的論文復制拼接的兩篇論文可能正文毫無關系參考文獻卻高度重合直接把相似度拉到 0.3 以上。一個簡單策略是正則匹配「參考文獻」章節把后面的段落單獨存到另一張表正文查重時不計入。代碼塊場景則適合用行首空格數和public、private、import這類特征判斷。如果一篇論文里貼著 Spring 的ApplicationContext另一篇也貼同一段那只能說明都在寫環境搭建不能證明正文抄襲。把這部分特征排除后分數才可信。5. 用“T 字回歸法”驗證 Java 查重系統改沒改壞查重系統最難維護的不是算法而是你每次調完停用詞、N-Gram 長度或閾值后沒法確定是改好了還是改壞了。我自己的做法是一個固定輸入、三條斷言的回歸測試被同事叫成“T 字回歸法”取一段 30 字左右的核心句子構造復制版、同義改寫版、無關版三份測試文本然后每次改動代碼后跑一遍確認三條關系不破public class RegressionCheck { public static void main(String[] args) { String src 本系統基于 Java 實現論文查重核心算法支持中文分詞與余弦相似度計算; String copy src; String rewrite 采用 Java 開發的論文查重程序使用中文分詞與余弦相似度完成核心計算; String unrelated 今天天氣很好適合出門跑步順便買一杯咖啡; double s1 CosineSimilarity.cosine( TextVectorizer.vectorize(src), TextVectorizer.vectorize(copy)); double s2 CosineSimilarity.cosine( TextVectorizer.vectorize(src), TextVectorizer.vectorize(rewrite)); double s3 CosineSimilarity.cosine( TextVectorizer.vectorize(src), TextVectorizer.vectorize(unrelated)); System.out.printf(copy%.4f rewrite%.4f unrelated%.4f%n, s1, s2, s3); assert s1 0.9 : 完全復制必須被識別; assert s2 0.8 : 同義改寫不能當成逐字復制; assert s3 0.2 : 無關文本分數不能過高; } }三條斷言的閾值來自我自己的經驗copy 必須高于 0.9rewrite 要低于 0.8unrelated 低于 0.2。如果你的系統跑完第一條不滿足說明特征提取被破壞或 N-Gram 過長第三條不滿足說明停用詞表太短或者“系統”“算法”這類高頻詞權重失控。把這組測試文本固定成三個文件丟進src/test/resources/regression/目錄每次跑mvn test時自動執行。如果你接手的源碼里已經有查重記錄表還可以再加一步“歷史回歸”找出上個月已經人工復核過的 50 對文檔把查重系統現在跑出來的分數和歷史人工結果做相關性對比。這樣能直觀看到閾值調整對“之前判定綠、現在判定黃”的文檔影響有多大再決定要不要全局重跑一遍。T 字回歸法的價值在于它把模糊的“感覺好多了”變成三個硬數字。每次改動分詞、停用詞或閾值后我只看copy是否保持高位、unrelated是否被壓下去就能在五分鐘內判斷這次改動是否安全。本文還有配套的精品資源點擊獲取