SpringBoot DFA實(shí)現(xiàn)敏感詞過濾功能
前言
敏感詞過濾系統(tǒng)已經(jīng)成為各大平臺(tái)的必備功能。無(wú)論是社交平臺(tái)的內(nèi)容審核、電商系統(tǒng)的商品管理,還是游戲系統(tǒng)的聊天監(jiān)控,都需要高效可靠的敏感詞過濾機(jī)制來(lái)維護(hù)健康的內(nèi)容生態(tài)。
傳統(tǒng)的字符串查找方式在處理大量敏感詞時(shí)性能急劇下降,而正則表達(dá)式在匹配復(fù)雜規(guī)則時(shí)更是捉襟見肘。
今天,介紹一種基于 DFA(有限狀態(tài)自動(dòng)機(jī))算法的高效敏感詞過濾方案,通過 Trie 樹數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)毫秒級(jí)響應(yīng),輕松應(yīng)對(duì)敏感內(nèi)容的實(shí)時(shí)過濾需求。
為什么需要 DFA 算法
傳統(tǒng)方案的性能瓶頸
在敏感詞過濾的實(shí)際應(yīng)用中,我們面臨著諸多挑戰(zhàn)。傳統(tǒng)的實(shí)現(xiàn)方式往往存在以下問題:
暴 力匹配的低效
// 時(shí)間復(fù)雜度:O(n × m × k)
// 隨著敏感詞數(shù)量增加,性能急劇下降
for (String word : sensitiveWords) {
if (text.contains(word)) {
// 處理敏感詞
}
}
這種簡(jiǎn)單粗暴的方式在小規(guī)模應(yīng)用中尚可接受,但當(dāng)敏感詞數(shù)量達(dá)到數(shù)千甚至數(shù)萬(wàn)時(shí),處理一篇1000字的文章可能需要數(shù)秒時(shí)間,嚴(yán)重影響用戶體驗(yàn)。
正則表達(dá)式的局限性 雖然正則表達(dá)式提供了強(qiáng)大的模式匹配能力,但在敏感詞過濾場(chǎng)景中卻存在明顯缺陷:
- 構(gòu)建超大的正則表達(dá)式會(huì)導(dǎo)致編譯時(shí)間過長(zhǎng)
- 動(dòng)態(tài)添加敏感詞需要重新編譯正則表達(dá)式
- 內(nèi)存占用隨敏感詞數(shù)量線性增長(zhǎng)
- 匹配效率在復(fù)雜規(guī)則下大幅下降
DFA 算法的優(yōu)勢(shì)
DFA 算法通過構(gòu)建狀態(tài)機(jī)模型,將敏感詞匹配過程轉(zhuǎn)化為狀態(tài)轉(zhuǎn)移,帶來(lái)了質(zhì)的飛躍:
- 線性時(shí)間復(fù)雜度: O(n) - 只需遍歷文本一次,不受敏感詞數(shù)量影響
- 空間共享優(yōu)化: 敏感詞前綴共享存儲(chǔ),顯著減少內(nèi)存占用
- 確定性匹配: 無(wú)需回溯,避免正則表達(dá)式的回溯開銷
- 動(dòng)態(tài)擴(kuò)展友好: 支持運(yùn)行時(shí)添加、刪除敏感詞,無(wú)需重建整個(gè)數(shù)據(jù)結(jié)構(gòu)
以實(shí)際場(chǎng)景為例,當(dāng)敏感詞庫(kù)從1000個(gè)擴(kuò)展到10000個(gè)時(shí):
- 暴力匹配算法耗時(shí)增加約10倍
- DFA算法耗時(shí)幾乎保持不變
DFA 算法原理解析
DFA 算法的數(shù)學(xué)本質(zhì)
DFA(Deterministic Finite Automaton)是一種數(shù)學(xué)模型,它定義了一個(gè)五元組 M = (Q, Σ, δ, q?, F):
- Q: 有限狀態(tài)集合
- Σ: 輸入字母表
- δ: 狀態(tài)轉(zhuǎn)移函數(shù) δ: Q × Σ → Q
- q?: 初始狀態(tài)
- F: 接受狀態(tài)集合
在敏感詞過濾中,每個(gè)狀態(tài)代表匹配到某個(gè)字符位置的狀態(tài)轉(zhuǎn)移路徑。
Trie 樹的可視化理解
Trie 樹是 DFA 的直觀實(shí)現(xiàn),也稱為字典樹或前綴樹。讓我們通過一個(gè)具體例子來(lái)理解:
假設(shè)有以下敏感詞:["apple", "app", "application", "apply", "orange"]
構(gòu)建的 Trie 樹結(jié)構(gòu)如下:
root
├── a
│ └── p
│ └── p [結(jié)束] ← "app"
│ └── l
│ ├── e [結(jié)束] ← "apple"
│ ├── i
│ │ └── c
│ │ └── a
│ │ └── t
│ │ └── i
│ │ └── o
│ │ └── n [結(jié)束] ← "application"
│ └── y [結(jié)束] ← "apply"
└── o
└── r
└── a
└── n
└── g
└── e [結(jié)束] ← "orange"
Trie 樹結(jié)構(gòu)的關(guān)鍵特征:
1. 前綴共享優(yōu)化:
- "app" 作為 "apple"、"application"、"apply" 的共同前綴,只存儲(chǔ)一次
- 從 "app" 節(jié)點(diǎn)分支出不同的后續(xù)字符路徑
2. 狀態(tài)轉(zhuǎn)移路徑:
- 每個(gè)
├──和└──表示一個(gè)字符狀態(tài)轉(zhuǎn)移 [結(jié)束]標(biāo)記表示 DFA 的接受狀態(tài)(敏感詞結(jié)束)
3. 空間效率:
- 5個(gè)敏感詞只需要存儲(chǔ) 10 個(gè)字符節(jié)點(diǎn)(含重復(fù)字符)
- 如果單獨(dú)存儲(chǔ)需要 5 + 3 + 11 + 4 + 6 = 29 個(gè)字符
4. 查找效率:
- 查找 "application" 只需要 11 次字符比較
- 查找 "app" 只需要 3 次字符比較且可以提前終止
關(guān)鍵特征解釋
1. 前綴共享: "app" 作為 "apple"、"application"、"apply" 的前綴,只在樹中存儲(chǔ)一次
2. 節(jié)點(diǎn)狀態(tài): 每個(gè)節(jié)點(diǎn)代表 DFA 的一個(gè)狀態(tài)
3. 邊轉(zhuǎn)移: 每條邊代表一個(gè)字符輸入引起的狀態(tài)轉(zhuǎn)移
4. 接受狀態(tài): 標(biāo)記 * 的節(jié)點(diǎn)表示敏感詞的結(jié)束狀態(tài)(接受狀態(tài))
例如,當(dāng)檢測(cè)文本 "I love apples" 時(shí):
- 從根節(jié)點(diǎn)開始,遇到 'a' 轉(zhuǎn)移到 a 節(jié)點(diǎn)
- 遇到 'p' 轉(zhuǎn)移到 p 節(jié)點(diǎn)
- 遇到 'p' 轉(zhuǎn)移到 p 節(jié)點(diǎn)
- 遇到 'l' 轉(zhuǎn)移到 l 節(jié)點(diǎn)
- 遇到 'e' 轉(zhuǎn)移到 e 節(jié)點(diǎn)(到達(dá)接受狀態(tài),發(fā)現(xiàn)敏感詞 "apple")
- 繼續(xù)檢測(cè) 's' 時(shí)狀態(tài)轉(zhuǎn)移失敗,回到初始位置繼續(xù)檢測(cè)
狀態(tài)轉(zhuǎn)移的實(shí)際過程
假設(shè)我們要在文本 "I like apples and apps" 中查找敏感詞:
- 從根節(jié)點(diǎn)開始,遇到 'a',轉(zhuǎn)移到 'a' 節(jié)點(diǎn)
- 遇到 'p',轉(zhuǎn)移到 'p' 節(jié)點(diǎn)
- 遇到 'p',轉(zhuǎn)移到 'p' 節(jié)點(diǎn)(此時(shí)檢測(cè)到 "app" 是敏感詞)
- 遇到 'l',轉(zhuǎn)移到 'l' 節(jié)點(diǎn)
- 遇到 'e',轉(zhuǎn)移到 'e' 節(jié)點(diǎn)(此時(shí)檢測(cè)到 "apple" 是敏感詞)
整個(gè)過程只需要一次線性遍歷,無(wú)需重復(fù)掃描或回溯。
核心實(shí)現(xiàn)詳解
1. Trie 節(jié)點(diǎn)結(jié)構(gòu)設(shè)計(jì)
Trie 節(jié)點(diǎn)是整個(gè) DFA 算法的基礎(chǔ),每個(gè)節(jié)點(diǎn)代表一個(gè)狀態(tài):
public class TrieNode {
// 子節(jié)點(diǎn)映射:字符 -> Trie節(jié)點(diǎn)
private Map<Character, TrieNode> children = new HashMap<>();
// 是否為敏感詞的結(jié)束節(jié)點(diǎn)
private boolean isEnd = false;
// 完整敏感詞內(nèi)容(便于輸出)
private String keyword;
public TrieNode getChild(char c) {
return children.get(c);
}
public TrieNode addChild(char c) {
return children.computeIfAbsent(c, k -> new TrieNode());
}
public boolean hasChild(char c) {
return children.containsKey(c);
}
// getters and setters...
}
2. DFA 過濾器核心實(shí)現(xiàn)
以下是最精簡(jiǎn)的 DFA 過濾器實(shí)現(xiàn),包含了核心的匹配邏輯:
public class SensitiveWordFilter {
private TrieNode root;
private int minWordLength = 1;
public SensitiveWordFilter(List<String> sensitiveWords) {
this.root = buildTrie(sensitiveWords);
this.minWordLength = sensitiveWords.stream()
.mapToInt(String::length).min().orElse(1);
}
/**
* 構(gòu)建 Trie 樹
*/
private TrieNode buildTrie(List<String> words) {
TrieNode root = new TrieNode();
for (String word : words) {
TrieNode node = root;
for (char c : word.toCharArray()) {
node = node.addChild(c);
}
node.setEnd(true);
node.setKeyword(word);
}
return root;
}
/**
* 檢查是否包含敏感詞 - 核心 DFA 匹配算法
*/
public boolean containsSensitiveWord(String text) {
if (text == null || text.length() < minWordLength) {
return false;
}
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (dfaMatch(chars, i)) {
return true;
}
}
return false;
}
/**
* DFA 狀態(tài)轉(zhuǎn)移匹配
*/
private boolean dfaMatch(char[] chars, int start) {
TrieNode node = root;
for (int i = start; i < chars.length; i++) {
char c = chars[i];
if (!node.hasChild(c)) {
break; // 狀態(tài)轉(zhuǎn)移失敗
}
node = node.getChild(c);
if (node.isEnd()) {
return true; // 到達(dá)接受狀態(tài)
}
}
return false;
}
/**
* 查找并替換敏感詞
*/
public String filter(String text, String replacement) {
List<SensitiveWordResult> words = findAllWords(text);
// 從后往前替換,避免索引變化問題
StringBuilder result = new StringBuilder(text);
for (int i = words.size() - 1; i >= 0; i--) {
SensitiveWordResult word = words.get(i);
String stars = String.valueOf(replacement != null ? replacement : "*")
.repeat(word.getEnd() - word.getStart() + 1);
result.replace(word.getStart(), word.getEnd() + 1, stars);
}
return result.toString();
}
/**
* 查找所有敏感詞
*/
public List<SensitiveWordResult> findAllWords(String text) {
List<SensitiveWordResult> results = new ArrayList<>();
if (text == null || text.length() < minWordLength) {
return results;
}
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
TrieNode node = root;
int j = i;
while (j < chars.length && node.hasChild(chars[j])) {
node = node.getChild(chars[j]);
j++;
if (node.isEnd()) {
results.add(new SensitiveWordResult(
text.substring(i, j), i, j - 1));
}
}
}
return results;
}
}
3. 敏感詞結(jié)果封裝
public class SensitiveWordResult {
private String word; // 敏感詞內(nèi)容
private int start; // 起始位置
private int end; // 結(jié)束位置
public SensitiveWordResult(String word, int start, int end) {
this.word = word;
this.start = start;
this.end = end;
}
// getters and toString...
}
實(shí)戰(zhàn)應(yīng)用場(chǎng)景
即時(shí)通訊系統(tǒng)中的實(shí)時(shí)過濾
在高并發(fā)的聊天系統(tǒng)中,敏感詞過濾需要滿足低延遲、高吞吐的要求:
public class ChatMessageFilter {
private SensitiveWordFilter wordFilter;
// 異步處理敏感詞檢測(cè)
private ExecutorService filterExecutor = Executors.newFixedThreadPool(10);
public CompletableFuture<Message> filterMessageAsync(Message message) {
return CompletableFuture.supplyAsync(() -> {
String content = message.getContent();
if (wordFilter.containsSensitiveWord(content)) {
// 實(shí)時(shí)替換
String filtered = wordFilter.filter(content, "***");
message.setContent(filtered);
// 記錄敏感詞統(tǒng)計(jì)
recordSensitiveWords(content);
}
return message;
}, filterExecutor);
}
private void recordSensitiveWords(String content) {
List<SensitiveWordResult> words = wordFilter.findAllWords(content);
// 統(tǒng)計(jì)敏感詞出現(xiàn)頻率,用于優(yōu)化詞庫(kù)
updateWordFrequency(words);
}
}
內(nèi)容審核系統(tǒng)的多級(jí)策略
不同類型的敏感詞需要不同的處理策略:
public class ContentAuditor {
private SensitiveWordFilter highRiskFilter; // 高風(fēng)險(xiǎn)詞
private SensitiveWordFilter mediumRiskFilter; // 中風(fēng)險(xiǎn)詞
private SensitiveWordFilter lowRiskFilter; // 低風(fēng)險(xiǎn)詞
public AuditResult auditContent(String content) {
AuditResult result = new AuditResult();
// 按風(fēng)險(xiǎn)級(jí)別檢測(cè)
List<SensitiveWordResult> highRiskWords = highRiskFilter.findAllWords(content);
if (!highRiskWords.isEmpty()) {
result.setStatus(AuditStatus.REJECT);
result.setReason("包含高風(fēng)險(xiǎn)敏感詞");
return result;
}
List<SensitiveWordResult> mediumRiskWords = mediumRiskFilter.findAllWords(content);
if (!mediumRiskWords.isEmpty()) {
result.setStatus(AuditStatus.MANUAL_REVIEW);
result.setReason("包含中風(fēng)險(xiǎn)敏感詞,需要人工審核");
return result;
}
List<SensitiveWordResult> lowRiskWords = lowRiskFilter.findAllWords(content);
if (!lowRiskWords.isEmpty()) {
// 低風(fēng)險(xiǎn)詞匯直接過濾
String filtered = lowRiskFilter.filter(content, "***");
result.setFilteredContent(filtered);
result.setStatus(AuditStatus.PASS_WITH_FILTER);
} else {
result.setStatus(AuditStatus.PASS);
}
return result;
}
}
動(dòng)態(tài)詞庫(kù)管理
實(shí)際項(xiàng)目中,敏感詞庫(kù)需要?jiǎng)討B(tài)更新:
@Service
public class SensitiveWordManager {
private volatile SensitiveWordFilter filter;
private ScheduledExecutorService updateExecutor =
Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init() {
loadWords();
// 定期更新詞庫(kù)
updateExecutor.scheduleAtFixedRate(this::loadWords, 0, 1, TimeUnit.HOURS);
}
public void loadWords() {
try {
// 從數(shù)據(jù)庫(kù)或配置中心加載最新詞庫(kù)
List<String> words = fetchLatestWords();
SensitiveWordFilter newFilter = new SensitiveWordFilter(words);
this.filter = newFilter;
log.info("敏感詞庫(kù)更新完成,當(dāng)前詞數(shù):{}", words.size());
} catch (Exception e) {
log.error("詞庫(kù)更新失敗", e);
}
}
public boolean containsSensitiveWord(String text) {
return filter != null && filter.containsSensitiveWord(text);
}
public String filterText(String text) {
return filter != null ? filter.filter(text, "***") : text;
}
}
高級(jí)優(yōu)化方案
對(duì)于不同規(guī)模的敏感詞過濾需求,基礎(chǔ)的 DFA 算法可能需要進(jìn)一步優(yōu)化。以下是幾種常用的高級(jí)方案:
方案1:雙數(shù)組 Trie(Double-Array Trie)
核心思想:將 Trie 樹壓縮為兩個(gè)數(shù)組,減小內(nèi)存使用。
// 雙數(shù)組結(jié)構(gòu)示意 int[] base = new int[size]; // 狀態(tài)轉(zhuǎn)移基礎(chǔ)值 int[] check = new int[size]; // 狀態(tài)轉(zhuǎn)移檢查值 // 狀態(tài)轉(zhuǎn)移:next_state = base[current_state] + char_code // if check[next_state] == current_state: 轉(zhuǎn)移成功
適用場(chǎng)景:詞庫(kù)規(guī)模較大
優(yōu)點(diǎn):內(nèi)存占用減少 50%-80%
缺點(diǎn):構(gòu)建復(fù)雜度增加,動(dòng)態(tài)更新困難
應(yīng)用:搜索引擎、大型社交平臺(tái)的離線詞庫(kù)
方案2:AC 自動(dòng)機(jī)(Aho-Corasick)
核心思想:在 Trie 基礎(chǔ)上增加失敗指針,實(shí)現(xiàn)一次遍歷匹配多個(gè)模式。
public class AhoCorasickAutomaton {
private Node root = new Node();
// Trie 節(jié)點(diǎn)結(jié)構(gòu)
static class Node {
Map<Character, Node> children = new HashMap<>();
Node fail; // 失敗指針
List<String> output = new ArrayList<>(); // 輸出模式
}
// 構(gòu)建自動(dòng)機(jī)
public void build(List<String> patterns) {
// 1. 構(gòu)建 Trie 樹
for (String pattern : patterns) {
Node node = root;
for (char c : pattern.toCharArray()) {
node = node.children.computeIfAbsent(c, k -> new Node());
}
node.output.add(pattern);
}
// 2. 添加失敗指針
Queue<Node> queue = new LinkedList<>();
// 初始化根節(jié)點(diǎn)的子節(jié)點(diǎn)
for (Node child : root.children.values()) {
child.fail = root;
queue.add(child);
}
// BFS 構(gòu)建失敗指針
while (!queue.isEmpty()) {
Node current = queue.poll();
for (Map.Entry<Character, Node> entry : current.children.entrySet()) {
char c = entry.getKey();
Node child = entry.getValue();
// 找到失敗指針
Node failNode = current.fail;
while (failNode != null && !failNode.children.containsKey(c)) {
failNode = failNode.fail;
}
child.fail = (failNode == null) ? root : failNode.children.get(c);
child.output.addAll(child.fail.output);
queue.add(child);
}
}
}
// 搜索匹配
public List<MatchResult> search(String text) {
List<MatchResult> results = new ArrayList<>();
Node node = root;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
// 失敗指針跳轉(zhuǎn)
while (node != null && !node.children.containsKey(c)) {
node = node.fail;
}
node = (node == null) ? root : node.children.get(c);
// 輸出匹配結(jié)果
for (String pattern : node.output) {
results.add(new MatchResult(pattern, i - pattern.length() + 1, i));
}
}
return results;
}
// 匹配結(jié)果
static class MatchResult {
String pattern;
int start;
int end;
public MatchResult(String pattern, int start, int end) {
this.pattern = pattern;
this.start = start;
this.end = end;
}
}
}
適用場(chǎng)景:多模式匹配、日志分析
優(yōu)點(diǎn):可同時(shí)匹配多個(gè)敏感詞,無(wú)需多次遍歷
缺點(diǎn):空間復(fù)雜度較高,實(shí)現(xiàn)相對(duì)復(fù)雜
應(yīng)用:網(wǎng)絡(luò)安全、內(nèi)容審核系統(tǒng)
方案3:分片 + 布隆過濾器預(yù)篩選
核心思想:通過分片降低單機(jī)壓力,用布隆過濾器快速過濾明顯不包含敏感詞的文本。
// 分片處理示例
public class ShardedFilter {
// 模擬分片
private List<SensitiveWordFilter> shards;
private BloomFilter<String> preFilter;
public boolean containsSensitive(String text) {
// 布隆過濾器預(yù)篩選
if (!preFilter.mightContain(text)) {
return false; // 肯定不包含敏感詞
}
// 分片精確匹配
int shardIndex = text.hashCode() % shards.size();
return shards.get(shardIndex).containsSensitiveWord(text);
}
}
適用場(chǎng)景:高并發(fā)、大規(guī)模文本處理
優(yōu)點(diǎn):支持水平擴(kuò)展,預(yù)處理可過濾大量無(wú)效請(qǐng)求
缺點(diǎn):存在誤判概率,系統(tǒng)復(fù)雜度增加
應(yīng)用:彈幕系統(tǒng)、即時(shí)通訊、微服務(wù)架構(gòu)
總結(jié)
DFA 算法通過 Trie 樹結(jié)構(gòu),為敏感詞過濾提供了一個(gè)兼具效率和準(zhǔn)確性的解決方案。
在實(shí)際應(yīng)用中,需要根據(jù)具體的業(yè)務(wù)場(chǎng)景選擇合適的實(shí)現(xiàn)策略。
無(wú)論是追求極致性能的聊天系統(tǒng),還是注重準(zhǔn)確率的內(nèi)容審核平臺(tái),DFA 算法都能提供堅(jiān)實(shí)的基礎(chǔ)支撐。
倉(cāng)庫(kù)地址:https://github.com/yuboon/java-examples/tree/master/springboot-dfa
到此這篇關(guān)于SpringBoot DFA實(shí)現(xiàn)敏感詞過濾功能的文章就介紹到這了,更多相關(guān)SpringBoot敏感詞過濾內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot Validated失效的問題及解決思路
文章主要介紹了Java Bean Validation(JSR 303/JSR 349)和Hibernate Validator的基本用法,包括常用注解的使用、@Valid和@Validated注解的區(qū)別、如何自定義校驗(yàn)注解以及如何在Spring Boot中使用這些校驗(yàn)機(jī)制2026-01-01
使用IntelliJ IDEA 15和Maven創(chuàng)建Java Web項(xiàng)目(圖文)
本篇文章主要介紹了使用IntelliJ IDEA 15和Maven創(chuàng)建Java Web項(xiàng)目(圖文),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09
Spring集成webSocket頁(yè)面訪問404問題的解決方法
這篇文章主要介紹了Spring集成webSocket頁(yè)面訪問404問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12
SpringBoot中Redis自動(dòng)配置的介紹、原理和使用詳解
這篇文章主要介紹了SpringBoot中Redis自動(dòng)配置的介紹、原理和使用詳解,Redis是一種高性能的鍵值對(duì)存儲(chǔ)數(shù)據(jù)庫(kù),它支持多種數(shù)據(jù)結(jié)構(gòu)和豐富的功能,如緩存、消息隊(duì)列、計(jì)數(shù)器、分布式鎖等,需要的朋友可以參考下2023-08-08
spring?boot?3使用?elasticsearch?提供搜索建議的實(shí)例詳解
這篇文章主要介紹了spring?boot3使用elasticsearch提供搜索建議,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08

