文章目录
- sensitive-word
-
- 创作目的
- 特性
- 变更日志
- 更多资料
-
- 敏感词控台
- 敏感词标签文件
- 快速开始
-
- 准备
- Maven 引入
- 核心方法
-
- 判断是否包含敏感词
- 返回第一个敏感词
- 返回所有敏感词
- 默认的替换策略
- 指定替换的内容
- 自定义替换策略
- IWordResultHandler 结果处理类
-
- 使用实例
- 更多特性
-
- 样式处理
-
- 忽略大小写
- 忽略半角圆角
- 忽略数字的写法
- 忽略繁简体
- 忽略英文的书写格式
- 忽略重复词
- 更多检测策略
-
- 邮箱检测
- 连续数字检测
- 网址检测
- 引导类特性配置
-
- 说明
- 配置方法
- 配置说明
- 忽略字符
-
- 说明
- 例子
- 敏感词标签
-
- 说明
- 入门例子
-
- 接口
- 配置文件
- 实现
- 动态加载(用户自定义)
-
- 情景说明
- 接口说明
-
- IWordDeny
- IWordAllow
- 配置使用
-
- 系统的默认配置
- 指定自己的实现
- 同时配置多个
- spring 整合
-
- 背景
- 自定义数据源
- 动态变更
- Benchmark
-
- 环境
- 测试效果记录
- 后期 road-map
- 拓展阅读
- NLP 开源矩阵
sensitive-word
sensitive-word 基于 DFA 算法实现的高性能敏感词工具。
The sensitive word tool for java.(敏感词/违禁词/违法词/脏词。基于 DFA 算法实现的高性能 java 敏感词过滤工具框架。请勿发布涉及政治、广告、营销、翻墙、违反国家法律法规等内容。高性能敏感词检测过滤组件,附带繁体简体互换,支持全角半角互换,汉字转拼音,模糊搜索等功能。)
在线体验
创作目的
实现一款好用敏感词工具。
基于 DFA 算法实现,目前敏感词库内容收录 6W+(源文件 18W+,经过一次删减)。
后期将进行持续优化和补充敏感词库,并进一步提升算法的性能。
希望可以细化敏感词的分类,感觉工作量比较大,暂时没有进行。
特性
-
6W+ 词库,且不断优化更新
-
基于 fluent-api 实现,使用优雅简洁
-
基于 DFA 算法,性能为 7W+ QPS,应用无感
-
支持敏感词的判断、返回、脱敏等常见操作
-
支持常见的格式转换
全角半角互换、英文大小写互换、数字常见形式的互换、中文繁简体互换、英文常见形式的互换、忽略重复词等
-
支持敏感词检测、邮箱检测、数字检测、网址检测等
-
支持自定义替换策略
-
支持用户自定义敏感词和白名单
-
支持数据的数据动态更新(用户自定义),实时生效
-
支持敏感词的标签接口
-
支持跳过一些特殊字符,让匹配更灵活
变更日志
CHANGE_LOG.md
更多资料
敏感词控台
有时候敏感词有一个控台,配置起来会更加灵活方便。
java 如何实现开箱即用的敏感词控台服务?
敏感词标签文件
梳理了大量的敏感词标签文件,可以让我们的敏感词更加方便。
这两个资料阅读可在下方文章获取:
v0.11.0-敏感词新特性及对应标签文件
快速开始
准备
-
JDK1.7+
-
Maven 3.x+
Maven 引入
<dependency> <groupId>com.github.houbb</groupId> <artifactId>sensitive-word</artifactId> <version>0.12.0</version> </dependency>
核心方法
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
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());
WordResultHandlers.raw() 可以保留对应的下标信息:
final String text = "五星红旗迎风飘扬,毛主席的画像屹立在天安门前。"; IWordResult word = SensitiveWordHelper.findFirst(text, WordResultHandlers.raw()); Assert.assertEquals("WordResult{startIndex=0, endIndex=4}", word.toString());
返回所有敏感词
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<IWordResult> wordList = SensitiveWordHelper.findAll(text, WordResultHandlers.raw()); Assert.assertEquals("[WordResult{startIndex=0, endIndex=4}, WordResult{startIndex=9, endIndex=12}, WordResult{startIndex=18, endIndex=21}]", wordList.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); }
其中
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 结果处理类
IWordResultHandler 可以对敏感词的结果进行处理,允许用户自定义。
内置实现见
- WordResultHandlers.word()
只保留敏感词单词本身。
- WordResultHandlers.raw()
保留敏感词相关信息,包含敏感词的开始和结束下标。
- WordResultHandlers.wordTags()
同时保留单词,和对应的词标签信息。
使用实例
所有测试案例参见 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());
- wordTags 例子
我们在
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 = SensitiveWordHelper.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 = SensitiveWordHelper.findAll(text); Assert.assertEquals("[[email protected]]", wordList.toString());
连续数字检测
一般用于过滤手机号/QQ等广告信息。
V0.2.1 之后,支持通过
final String text = "你懂得:12345678"; // 默认检测 8 位 List<String> wordList = SensitiveWordBs.newInstance().init().findAll(text); Assert.assertEquals("[12345678]", wordList.toString()); // 指定数字的长度,避免误杀 List<String> wordList2 = SensitiveWordBs.newInstance() .numCheckLen(9) .init() .findAll(text); Assert.assertEquals("[]", wordList2.toString());
网址检测
用于过滤常见的网址信息。
final String text = "点击链接 www.baidu.com查看答案"; List<String> wordList = SensitiveWordBs.newInstance().init().findAll(text); Assert.assertEquals("[链接, www.baidu.com]", wordList.toString()); Assert.assertEquals("点击** *************查看答案", SensitiveWordBs .newInstance() .init() .replace(text));
引导类特性配置
说明
上面的特性默认都是开启的,有时业务需要灵活定义相关的配置特性。
所以 v0.0.14 开放了属性配置。
配置方法
为了让使用更加优雅,统一使用 fluent-api 的方式定义。
用户可以使用
SensitiveWordBs wordBs = SensitiveWordBs.newInstance() .ignoreCase(true) .ignoreWidth(true) .ignoreNumStyle(true) .ignoreChineseStyle(true) .ignoreEnglishStyle(true) .ignoreRepeat(false) .enableNumCheck(true) .enableEmailCheck(true) .enableUrlCheck(true) .enableWordCheck(true) .numCheckLen(8) .wordTag(WordTags.none()) .charIgnore(SensitiveWordCharIgnores.defaults()) .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 | 是否启用数字检测。 | true |
8 | enableEmailCheck | 是有启用邮箱检测 | true |
9 | enableUrlCheck | 是否启用链接检测 | true |
10 | enableWordCheck | 是否启用敏感单词检测 | true |
11 | numCheckLen | 数字检测,自定义指定长度。 | 8 |
12 | wordTag | 词对应的标签 | none |
13 | charIgnore | 忽略的字符 | none |
忽略字符
说明
我们的敏感词一般都是比较连续的,比如【傻帽】
那就有大聪明发现,可以在中间加一些字符,比如【傻!@#$帽】跳过检测,但是骂人等攻击力不减。
那么,如何应对这些类似的场景呢?
我们可以指定特殊字符的跳过集合,忽略掉这些无意义的字符即可。
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
入门例子
接口
这里只是一个抽象的接口,用户可以自行定义实现。比如从数据库查询等。
public interface IWordTag { /** * 查询标签列表 * @param word 脏词 * @return 结果 */ Set<String> getTag(String word); }
配置文件
我们可以自定义 dict 标签文件,通过 WordTags.file() 创建一个 WordTag 实现。
- dict_tag_test.txt
五星红旗 政治,国家
格式如下:
敏感词 tag1,tag2
实现
具体的效果如下,在引导类设置一下即可。
默认的 wordTag 是空的。
String filePath = "dict_tag_test.txt"; IWordTag wordTag = WordTags.file(filePath); SensitiveWordBs sensitiveWordBs = SensitiveWordBs.newInstance() .wordTag(wordTag) .init(); Assert.assertEquals("[政治, 国家]", sensitiveWordBs.tags("五星红旗").toString());;
后续会考虑引入一个内置的标签文件策略。
动态加载(用户自定义)
情景说明
有时候我们希望将敏感词的加载设计成动态的,比如控台修改,然后可以实时生效。
v0.0.13 支持了这种特性。
接口说明
为了实现这个特性,并且兼容以前的功能,我们定义了两个接口。
IWordDeny
接口如下,可以自定义自己的实现。
返回的列表,表示这个词是一个敏感词。
/** * 拒绝出现的数据-返回的内容被当做是敏感词 * @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("我的自定义敏感词"); } }
IWordAllow
接口如下,可以自定义自己的实现。
返回的列表,表示这个词不是一个敏感词。
/** * 允许的内容-返回的内容不被当做敏感词 * @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("五星红旗"); } }
配置使用
接口自定义之后,当然需要指定才能生效。
为了让使用更加优雅,我们设计了引导类
可以通过 wordDeny() 指定敏感词,wordAllow() 指定非敏感词,通过 init() 初始化敏感词字典。
系统的默认配置
SensitiveWordBs wordBs = SensitiveWordBs.newInstance() .wordDeny(WordDenys.system()) .wordAllow(WordAllows.system()) .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());
这里只有
当然,这里是全部使用我们自定义的实现,一般建议使用系统的默认配置+自定义配置。
可以使用下面的方式。
同时配置多个
- 多个敏感词
- 多个白名单
例子:
String text = "这是一个测试。我的自定义敏感词。"; IWordDeny wordDeny = WordDenys.chains(WordDenys.system(), new MyWordDeny()); IWordAllow wordAllow = WordAllows.chains(WordAllows.system(), new MyWordAllow()); SensitiveWordBs wordBs = SensitiveWordBs.newInstance() .wordDeny(wordDeny) .wordAllow(wordAllow) .init(); Assert.assertEquals("[我的自定义敏感词]", wordBs.findAll(text).toString());
这里都是同时使用了系统默认配置,和自定义的配置。
注意:我们初始化了新的 wordBs,那么用新的 wordBs 去判断。而不是用以前的
spring 整合
背景
实际使用中,比如可以在页面配置修改,然后实时生效。
数据存储在数据库中,下面是一个伪代码的例子,可以参考 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.system(), myDdWordAllow)) .wordDeny(myDdWordDeny) // 各种其他配置 .init(); return sensitiveWordBs; } }
敏感词库的初始化较为耗时,建议程序启动时做一次 init 初始化。
动态变更
为了保证敏感词修改可以实时生效且保证接口的尽可能简化,此处没有新增 add/remove 的方法。
而是在调用
因为初始化可能耗时较长(秒级别),所有优化为 init 未完成时不影响旧的词库功能,完成后以新的为准。
@Component public class SensitiveWordService { @Autowired private SensitiveWordBs sensitiveWordBs; /** * 更新词库 * * 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法。 * 如果需要生效,则调用这个方法。 * * 说明:重新初始化不影响旧的方法使用。初始化完成后,会以新的为准。 */ public void refresh() { // 每次数据库的信息发生变化之后,首先调用更新数据库敏感词库的方法,然后调用这个方法。 sensitiveWordBs.init(); } }
如上,你可以在数据库词库发生变更时,需要词库生效,主动触发一次初始化
其他使用保持不变,无需重启应用。
Benchmark
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 | 满足大部分场景 |
后期 road-map
-
移除单个汉字的敏感词,在中国,要把词组当做一次词,降低误判率。
-
支持单个的敏感词变化?
remove、add、edit?
-
敏感词标签接口支持
-
敏感词处理时标签支持
-
wordData 的内存占用对比 + 优化
-
用户指定自定义的词组,同时允许指定词组的组合获取,更加灵活
FormatCombine/CheckCombine/AllowDenyCombine 组合策略,允许用户自定义。
-
word check 策略的优化,统一遍历+转换
-
添加 ThreadLocal 等性能优化
拓展阅读
敏感词工具实现思路
DFA 算法讲解
敏感词库优化流程
java 如何实现开箱即用的敏感词控台服务?
v0.11.0-敏感词新特性及对应标签文件
NLP 开源矩阵
pinyin 汉字转拼音
pinyin2hanzi 拼音转汉字
segment 高性能中文分词
opencc4j 中文繁简体转换
nlp-hanzi-similar 汉字相似度
word-checker 拼写检测
sensitive-word 敏感词