- Notifications
You must be signed in to change notification settings - Fork663
👮♂️The sensitive word tool for java.(敏感词/违禁词/违法词/脏词。基于 DFA 算法实现的高性能 java 敏感词过滤工具框架。内置支持单词标签分类分级。请勿发布涉及政治、广告、营销、翻墙、违反国家法律法规等内容。高性能敏感词检测过滤组件,附带繁体简体互换,支持全角半角互换,汉字转拼音,模糊搜索等功能。)
License
houbb/sensitive-word
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
sensitive-word 基于 DFA 算法实现的高性能敏感词工具。
如果有一些疑难杂症,可以加入:技术交流群
sensitive-word-admin 是对应的控台的应用,目前功能处于初期开发中,MVP 版本可用。
大家好,我是老马。
一直想实现一款简单好用敏感词工具,于是开源实现了这个工具。
基于 DFA 算法实现,目前敏感词库内容收录 6W+(源文件 18W+,经过一次删减)。
后期将进行持续优化和补充敏感词库,并进一步提升算法的性能。
v0.24.0 开始内置支持对敏感词的分类细化,不过工作量比较大,难免存在疏漏。
欢迎 PR 改进, github 提需求,或者加入技术交流群沟通吹牛!
6W+ 词库,且不断优化更新
基于 fluent-api 实现,使用优雅简洁
全角半角互换、英文大小写互换、数字常见形式的互换、中文繁简体互换、英文常见形式的互换、忽略重复词等
有时候敏感词有一个控台,配置起来会更加灵活方便。
梳理了大量的敏感词标签文件,可以让我们的敏感词更加方便。
这两个资料阅读可在下方文章获取:
目前 v0.24.0 已内置实现单词标签,需要的建议升级到最新版本。
开源不易,如果本项目对你有帮助,你可以请老马喝一杯奶茶。
JDK1.8+
Maven 3.x+
<dependency> <groupId>com.github.houbb</groupId> <artifactId>sensitive-word</artifactId> <version>0.25.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) | 获取敏感词的标签 | 敏感词字符串 | 返回敏感词的标签列表 |
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";Assert.assertTrue(SensitiveWordHelper.contains(text));
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";Stringword =SensitiveWordHelper.findFirst(text);Assert.assertEquals("五星红旗",word);
SensitiveWordHelper.findFirst(text) 等价于:
Stringword =SensitiveWordHelper.findFirst(text,WordResultHandlers.word());
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";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() 可以保留对应的下标信息、类别信息:
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";// 默认敏感词标签为空List<WordTagsDto>wordList1 =SensitiveWordHelper.findAll(text,WordResultHandlers.wordTags());Assert.assertEquals("[WordTagsDto{word='五星红旗', tags=[]}, WordTagsDto{word='毛主席', tags=[]}, WordTagsDto{word='天安门', tags=[]}]",wordList1.toString());
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";Stringresult =SensitiveWordHelper.replace(text);Assert.assertEquals("****迎风飘扬,***的画像屹立在***前。",result);
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";Stringresult =SensitiveWordHelper.replace(text,'0');Assert.assertEquals("0000迎风飘扬,000的画像屹立在000前。",result);
V0.2.0 支持该特性。
场景说明:有时候我们希望不同的敏感词有不同的替换结果。比如【游戏】替换为【电子竞技】,【失业】替换为【灵活就业】。
诚然,提前使用字符串的正则替换也可以,不过性能一般。
使用例子:
/** * 自定替换策略 * @since 0.2.0 */@TestpublicvoiddefineReplaceTest() {finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";ISensitiveWordReplacereplace =newMySensitiveWordReplace();Stringresult =SensitiveWordHelper.replace(text,replace);Assert.assertEquals("国家旗帜迎风飘扬,教员的画像屹立在***前。",result);}
其中MySensitiveWordReplace
是我们自定义的替换策略,实现如下:
publicclassMyWordReplaceimplementsIWordReplace {@Overridepublicvoidreplace(StringBuilderstringBuilder,finalchar[]rawChars,IWordResultwordResult,IWordContextwordContext) {StringsensitiveWord =InnerWordCharUtils.getString(rawChars,wordResult);// 自定义不同的敏感词替换策略,可以从数据库等地方读取if("五星红旗".equals(sensitiveWord)) {stringBuilder.append("国家旗帜"); }elseif("毛主席".equals(sensitiveWord)) {stringBuilder.append("教员"); }else {// 其他默认使用 * 代替intwordLength =wordResult.endIndex() -wordResult.startIndex();for(inti =0;i <wordLength;i++) {stringBuilder.append('*'); } } }}
我们针对其中的部分词做固定映射处理,其他的默认转换为*
。
IWordResultHandler 可以对敏感词的结果进行处理,允许用户自定义。
内置实现见WordResultHandlers
工具类:
- WordResultHandlers.word()
只保留敏感词单词本身。
- WordResultHandlers.raw()
保留敏感词相关信息,包含敏感词的开始和结束下标。
- WordResultHandlers.wordTags()
同时保留单词,和对应的词标签信息。
所有测试案例参见SensitiveWordHelperTest
1)基本例子
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";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());
- wordTags 例子
我们在dict_tag_test.txt
文件中指定对应词的标签信息。
finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";// 默认敏感词标签为空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());
后续的诸多特性,主要是针对各种针对各种情况的处理,尽可能的提升敏感词命中率。
这是一场漫长的攻防之战。
finalStringtext ="fuCK the bad words.";Stringword =SensitiveWordHelper.findFirst(text);Assert.assertEquals("fuCK",word);
finalStringtext ="fuck the bad words.";Stringword =SensitiveWordHelper.findFirst(text);Assert.assertEquals("fuck",word);
这里实现了数字常见形式的转换。
finalStringtext ="这个是我的微信:9⓿二肆⁹₈③⑸⒋➃㈤㊄";List<String>wordList =SensitiveWordBs.newInstance().enableNumCheck(true).init().findAll(text);Assert.assertEquals("[9⓿二肆⁹₈③⑸⒋➃㈤㊄]",wordList.toString());
finalStringtext ="我爱我的祖国和五星紅旗。";List<String>wordList =SensitiveWordHelper.findAll(text);Assert.assertEquals("[五星紅旗]",wordList.toString());
finalStringtext ="Ⓕⓤc⒦ the bad words";List<String>wordList =SensitiveWordHelper.findAll(text);Assert.assertEquals("[Ⓕⓤc⒦]",wordList.toString());
finalStringtext ="ⒻⒻⒻfⓤuⓤ⒰cⓒ⒦ the bad words";List<String>wordList =SensitiveWordBs.newInstance() .ignoreRepeat(true) .init() .findAll(text);Assert.assertEquals("[ⒻⒻⒻfⓤuⓤ⒰cⓒ⒦]",wordList.toString());
v0.25.0 目前的几个策略,也支持用户引导类自定义。所有的策略都是接口,支持用户自定义实现。
序号 | 方法 | 说明 | 默认值 |
---|---|---|---|
16 | wordCheckNum | 数字检测策略(v0.25.0开始支持) | WordChecks.num() |
17 | wordCheckEmail | 邮箱检测策略(v0.25.0开始支持) | WordChecks.email() |
18 | wordCheckUrl | URL检测策略(v0.25.0开始支持),内置还是实现了urlNoPrefix() | (WordChecks.url() |
19 | wordCheckIpv4 | ipv4检测策略(v0.25.0开始支持) | WordChecks.ipv4() |
20 | wordCheckWord | 敏感词检测策略(v0.25.0开始支持) | WordChecks.word() |
内置实现:
a)WordChecks.urlNoPrefix()
作为 url 的额外实现,可以不需要https://
和http://
前缀。
邮箱等个人信息,默认未启用。
finalStringtext ="楼主好人,邮箱 sensitiveword@xx.com";List<String>wordList =SensitiveWordBs.newInstance().enableEmailCheck(true).init().findAll(text);Assert.assertEquals("[sensitiveword@xx.com]",wordList.toString());
一般用于过滤手机号/QQ等广告信息,默认未启用。
V0.2.1 之后,支持通过numCheckLen(长度)
自定义检测的长度。
finalStringtext ="你懂得: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 检测,更加严格,降低误判率
finalStringtext ="点击链接 https://www.baidu.com 查看答案";finalSensitiveWordBssensitiveWordBs =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.25.0 内置支持不需要 http 协议的前缀检测:
finalStringtext ="点击链接 https://www.baidu.com 查看答案,当然也可以是 baidu.com、www.baidu.com";finalSensitiveWordBssensitiveWordBs =SensitiveWordBs.newInstance() .enableUrlCheck(true)// 启用URL检测 .wordCheckUrl(WordChecks.urlNoPrefix())//指定检测的方式 .init();List<String>wordList =sensitiveWordBs.findAll(text);Assert.assertEquals("[www.baidu.com, baidu.com, www.baidu.com]",wordList.toString());Assert.assertEquals("点击链接 https://************* 查看答案,当然也可以是 *********、*************",sensitiveWordBs.replace(text));
v0.17.0 支持
避免用户通过 ip 绕过网址检测等,默认未启用。
finalStringtext ="个人网站,如果网址打不开可以访问 127.0.0.1。";finalSensitiveWordBssensitiveWordBs =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
的对象,而不是以前的工具方法。工具方法配置都是默认的。
SensitiveWordBswordBs =SensitiveWordBs.newInstance() .ignoreCase(true) .ignoreWidth(true) .ignoreNumStyle(true) .ignoreChineseStyle(true) .ignoreEnglishStyle(true) .ignoreRepeat(false) .enableNumCheck(false) .enableEmailCheck(false) .enableUrlCheck(false) .enableIpv4Check(false) .enableWordCheck(true) .wordCheckNum(WordChecks.num()) .wordCheckEmail(WordChecks.email()) .wordCheckUrl(WordChecks.url()) .wordCheckIpv4(WordChecks.ipv4()) .wordCheckWord(WordChecks.word()) .numCheckLen(8) .wordTag(WordTags.none()) .charIgnore(SensitiveWordCharIgnores.defaults()) .wordResultCondition(WordResultConditions.alwaysTrue()) .init();finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";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 | 针对匹配的敏感词额外加工,比如可以限制英文单词必须全匹配 | 恒为真 |
16 | wordCheckNum | 数字检测策略(v0.25.0开始支持) | WordChecks.num() |
17 | wordCheckEmail | 邮箱检测策略(v0.25.0开始支持) | WordChecks.email() |
18 | wordCheckUrl | URL检测策略(v0.25.0开始支持) | (WordChecks.url() |
19 | wordCheckIpv4 | ipv4检测策略(v0.25.0开始支持) | WordChecks.ipv4() |
20 | wordCheckWord | 敏感词检测策略(v0.25.0开始支持) | WordChecks.word() |
21 | wordReplace | 替换策略 | WordReplaces.defaults() |
v0.16.1 开始支持,有时候我们需要释放内存,可以如下:
SensitiveWordBswordBs =SensitiveWordBs.newInstance() .init();// 后续因为一些原因移除了对应信息,希望释放内存。wordBs.destroy();
使用场景:在初始化之后,我们希望针对单个词的新增/删除,而不是完全重新初始化。这个特性就是为此准备的。
支持版本:v0.19.0
addWord(word)
新增敏感词,支持单个词/集合
removeWord(word)
删除敏感词,支持单个词/集合
finalStringtext ="测试一下新增敏感词,验证一下删除和新增对不对";SensitiveWordBssensitiveWordBs =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)
删除白名单,支持单个词/集合
finalStringtext ="测试一下新增敏感词白名单,验证一下删除和新增对不对";SensitiveWordBssensitiveWordBs =SensitiveWordBs.newInstance() .wordAllow(WordAllows.empty()) .wordDeny(newIWordDeny() {@OverridepublicList<String>deny() {returnArrays.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 未完成时不影响旧的词库功能,完成后以新的为准。
@ComponentpublicclassSensitiveWordService {@AutowiredprivateSensitiveWordBssensitiveWordBs;/** * 更新词库 * * 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法。 * 如果需要生效,则调用这个方法。 * * 说明:重新初始化不影响旧的方法使用。初始化完成后,会以新的为准。 */publicvoidrefresh() {// 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法,然后调用这个方法。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 |
原始的默认情况:
finalStringtext ="I have a nice day。";List<String>wordList =SensitiveWordBs.newInstance() .wordDeny(newIWordDeny() {@OverridepublicList<String>deny() {returnCollections.singletonList("av"); } }) .wordResultCondition(WordResultConditions.alwaysTrue()) .init() .findAll(text);Assert.assertEquals("[av]",wordList.toString());
我们可以指定为英文必须全词匹配。
finalStringtext ="I have a nice day。";List<String>wordList =SensitiveWordBs.newInstance() .wordDeny(newIWordDeny() {@OverridepublicList<String>deny() {returnCollections.singletonList("av"); } }) .wordResultCondition(WordResultConditions.englishWordMatch()) .init() .findAll(text);Assert.assertEquals("[]",wordList.toString());
当然可以根据需要实现更加复杂的策略。
支持版本:v0.23.0
我们可以只返回隶属于某一种标签的敏感词。
我们指定了两个敏感词:商品、AV
MyWordTag 是我们定义的一个敏感词标签实现:
/** * 自定义单词标签 * @since 0.23.0 */publicclassMyWordTagextendsAbstractWordTag {privatestaticMap<String,Set<String>>dataMap;static {dataMap =newHashMap<>();dataMap.put("商品",buildSet("广告","中文"));dataMap.put("AV",buildSet("色情","单词","英文")); }privatestaticSet<String>buildSet(String...tags) {Set<String>set =newHashSet<>();for(Stringtag :tags) {set.add(tag); }returnset; }@OverrideprotectedSet<String>doGetTag(Stringword) {returndataMap.get(word); }}
测试用例如下,我们模拟了两个不同的实现类,每一个关注的单词标签不同。
// 只关心SE情SensitiveWordBssensitiveWordBsYellow =SensitiveWordBs.newInstance() .wordDeny(newIWordDeny() {@OverridepublicList<String>deny() {returnArrays.asList("商品","AV"); } }) .wordAllow(WordAllows.empty()) .wordTag(newMyWordTag()) .wordResultCondition(WordResultConditions.wordTags(Arrays.asList("色情"))) .init();// 只关心广告SensitiveWordBssensitiveWordBsAd =SensitiveWordBs.newInstance() .wordDeny(newIWordDeny() {@OverridepublicList<String>deny() {returnArrays.asList("商品","AV"); } }) .wordAllow(WordAllows.empty()) .wordTag(newMyWordTag()) .wordResultCondition(WordResultConditions.wordTags(Arrays.asList("广告"))) .init();finalStringtext ="这些 AV 商品什么价格?";Assert.assertEquals("[AV]",sensitiveWordBsYellow.findAll(text).toString());Assert.assertEquals("[商品]",sensitiveWordBsAd.findAll(text).toString());
我们的敏感词一般都是比较连续的,比如【傻帽】
那就有大聪明发现,可以在中间加一些字符,比如【傻!@#$帽】跳过检测,但是骂人等攻击力不减。
那么,如何应对这些类似的场景呢?
我们可以指定特殊字符的跳过集合,忽略掉这些无意义的字符即可。
v0.11.0 开始支持
其中 charIgnore 对应的字符策略,用户可以自行灵活定义。
finalStringtext ="傻@冒,狗+东西";//默认因为有特殊字符分割,无法识别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 调用等。
publicinterfaceIWordTag {/** * 查询标签列表 * @param word 脏词 * @return 结果 */Set<String>getTag(Stringword);}
为了方便大部分情况使用,内置实现一些场景策略在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()
。
说明:目前数据统计自网络,存在不少疏漏。也欢迎大家指正,持续改进中...
SensitiveWordBssensitiveWordBs =SensitiveWordBs.newInstance().wordTag(WordTags.system()).init();Set<String>tagSet =sensitiveWordBs.tags("博彩");Assert.assertEquals("[3]",tagSet.toString());
这里为了压缩大小优化,对应的类别用数字表示。
数字的含义列表如下:
0 政治1 毒品2 色情3 赌博4 违法
这里以文件为例子,演示一下如何使用。
finalStringpath ="~\\test\\resources\\dict_tag_test.txt";// 演示默认方法IWordTagwordTag =WordTags.file(path);SensitiveWordBssensitiveWordBs =SensitiveWordBs.newInstance() .wordTag(wordTag) .init();Set<String>tagSet =sensitiveWordBs.tags("零售");Assert.assertEquals("[广告, 网络]",tagSet.toString());// 演示指定分隔符IWordTagwordTag2 =WordTags.file(path," ",",");SensitiveWordBssensitiveWordBs2 =SensitiveWordBs.newInstance() .wordTag(wordTag2) .init();Set<String>tagSet2 =sensitiveWordBs2.tags("零售");Assert.assertEquals("[广告, 网络]",tagSet2.toString());
其中dict_tag_test.txt
我们自定义的内容如下:
零售 广告,网络
我们在获取敏感词的时候,是可以设置对应的结果处理策略,从而获取对应的敏感词标签信息
// 自定义测试标签类IWordTagwordTag =WordTags.lines(Arrays.asList("天安门 政治,国家,地址"));// 指定初始化SensitiveWordBssensitiveWordBs =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 */publicinterfaceIWordDeny {/** * 获取结果 * @return 结果 * @since 0.0.13 */List<String>deny();}
比如:
publicclassMyWordDenyimplementsIWordDeny {@OverridepublicList<String>deny() {returnArrays.asList("我的自定义敏感词"); }}
接口如下,可以自定义自己的实现。
返回的列表,表示这个词不是一个敏感词。
/** * 允许的内容-返回的内容不被当做敏感词 * @author binbin.hou * @since 0.0.13 */publicinterfaceIWordAllow {/** * 获取结果 * @return 结果 * @since 0.0.13 */List<String>allow();}
如:
publicclassMyWordAllowimplementsIWordAllow {@OverridepublicList<String>allow() {returnArrays.asList("五星红旗"); }}
接口自定义之后,当然需要指定才能生效。
为了让使用更加优雅,我们设计了引导类SensitiveWordBs
。
可以通过 wordDeny() 指定敏感词,wordAllow() 指定非敏感词,通过 init() 初始化敏感词字典。
SensitiveWordBswordBs =SensitiveWordBs.newInstance() .wordDeny(WordDenys.defaults()) .wordAllow(WordAllows.defaults()) .init();finalStringtext ="五星红旗迎风飘扬,毛主席的画像屹立在天安门前。";Assert.assertTrue(wordBs.contains(text));
备注:init() 对于敏感词 DFA 的构建是比较耗时的,一般建议在应用初始化的时候只初始化一次。而不是重复初始化!
我们可以测试一下自定义的实现,如下:
Stringtext ="这是一个测试,我的自定义敏感词。";SensitiveWordBswordBs =SensitiveWordBs.newInstance() .wordDeny(newMyWordDeny()) .wordAllow(newMyWordAllow()) .init();Assert.assertEquals("[我的自定义敏感词]",wordBs.findAll(text).toString());
这里只有我的自定义敏感词
是敏感词,而测试
不是敏感词。
当然,这里是全部使用我们自定义的实现,一般建议使用系统的默认配置+自定义配置。
可以使用下面的方式。
- 多个敏感词
WordDenys.chains()
方法,将多个实现合并为同一个 IWordDeny。
- 多个白名单
WordAllows.chains()
方法,将多个实现合并为同一个 IWordAllow。
例子:
Stringtext ="这是一个测试。我的自定义敏感词。";IWordDenywordDeny =WordDenys.chains(WordDenys.defaults(),newMyWordDeny());IWordAllowwordAllow =WordAllows.chains(WordAllows.defaults(),newMyWordAllow());SensitiveWordBswordBs =SensitiveWordBs.newInstance() .wordDeny(wordDeny) .wordAllow(wordAllow) .init();Assert.assertEquals("[我的自定义敏感词]",wordBs.findAll(text).toString());
这里都是同时使用了系统默认配置,和自定义的配置。
注意:我们初始化了新的 wordBs,那么用新的 wordBs 去判断。而不是用以前的SensitiveWordHelper
工具方法,工具方法配置是默认的!
实际使用中,比如可以在页面配置修改,然后实时生效。
数据存储在数据库中,下面是一个伪代码的例子,可以参考SpringSensitiveWordConfig.java
要求,版本 v0.0.15 及其以上。
简化伪代码如下,数据的源头为数据库。
MyDdWordAllow 和 MyDdWordDeny 是基于数据库为源头的自定义实现类。
@ConfigurationpublicclassSpringSensitiveWordConfig {@AutowiredprivateMyDdWordAllowmyDdWordAllow;@AutowiredprivateMyDdWordDenymyDdWordDeny;/** * 初始化引导类 * @return 初始化引导类 * @since 1.0.0 */@BeanpublicSensitiveWordBssensitiveWordBs() {SensitiveWordBssensitiveWordBs =SensitiveWordBs.newInstance() .wordAllow(WordAllows.chains(WordAllows.defaults(),myDdWordAllow)) .wordDeny(myDdWordDeny)// 各种其他配置 .init();returnsensitiveWordBs; }}
敏感词库的初始化较为耗时,建议程序启动时做一次 init 初始化。
V0.6.0 以后,添加对应的 benchmark 测试。
测试环境为普通的笔记本:
处理器12th Gen Intel(R) Core(TM) i7-1260P 2.10 GHz机带 RAM16.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 发布 如何支持分布式部署?
05-敏感词之 DFA 算法(Trie Tree 算法)详解
06-敏感词(脏词) 如何忽略无意义的字符?达到更好的过滤效果
v0.19.0-敏感词新特性之敏感词单个编辑,不必重复初始化
About
👮♂️The sensitive word tool for java.(敏感词/违禁词/违法词/脏词。基于 DFA 算法实现的高性能 java 敏感词过滤工具框架。内置支持单词标签分类分级。请勿发布涉及政治、广告、营销、翻墙、违反国家法律法规等内容。高性能敏感词检测过滤组件,附带繁体简体互换,支持全角半角互换,汉字转拼音,模糊搜索等功能。)