sensitive-word 基於DFA 算法實現的高性能敏感詞工具。
在線體驗
如果有一些疑難雜症,可以加入:技術交流群
sensitive-word-admin 是對應的控台的應用,目前功能處於初期開發中,MVP 版本可用。
大家好,我是老馬。
一直想實現一款簡單好用敏感詞工具,於是開源實現了這個工具。
基於DFA 算法實現,目前敏感詞庫內容收錄6W+(源文件18W+,經過一次刪減)。
後期將進行持續優化和補充敏感詞庫,並進一步提升算法的性能。
v0.24.0 開始內置支持對敏感詞的分類細化,不過工作量比較大,難免存在疏漏。
歡迎PR 改進, github 提需求,或者加入技術交流群溝通吹牛!
6W+ 詞庫,且不斷優化更新
基於fluent-api 實現,使用優雅簡潔
基於DFA 算法,性能為7W+ QPS,應用無感
支持敏感詞的判斷、返回、脫敏等常見操作
支持常見的格式轉換
全角半角互換、英文大小寫互換、數字常見形式的互換、中文繁簡體互換、英文常見形式的互換、忽略重複詞等
支持敏感詞檢測、郵箱檢測、數字檢測、網址檢測、IPV4等
支持自定義替換策略
支持用戶自定義敏感詞和白名單
支持數據的數據動態更新(用戶自定義),實時生效
支持敏感詞的標籤接口+內置分類實現
支持跳過一些特殊字符,讓匹配更靈活
支持黑白名單單個的新增/修改,無需全量初始化
CHANGE_LOG.md
有時候敏感詞有一個控台,配置起來會更加靈活方便。
java 如何實現開箱即用的敏感詞控台服務?
梳理了大量的敏感詞標籤文件,可以讓我們的敏感詞更加方便。
這兩個資料閱讀可在下方文章獲取:
v0.11.0-敏感詞新特性及對應標籤文件
目前v0.24.0 已內置實現單詞標籤,需要的建議升級到最新版本。
開源不易,如果本項目對你有幫助,你可以請老馬喝一杯奶茶。

JDK1.8+
Maven 3.x+
< dependency >
< groupId >com.github.houbb</ groupId >
< artifactId >sensitive-word</ artifactId >
< version >0.24.0</ version >
</ dependency >SensitiveWordHelper作為敏感詞的工具類,核心方法如下:
注意: SensitiveWordHelper提供的都是默認配置。如果你希望進行靈活的自定義配置,可參考引導類特性配置
| 方法 | 參數 | 返回值 | 說明 |
|---|---|---|---|
| contains(String) | 待驗證的字符串 | 布爾值 | 驗證字符串是否包含敏感詞 |
| replace(String, ISensitiveWordReplace) | 使用指定的替換策略替換敏感詞 | 字符串 | 返回脫敏後的字符串 |
| replace(String, char) | 使用指定的char 替換敏感詞 | 字符串 | 返回脫敏後的字符串 |
| replace(String) | 使用*替換敏感詞 | 字符串 | 返回脫敏後的字符串 |
| findAll(String) | 待驗證的字符串 | 字符串列表 | 返回字符串中所有敏感詞 |
| findFirst(String) | 待驗證的字符串 | 字符串 | 返回字符串中第一個敏感詞 |
| findAll(String, IWordResultHandler) | IWordResultHandler 結果處理類 | 字符串列表 | 返回字符串中所有敏感詞 |
| findFirst(String, IWordResultHandler) | IWordResultHandler 結果處理類 | 字符串 | 返回字符串中第一個敏感詞 |
| tags(String) | 獲取敏感詞的標籤 | 敏感詞字符串 | 返回敏感詞的標籤列表 |
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
Assert . assertTrue ( SensitiveWordHelper . contains ( text )); final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
String word = SensitiveWordHelper . findFirst ( text );
Assert . assertEquals ( "五星红旗" , word );SensitiveWordHelper.findFirst(text) 等價於:
String word = SensitiveWordHelper . findFirst ( text , WordResultHandlers . word ()); final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
List < String > wordList = SensitiveWordHelper . findAll ( text );
Assert . assertEquals ( "[五星红旗, 毛主席, 天安门]" , wordList . toString ());返回所有敏感詞用法上類似於SensitiveWordHelper.findFirst(),同樣也支持指定結果處理類。
SensitiveWordHelper.findAll(text) 等價於:
List < String > wordList = SensitiveWordHelper . findAll ( text , WordResultHandlers . word ());WordResultHandlers.raw() 可以保留對應的下標信息、類別信息:
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
// 默认敏感词标签为空
List < WordTagsDto > wordList1 = SensitiveWordHelper . findAll ( text , WordResultHandlers . wordTags ());
Assert . assertEquals ( "[WordTagsDto{word='五星红旗', tags=[]}, WordTagsDto{word='毛主席', tags=[]}, WordTagsDto{word='天安门', tags=[]}]" , wordList1 . toString ()); final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
String result = SensitiveWordHelper . replace ( text );
Assert . assertEquals ( "****迎风飘扬,***的画像屹立在***前。" , result ); final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
String result = SensitiveWordHelper . replace ( text , '0' );
Assert . assertEquals ( "0000迎风飘扬,000的画像屹立在000前。" , result );V0.2.0 支持該特性。
場景說明:有時候我們希望不同的敏感詞有不同的替換結果。比如【遊戲】替換為【電子競技】,【失業】替換為【靈活就業】。
誠然,提前使用字符串的正則替換也可以,不過性能一般。
使用例子:
/**
* 自定替换策略
* @since 0.2.0
*/
@ Test
public void defineReplaceTest () {
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
ISensitiveWordReplace replace = new MySensitiveWordReplace ();
String result = SensitiveWordHelper . replace ( text , replace );
Assert . assertEquals ( "国家旗帜迎风飘扬,教员的画像屹立在***前。" , result );
}其中MySensitiveWordReplace是我們自定義的替換策略,實現如下:
public class MyWordReplace implements IWordReplace {
@ Override
public void replace ( StringBuilder stringBuilder , final char [] rawChars , IWordResult wordResult , IWordContext wordContext ) {
String sensitiveWord = InnerWordCharUtils . getString ( rawChars , wordResult );
// 自定义不同的敏感词替换策略,可以从数据库等地方读取
if ( "五星红旗" . equals ( sensitiveWord )) {
stringBuilder . append ( "国家旗帜" );
} else if ( "毛主席" . equals ( sensitiveWord )) {
stringBuilder . append ( "教员" );
} else {
// 其他默认使用 * 代替
int wordLength = wordResult . endIndex () - wordResult . startIndex ();
for ( int i = 0 ; i < wordLength ; i ++) {
stringBuilder . append ( '*' );
}
}
}
}我們針對其中的部分詞做固定映射處理,其他的默認轉換為* 。
IWordResultHandler 可以對敏感詞的結果進行處理,允許用戶自定義。
內置實現見WordResultHandlers工具類:
只保留敏感詞單詞本身。
保留敏感詞相關信息,包含敏感詞的開始和結束下標。
同時保留單詞,和對應的詞標籤信息。
所有測試案例參見SensitiveWordHelperTest
1)基本例子
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
List < String > wordList = SensitiveWordHelper . findAll ( text );
Assert . assertEquals ( "[五星红旗, 毛主席, 天安门]" , wordList . toString ());
List < String > wordList2 = SensitiveWordHelper . findAll ( text , WordResultHandlers . word ());
Assert . assertEquals ( "[五星红旗, 毛主席, 天安门]" , wordList2 . toString ());
List < IWordResult > wordList3 = SensitiveWordHelper . findAll ( text , WordResultHandlers . raw ());
Assert . assertEquals ( "[WordResult{startIndex=0, endIndex=4}, WordResult{startIndex=9, endIndex=12}, WordResult{startIndex=18, endIndex=21}]" , wordList3 . toString ());我們在dict_tag_test.txt文件中指定對應詞的標籤信息。
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
// 默认敏感词标签为空
List < WordTagsDto > wordList1 = SensitiveWordHelper . findAll ( text , WordResultHandlers . wordTags ());
Assert . assertEquals ( "[WordTagsDto{word='五星红旗', tags=[]}, WordTagsDto{word='毛主席', tags=[]}, WordTagsDto{word='天安门', tags=[]}]" , wordList1 . toString ());
List < WordTagsDto > wordList2 = SensitiveWordBs . newInstance ()
. wordTag ( WordTags . file ( "dict_tag_test.txt" ))
. init ()
. findAll ( text , WordResultHandlers . wordTags ());
Assert . assertEquals ( "[WordTagsDto{word='五星红旗', tags=[政治, 国家]}, WordTagsDto{word='毛主席', tags=[政治, 伟人, 国家]}, WordTagsDto{word='天安门', tags=[政治, 国家, 地址]}]" , wordList2 . toString ());後續的諸多特性,主要是針對各種針對各種情況的處理,盡可能的提升敏感詞命中率。
這是一場漫長的攻防之戰。
final String text = "fuCK the bad words." ;
String word = SensitiveWordHelper . findFirst ( text );
Assert . assertEquals ( "fuCK" , word ); final String text = "fuck the bad words." ;
String word = SensitiveWordHelper . findFirst ( text );
Assert . assertEquals ( "fuck" , word );這裡實現了數字常見形式的轉換。
final String text = "这个是我的微信:9⓿二肆⁹₈③⑸⒋➃㈤㊄" ;
List < String > wordList = SensitiveWordBs . newInstance (). enableNumCheck ( true ). init (). findAll ( text );
Assert . assertEquals ( "[9⓿二肆⁹₈③⑸⒋➃㈤㊄]" , wordList . toString ()); final String text = "我爱我的祖国和五星紅旗。" ;
List < String > wordList = SensitiveWordHelper . findAll ( text );
Assert . assertEquals ( "[五星紅旗]" , wordList . toString ()); final String text = "Ⓕⓤc⒦ the bad words" ;
List < String > wordList = SensitiveWordHelper . findAll ( text );
Assert . assertEquals ( "[Ⓕⓤc⒦]" , wordList . toString ()); final String text = "ⒻⒻⒻfⓤuⓤ⒰cⓒ⒦ the bad words" ;
List < String > wordList = SensitiveWordBs . newInstance ()
. ignoreRepeat ( true )
. init ()
. findAll ( text );
Assert . assertEquals ( "[ⒻⒻⒻfⓤuⓤ⒰cⓒ⒦]" , wordList . toString ());郵箱等個人信息,默認未啟用。
final String text = "楼主好人,邮箱 [email protected]" ;
List < String > wordList = SensitiveWordBs . newInstance (). enableEmailCheck ( true ). init (). findAll ( text );
Assert . assertEquals ( "[[email protected]]" , wordList . toString ());一般用於過濾手機號/QQ等廣告信息,默認未啟用。
V0.2.1 之後,支持通過numCheckLen(长度)自定義檢測的長度。
final String text = "你懂得:12345678" ;
// 默认检测 8 位
List < String > wordList = SensitiveWordBs . newInstance ()
. enableNumCheck ( true )
. init (). findAll ( text );
Assert . assertEquals ( "[12345678]" , wordList . toString ());
// 指定数字的长度,避免误杀
List < String > wordList2 = SensitiveWordBs . newInstance ()
. enableNumCheck ( true )
. numCheckLen ( 9 )
. init ()
. findAll ( text );
Assert . assertEquals ( "[]" , wordList2 . toString ());用於過濾常見的網址信息,默認未啟用。
v0.18.0 優化URL 檢測,更加嚴格,降低誤判率
final String text = "点击链接 https://www.baidu.com 查看答案" ;
final SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance (). enableUrlCheck ( true ). init ();
List < String > wordList = sensitiveWordBs . findAll ( text );
Assert . assertEquals ( "[https://www.baidu.com]" , wordList . toString ());
Assert . assertEquals ( "点击链接 ********************* 查看答案" , sensitiveWordBs . replace ( text ));v0.17.0 支持
避免用戶通過ip 繞過網址檢測等,默認未啟用。
final String text = "个人网站,如果网址打不开可以访问 127.0.0.1。" ;
final SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance (). enableIpv4Check ( true ). init ();
List < String > wordList = sensitiveWordBs . findAll ( text );
Assert . assertEquals ( "[127.0.0.1]" , wordList . toString ());上面的特性默認都是開啟的,有時業務需要靈活定義相關的配置特性。
所以v0.0.14 開放了屬性配置。
為了讓使用更加優雅,統一使用fluent-api 的方式定義。
用戶可以使用SensitiveWordBs進行如下定義:
注意:配置後,要使用我們新定義的SensitiveWordBs的對象,而不是以前的工具方法。工具方法配置都是默認的。
SensitiveWordBs wordBs = SensitiveWordBs . newInstance ()
. ignoreCase ( true )
. ignoreWidth ( true )
. ignoreNumStyle ( true )
. ignoreChineseStyle ( true )
. ignoreEnglishStyle ( true )
. ignoreRepeat ( false )
. enableNumCheck ( false )
. enableEmailCheck ( false )
. enableUrlCheck ( false )
. enableIpv4Check ( false )
. enableWordCheck ( true )
. numCheckLen ( 8 )
. wordTag ( WordTags . none ())
. charIgnore ( SensitiveWordCharIgnores . defaults ())
. wordResultCondition ( WordResultConditions . alwaysTrue ())
. init ();
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
Assert . assertTrue ( wordBs . contains ( text ));其中各項配置的說明如下:
| 序號 | 方法 | 說明 | 預設值 |
|---|---|---|---|
| 1 | ignoreCase | 忽略大小寫 | true |
| 2 | ignoreWidth | 忽略半角圓角 | true |
| 3 | ignoreNumStyle | 忽略數字的寫法 | true |
| 4 | ignoreChineseStyle | 忽略中文的書寫格式 | true |
| 5 | ignoreEnglishStyle | 忽略英文的書寫格式 | true |
| 6 | ignoreRepeat | 忽略重複詞 | false |
| 7 | enableNumCheck | 是否啟用數字檢測。 | false |
| 8 | enableEmailCheck | 是有啟用郵箱檢測 | false |
| 9 | enableUrlCheck | 是否啟用鏈接檢測 | false |
| 10 | enableIpv4Check | 是否啟用IPv4檢測 | false |
| 11 | enableWordCheck | 是否啟用敏感單詞檢測 | true |
| 12 | numCheckLen | 數字檢測,自定義指定長度。 | 8 |
| 13 | wordTag | 詞對應的標籤 | none |
| 14 | charIgnore | 忽略的字符 | none |
| 15 | wordResultCondition | 針對匹配的敏感詞額外加工,比如可以限制英文單詞必須全匹配 | 恆為真 |
v0.16.1 開始支持,有時候我們需要釋放內存,可以如下:
關於內存回收問題
SensitiveWordBs wordBs = SensitiveWordBs . newInstance ()
. init ();
// 后续因为一些原因移除了对应信息,希望释放内存。
wordBs . destroy ();使用場景:在初始化之後,我們希望針對單個詞的新增/刪除,而不是完全重新初始化。這個特性就是為此準備的。
支持版本:v0.19.0
addWord(word)新增敏感詞,支持單個詞/集合
removeWord(word)刪除敏感詞,支持單個詞/集合
final String text = "测试一下新增敏感词,验证一下删除和新增对不对" ;
SensitiveWordBs sensitiveWordBs =
SensitiveWordBs . newInstance ()
. wordAllow ( WordAllows . empty ())
. wordDeny ( WordDenys . empty ())
. init ();
// 当前
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增单个
sensitiveWordBs . addWord ( "测试" );
sensitiveWordBs . addWord ( "新增" );
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除单个
sensitiveWordBs . removeWord ( "新增" );
Assert . assertEquals ( "[测试]" , sensitiveWordBs . findAll ( text ). toString ());
sensitiveWordBs . removeWord ( "测试" );
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增集合
sensitiveWordBs . addWord ( Arrays . asList ( "新增" , "测试" ));
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除集合
sensitiveWordBs . removeWord ( Arrays . asList ( "新增" , "测试" ));
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增数组
sensitiveWordBs . addWord ( "新增" , "测试" );
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除集合
sensitiveWordBs . removeWord ( "新增" , "测试" );
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());使用場景:在初始化之後,我們希望針對單個詞的新增/刪除,而不是完全重新初始化。這個特性就是為此準備的。
支持版本:v0.21.0
addWordAllow(word)新增白名單,支持單個詞/集合
removeWordAllow(word)刪除白名單,支持單個詞/集合
final String text = "测试一下新增敏感词白名单,验证一下删除和新增对不对" ;
SensitiveWordBs sensitiveWordBs =
SensitiveWordBs . newInstance ()
. wordAllow ( WordAllows . empty ())
. wordDeny ( new IWordDeny () {
@ Override
public List < String > deny () {
return Arrays . asList ( "测试" , "新增" );
}
})
. init ();
// 当前
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增单个
sensitiveWordBs . addWordAllow ( "测试" );
sensitiveWordBs . addWordAllow ( "新增" );
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除单个
sensitiveWordBs . removeWordAllow ( "测试" );
Assert . assertEquals ( "[测试]" , sensitiveWordBs . findAll ( text ). toString ());
sensitiveWordBs . removeWordAllow ( "新增" );
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增集合
sensitiveWordBs . addWordAllow ( Arrays . asList ( "新增" , "测试" ));
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除集合
sensitiveWordBs . removeWordAllow ( Arrays . asList ( "新增" , "测试" ));
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());
// 新增数组
sensitiveWordBs . addWordAllow ( "新增" , "测试" );
Assert . assertEquals ( "[]" , sensitiveWordBs . findAll ( text ). toString ());
// 删除集合
sensitiveWordBs . removeWordAllow ( "新增" , "测试" );
Assert . assertEquals ( "[测试, 新增, 新增]" , sensitiveWordBs . findAll ( text ). toString ());此方式已廢棄,建議使用上面增量添加的方式,避免全量加載。為了兼容,此方式依然保留。
使用方式:在調用sensitiveWordBs.init()的時候,根據IWordDeny+IWordAllow 重新構建敏感詞庫。 因為初始化可能耗時較長(秒級別),所有優化為init 未完成時不影響舊的詞庫功能,完成後以新的為準。
@ Component
public class SensitiveWordService {
@ Autowired
private SensitiveWordBs sensitiveWordBs ;
/**
* 更新词库
*
* 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法。
* 如果需要生效,则调用这个方法。
*
* 说明:重新初始化不影响旧的方法使用。初始化完成后,会以新的为准。
*/
public void refresh () {
// 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法,然后调用这个方法。
sensitiveWordBs . init ();
}
}如上,你可以在數據庫詞庫發生變更時,需要詞庫生效,主動觸發一次初始化sensitiveWordBs.init(); 。
其他使用保持不變,無需重啟應用。
支持版本:v0.13.0
有時候我們可能希望對匹配的敏感詞進一步限制,比如雖然我們定義了【av】作為敏感詞,但是不希望【have】被匹配。
就可以自定義實現wordResultCondition 接口,實現自己的策略。
系統內置的策略在WordResultConditions#alwaysTrue()恆為真, WordResultConditions#englishWordMatch()則要求英文必須全詞匹配。
WordResultConditions 工具類可以獲取匹配策略
| 實現 | 說明 | 支持版本 |
|---|---|---|
| alwaysTrue | 恆為真 | |
| englishWordMatch | 英文單詞全詞匹配 | v0.13.0 |
| englishWordNumMatch | 英文單詞/數字全詞匹配 | v0.20.0 |
| wordTags | 滿足特定標籤的,比如只關注【廣告】標籤 | v0.23.0 |
| chains(IWordResultCondition ...conditions) | 支持指定多個條件,同時滿足 | v0.23.0 |
原始的默認情況:
final String text = "I have a nice day。" ;
List < String > wordList = SensitiveWordBs . newInstance ()
. wordDeny ( new IWordDeny () {
@ Override
public List < String > deny () {
return Collections . singletonList ( "av" );
}
})
. wordResultCondition ( WordResultConditions . alwaysTrue ())
. init ()
. findAll ( text );
Assert . assertEquals ( "[av]" , wordList . toString ());我們可以指定為英文必須全詞匹配。
final String text = "I have a nice day。" ;
List < String > wordList = SensitiveWordBs . newInstance ()
. wordDeny ( new IWordDeny () {
@ Override
public List < String > deny () {
return Collections . singletonList ( "av" );
}
})
. wordResultCondition ( WordResultConditions . englishWordMatch ())
. init ()
. findAll ( text );
Assert . assertEquals ( "[]" , wordList . toString ());當然可以根據需要實現更加複雜的策略。
支持版本: v0.23.0
我們可以只返回隸屬於某一種標籤的敏感詞。
我們指定了兩個敏感詞:商品、AV
MyWordTag 是我們定義的一個敏感詞標籤實現:
/**
* 自定义单词标签
* @since 0.23.0
*/
public class MyWordTag extends AbstractWordTag {
private static Map < String , Set < String >> dataMap ;
static {
dataMap = new HashMap <>();
dataMap . put ( "商品" , buildSet ( "广告" , "中文" ));
dataMap . put ( "AV" , buildSet ( "色情" , "单词" , "英文" ));
}
private static Set < String > buildSet ( String ... tags ) {
Set < String > set = new HashSet <>();
for ( String tag : tags ) {
set . add ( tag );
}
return set ;
}
@ Override
protected Set < String > doGetTag ( String word ) {
return dataMap . get ( word );
}
}測試用例如下,我們模擬了兩個不同的實現類,每一個關注的單詞標籤不同。
// 只关心SE情
SensitiveWordBs sensitiveWordBsYellow = SensitiveWordBs . newInstance ()
. wordDeny ( new IWordDeny () {
@ Override
public List < String > deny () {
return Arrays . asList ( "商品" , "AV" );
}
})
. wordAllow ( WordAllows . empty ())
. wordTag ( new MyWordTag ())
. wordResultCondition ( WordResultConditions . wordTags ( Arrays . asList ( "色情" )))
. init ();
// 只关心广告
SensitiveWordBs sensitiveWordBsAd = SensitiveWordBs . newInstance ()
. wordDeny ( new IWordDeny () {
@ Override
public List < String > deny () {
return Arrays . asList ( "商品" , "AV" );
}
})
. wordAllow ( WordAllows . empty ())
. wordTag ( new MyWordTag ())
. wordResultCondition ( WordResultConditions . wordTags ( Arrays . asList ( "广告" )))
. init ();
final String text = "这些 AV 商品什么价格?" ;
Assert . assertEquals ( "[AV]" , sensitiveWordBsYellow . findAll ( text ). toString ());
Assert . assertEquals ( "[商品]" , sensitiveWordBsAd . findAll ( text ). toString ());我們的敏感詞一般都是比較連續的,比如【傻帽】
那就有大聰明發現,可以在中間加一些字符,比如【傻!@#$帽】跳過檢測,但是罵人等攻擊力不減。
那麼,如何應對這些類似的場景呢?
我們可以指定特殊字符的跳過集合,忽略掉這些無意義的字符即可。
v0.11.0 開始支持
其中charIgnore 對應的字符策略,用戶可以自行靈活定義。
final String text = "傻@冒,狗+东西" ;
//默认因为有特殊字符分割,无法识别
List < String > wordList = SensitiveWordBs . newInstance (). init (). findAll ( text );
Assert . assertEquals ( "[]" , wordList . toString ());
// 指定忽略的字符策略,可自行实现。
List < String > wordList2 = SensitiveWordBs . newInstance ()
. charIgnore ( SensitiveWordCharIgnores . specialChars ())
. init ()
. findAll ( text );
Assert . assertEquals ( "[傻@冒, 狗+东西]" , wordList2 . toString ());有時候我們希望對敏感詞加一個分類標籤:比如社情、暴/力等等。
這樣後續可以按照標籤等進行更多特性操作,比如只處理某一類的標籤。
支持版本:v0.10.0
主要特性支持版本:v0.24.0
這裡只是一個抽象的接口,用戶可以自行定義實現。比如從數據庫查詢、文件讀取、api 調用等。
public interface IWordTag {
/**
* 查询标签列表
* @param word 脏词
* @return 结果
*/
Set < String > getTag ( String word );
}為了方便大部分情況使用,內置實現一些場景策略在WordTags類中
| 實現方法 | 說明 | 備註 |
|---|---|---|
| none() | 空實現 | v0.10.0 支持 |
| file(String filePath) | 指定文件路徑 | v0.10.0 支持 |
| file(String filePath, String wordSplit, String tagSplit) | 指定文件路徑,以及單詞分隔符、標籤分隔符 | v0.10.0 支持 |
| map(final Map<String, Set> wordTagMap) | 根據map初始化 | v0.24.0 支持 |
| lines(Collection lines) | 字符串列表 | v0.24.0 支持 |
| lines(Collection lines, String wordSplit, String tagSpli) | 字符串列表,以及單詞分隔符、標籤分隔符 | v0.24.0 支持 |
| system() | 系件文件內置實現,整合網絡分類 | v0.24.0 支持 |
| defaults() | 默認策略,目前為system | v0.24.0 支持 |
| chains(IWordTag... others) | 鍊式方法,支持用戶整合實現多個策略 | v0.24.0 支持 |
敏感詞標籤的格式我們默認約定如下敏感词tag1,tag2 ,代表這敏感词的標籤為tag1 和tag2
比如
五星红旗 政治,国家
所有的文件行內容,和指定的字符串行內容也建議用這種方式。如果不滿足,自定義實現即可。
v0.24.0 版本開始,默認的單詞標籤為WordTags.system() 。
說明:目前數據統計自網絡,存在不少疏漏。也歡迎大家指正,持續改進中...
SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance ()
. wordTag ( WordTags . system ())
. init ();
Set < String > tagSet = sensitiveWordBs . tags ( "博彩" );
Assert . assertEquals ( "[3]" , tagSet . toString ());這里為了壓縮大小優化,對應的類別用數字表示。
數字的含義列表如下:
0 政治
1 毒品
2 色情
3 赌博
4 违法
這里以文件為例子,演示一下如何使用。
final String path = "~ \ test \ resources \ dict_tag_test.txt" ;
// 演示默认方法
IWordTag wordTag = WordTags . file ( path );
SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance ()
. wordTag ( wordTag )
. init ();
Set < String > tagSet = sensitiveWordBs . tags ( "零售" );
Assert . assertEquals ( "[广告, 网络]" , tagSet . toString ());
// 演示指定分隔符
IWordTag wordTag2 = WordTags . file ( path , " " , "," );
SensitiveWordBs sensitiveWordBs2 = SensitiveWordBs . newInstance ()
. wordTag ( wordTag2 )
. init ();
Set < String > tagSet2 = sensitiveWordBs2 . tags ( "零售" );
Assert . assertEquals ( "[广告, 网络]" , tagSet2 . toString ());其中dict_tag_test.txt我們自定義的內容如下:
零售 广告,网络
我們在獲取敏感詞的時候,是可以設置對應的結果處理策略,從而獲取對應的敏感詞標籤信息
// 自定义测试标签类
IWordTag wordTag = WordTags . lines ( Arrays . asList ( "天安门 政治,国家,地址" ));
// 指定初始化
SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance ()
. wordTag ( wordTag )
. init ()
;
List < WordTagsDto > wordTagsDtoList1 = sensitiveWordBs . findAll ( "天安门" , WordResultHandlers . wordTags ());
Assert . assertEquals ( "[WordTagsDto{word='天安门', tags=[政治, 国家, 地址]}]" , wordTagsDtoList1 . toString ());我們自定義了天安门關鍵詞的標籤,然後通過指定findAll 的結果處理策略為WordResultHandlers.wordTags() ,就可以在獲取敏感詞的同時,獲取對應的標籤列表。
有時候我們希望將敏感詞的加載設計成動態的,比如控台修改,然後可以實時生效。
v0.0.13 支持了這種特性。
為了實現這個特性,並且兼容以前的功能,我們定義了兩個接口。
接口如下,可以自定義自己的實現。
返回的列表,表示這個詞是一個敏感詞。
/**
* 拒绝出现的数据-返回的内容被当做是敏感词
* @author binbin.hou
* @since 0.0.13
*/
public interface IWordDeny {
/**
* 获取结果
* @return 结果
* @since 0.0.13
*/
List < String > deny ();
}比如:
public class MyWordDeny implements IWordDeny {
@ Override
public List < String > deny () {
return Arrays . asList ( "我的自定义敏感词" );
}
}接口如下,可以自定義自己的實現。
返回的列表,表示這個詞不是一個敏感詞。
/**
* 允许的内容-返回的内容不被当做敏感词
* @author binbin.hou
* @since 0.0.13
*/
public interface IWordAllow {
/**
* 获取结果
* @return 结果
* @since 0.0.13
*/
List < String > allow ();
}如:
public class MyWordAllow implements IWordAllow {
@ Override
public List < String > allow () {
return Arrays . asList ( "五星红旗" );
}
}接口自定義之後,當然需要指定才能生效。
為了讓使用更加優雅,我們設計了引導類SensitiveWordBs 。
可以通過wordDeny() 指定敏感詞,wordAllow() 指定非敏感詞,通過init() 初始化敏感詞字典。
SensitiveWordBs wordBs = SensitiveWordBs . newInstance ()
. wordDeny ( WordDenys . defaults ())
. wordAllow ( WordAllows . defaults ())
. init ();
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。" ;
Assert . assertTrue ( wordBs . contains ( text ));備註:init() 對於敏感詞DFA 的構建是比較耗時的,一般建議在應用初始化的時候只初始化一次。而不是重複初始化!
我們可以測試一下自定義的實現,如下:
String text = "这是一个测试,我的自定义敏感词。" ;
SensitiveWordBs wordBs = SensitiveWordBs . newInstance ()
. wordDeny ( new MyWordDeny ())
. wordAllow ( new MyWordAllow ())
. init ();
Assert . assertEquals ( "[我的自定义敏感词]" , wordBs . findAll ( text ). toString ());這裡只有我的自定义敏感词是敏感詞,而测试不是敏感詞。
當然,這裡是全部使用我們自定義的實現,一般建議使用系統的默認配置+自定義配置。
可以使用下面的方式。
WordDenys.chains()方法,將多個實現合併為同一個IWordDeny。
WordAllows.chains()方法,將多個實現合併為同一個IWordAllow。
例子:
String text = "这是一个测试。我的自定义敏感词。" ;
IWordDeny wordDeny = WordDenys . chains ( WordDenys . defaults (), new MyWordDeny ());
IWordAllow wordAllow = WordAllows . chains ( WordAllows . defaults (), new MyWordAllow ());
SensitiveWordBs wordBs = SensitiveWordBs . newInstance ()
. wordDeny ( wordDeny )
. wordAllow ( wordAllow )
. init ();
Assert . assertEquals ( "[我的自定义敏感词]" , wordBs . findAll ( text ). toString ());這裡都是同時使用了系統默認配置,和自定義的配置。
注意:我們初始化了新的wordBs,那麼用新的wordBs 去判斷。而不是用以前的SensitiveWordHelper工具方法,工具方法配置是默認的!
實際使用中,比如可以在頁面配置修改,然後實時生效。
數據存儲在數據庫中,下面是一個偽代碼的例子,可以參考SpringSensitiveWordConfig.java
要求,版本v0.0.15 及其以上。
簡化偽代碼如下,數據的源頭為數據庫。
MyDdWordAllow 和MyDdWordDeny 是基於數據庫為源頭的自定義實現類。
@ Configuration
public class SpringSensitiveWordConfig {
@ Autowired
private MyDdWordAllow myDdWordAllow ;
@ Autowired
private MyDdWordDeny myDdWordDeny ;
/**
* 初始化引导类
* @return 初始化引导类
* @since 1.0.0
*/
@ Bean
public SensitiveWordBs sensitiveWordBs () {
SensitiveWordBs sensitiveWordBs = SensitiveWordBs . newInstance ()
. wordAllow ( WordAllows . chains ( WordAllows . defaults (), myDdWordAllow ))
. wordDeny ( myDdWordDeny )
// 各种其他配置
. init ();
return sensitiveWordBs ;
}
}敏感詞庫的初始化較為耗時,建議程序啟動時做一次init 初始化。
V0.6.0 以後,添加對應的benchmark 測試。
BenchmarkTimesTest
測試環境為普通的筆記本:
处理器 12th Gen Intel(R) Core(TM) i7-1260P 2.10 GHz
机带 RAM 16.0 GB (15.7 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
ps: 不同環境會有差異,但是比例基本穩定。
測試數據:100+ 字符串,循環10W 次。
| 序號 | 場景 | 耗時 | 備註 |
|---|---|---|---|
| 1 | 只做敏感詞,無任何格式轉換 | 1470ms,約7.2W QPS | 追求極致性能,可以這樣配置 |
| 2 | 只做敏感詞,支持全部格式轉換 | 2744ms,約3.7W QPS | 滿足大部分場景 |
移除單個漢字的敏感詞,在中國,要把詞組當做一次詞,降低誤判率。
支持單個的敏感詞變化?
remove、add、edit?
敏感詞標籤接口支持
敏感詞處理時標籤支持
wordData 的內存佔用對比+ 優化
用戶指定自定義的詞組,同時允許指定詞組的組合獲取,更加靈活
FormatCombine/CheckCombine/AllowDenyCombine 組合策略,允許用戶自定義。
word check 策略的優化,統一遍歷+轉換
添加ThreadLocal 等性能優化
sensitive-word-admin 敏感詞控台v1.2.0 版本開源
sensitive-word-admin v1.3.0 發佈如何支持分佈式部署?
01-開源敏感詞工具入門使用
02-如何實現一個敏感詞工具?違禁詞實現思路梳理
03-敏感詞之StopWord 停止詞優化與特殊符號
04-敏感詞之字典瘦身
05-敏感詞之DFA 算法(Trie Tree 算法)詳解
06-敏感詞(臟詞) 如何忽略無意義的字符?達到更好的過濾效果
v0.10.0-臟詞分類標籤初步支持
v0.11.0-敏感詞新特性:忽略無意義的字符,詞標籤字典
v0.12.0-敏感詞/臟詞詞標籤能力進一步增強
v0.13.0-敏感詞特性版本發布支持英文單詞全詞匹配
v0.16.1-敏感詞新特性之字典內存資源釋放
v0.19.0-敏感詞新特性之敏感詞單個編輯,不必重複初始化
v0.20.0 敏感詞新特性之數字全部匹配,而不是部分匹配
v0.21.0 敏感詞新特性之白名單支持單個編輯,修正白名單包含黑名單時的問題
pinyin 漢字轉拼音
pinyin2hanzi 拼音轉漢字
segment 高性能中文分詞
opencc4j 中文繁簡體轉換
nlp-hanzi-similar 漢字相似度
word-checker 拼寫檢測
sensitive-word 敏感詞