一款高性能敏感词(非法词/脏字)检测过滤组件ToolGood.Words,附带繁体简体互换,汉字转拼音...

大家好,我是宝弟!

今天给大家推荐一款高性能敏感词(非法词/脏字)检测过滤组件ToolGood.Words,附带繁体简体互换,支持全角半角互换,汉字转拼音,模糊搜索等功能。

C#使用StringSearchEx2.Replace 过滤,在48k敏感词库上的过滤速度超过3亿字符每秒。(cpu i7 8750h).

项目结构
ToolGood.Pinyin.Build:          生成词的拼音ToolGood.Pinyin.Pretreatment:   生成拼音预处理,核对拼音,词组最小化ToolGood.Transformation.Build:生成简体繁体转换文档,更新时文档放在同一目录下,词库参考 https://github.com/BYVoid/OpenCCToolGood.Words.Contrast:        字符串搜索对比ToolGood.Words.Test:            单元测试ToolGood.Words:                 本项目源代码
敏感词过滤提供的方法

非法词(敏感词)检测类:StringSearchStringSearchExStringSearchEx2WordsSearchWordsSearchExWordsSearchEx2IllegalWordsSearchStringMatchStringMatchExWordsMatchWordsMatchEx

  • StringSearchStringSearchExStringSearchEx2StringSearchEx3: 搜索FindFirst方法返回结果为string类型。

  • WordsSearchWordsSearchExWordsSearchEx2WordsSearchEx3: 搜索FindFirst方法返回结果为WordsSearchResult类型,WordsSearchResult不仅仅有关键字,还有关键字的开始位置、结束位置,关键字序号等。

  • IllegalWordsSearch: 过滤非法词(敏感词)专用类,可设置跳字长度,默认全角转半角,忽略大小写,跳词,重复词,黑名单, 搜索FindFirst方法返回为IllegalWordsSearchResult,有关键字,对应原文,开始、位置,黑名单类型。

  • IllegalWordsSearchStringSearchExStringSearchEx2WordsSearchExWordsSearchEx2 使用SaveLoad方法,可以加快初始化。

  • 共同方法有:SetKeywordsContainsAnyFindFirstFindAllReplace

  • IllegalWordsSearch独有方法:SetSkipWords(设置跳词)、SetBlacklist(设置黑名单)。

  • IllegalWordsSearch字段UseIgnoreCase:设置是否忽略大小写,必须在SetKeywords方法之前,注:使用Load方法则该字段无效。

  • StringSearchEx3WordsSearchEx3为指针版优化版,实测时发现性能浮动比较大。

  • StringMatchStringMatchExWordsMatchWordsMatchEx支持部分正则表达式类型:.(点)?(问号) [](方括号) (|)(括号与竖线)

使用Demo

通过了解ToolGood.Words提供的方法,我们可以知道它可以针对敏感词及其拼音、跳词等变形进行检测,在实际的应用场景中能满足大部分的需求。本实例中,使用.NET Core进行展示。

1
 

ValidationAttribute

我们先定义两个简单的模型来绑定输入参数,一个是只要输入含有敏感词就会报错,一个是只要输入含有敏感词就会把相关的字符串替换为 * ,代码如下:

public class MinganCheckInput{    [MinGanCheck]    public string Text { get; set; }}public class MinganReplaceInput{    [MinGanReplace]    public string Text { get; set; }

其中 [MinGanCheck] 和 [MinGanReplace] 是我们定义的特性标记,将其继承 ValidationAttribute,就和我们常用的 [Required] 一样方便。

/// <summary>/// 敏感词检查的特性,一匹配就抛异常/// </summary>[AttributeUsage(AttributeTargets.Property)]public class MinGanCheck : ValidationAttribute{    protected override ValidationResult IsValid(object value, ValidationContext validationContext)    {        throw new NotImplementedException();    }}/// <summary>/// 敏感词替换/// </summary>[AttributeUsage(AttributeTargets.Property)]public class MinGanReplace : ValidationAttribute{    protected override ValidationResult IsValid(object value, ValidationContext validationContext)    {        return ValidationResult.Success;    }}

接下来就是实现 ValidationAttribute 的功能,如果看过水弟写过的 aop 文章,这时候就不会直接在校验的方法中直接引入 ToolGood.Words ,这样会带来很大的耦合,也不便于我们替换为其他的敏感词组件或服务。所以我们只要再多一层抽象就可以了。

// 检查protected override ValidationResult IsValid(object value, ValidationContext validationContext){    return validationContext.GetService<IMinGanCheckValidator>().IsValid(value, validationContext);}// 替换protected override ValidationResult IsValid(object value, ValidationContext validationContext){    validationContext.GetService<IMinGanReplaceValidator>().IsValid(value, validationContext);    return ValidationResult.Success;}

接着我们分别实现 IMinGanCheckValidator 和 IMinGanReplaceValidator 的功能,也即检查和替换功能。

// 检查public class MinGanCheckValidator : IMinGanCheckValidator{    public ValidationResult IsValid(object value, ValidationContext validationContext)    {        if (value is string v)        {            if (!String.IsNullOrEmpty(v))            {                // 文字检查                if (MinGanProvider.Instance.IllegalWordsSearch.ContainsAny(v))                {                    return new ValidationResult("存在敏感词", new[] { validationContext.MemberName });                }                // 检查拼音                if (MinGanProvider.Instance.IllegalWordsSearch.ContainsAny(WordsHelper.GetPinyin(v)))                {                    return new ValidationResult("存在敏感词", new[] { validationContext.MemberName });                }                // todo:其他变种            }        }        return ValidationResult.Success;    }}//替换public class MinGanReplaceValidator : IMinGanReplaceValidator{    public void Replace(object value, ValidationContext validationContext)    {        if (value is string v)        {            if (!String.IsNullOrEmpty(v))            {                v = MinGanProvider.Instance.IllegalWordsSearch.Replace(v);                SetPropertyByName(validationContext.ObjectInstance, validationContext.MemberName, v);            }        }    }    static bool SetPropertyByName(Object obj, string name, Object value)    {        var type = obj.GetType();        var prop = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);        if (null == prop || !prop.CanWrite) return false;        prop.SetValue(obj, value, null);        return true;    }}

其中 MinGanProvider.Instance.IllegalWordsSearch 是 ToolGood.Words 中的检测类单例,这里不详细展开。这样我们就有一个大概能用的敏感词检测组件了,然而在实际过程中,我们还需要对敏感词进行管理,特别是需要实时更新敏感词。

2
 

敏感词热重载

以 json 配置文件存放敏感词为例,只需要配置热重载就行了。

首先是 Program.cs 文件中让 json 配置文件热重载。

public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)        .ConfigureAppConfiguration((builderContext, config) =>        {            config.AddJsonFile("IllegalKeywords.json", optional: false, reloadOnChange: true);// 配置可热重载        })        .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });

最后是在 Startup.cs 中文件处理重载事件。

ChangeToken.OnChange(() => Configuration.GetReloadToken(), () =>    {        // 敏感词重载    var keys = Configuration.GetSection("IllegalKeywords").Get<List<string>>();    if (keys!=null&&keys.Any())    {        var allKeys = new List<string>();        foreach (var k in keys)        {            allKeys.Add(k); // 增加词汇            allKeys.Add(WordsHelper.ToTraditionalChinese(k)); // 增加繁体            allKeys.Add(WordsHelper.GetPinyin(k)); // 增加拼音        }IllegalWordsSearch.SetKeywords(allKeys);    }    });
3
 

运行效果

一款高性能敏感词(非法词/脏字)检测过滤组件ToolGood.Words,附带繁体简体互换,汉字转拼音...

一款高性能敏感词(非法词/脏字)检测过滤组件ToolGood.Words,附带繁体简体互换,汉字转拼音...

繁简体互换
    // 转成简体    WordsHelper.ToSimplifiedChinese("我愛中國");    WordsHelper.ToSimplifiedChinese("我愛中國",1);// 港澳繁体 转 简体    WordsHelper.ToSimplifiedChinese("我愛中國",2);// 台湾正体 转 简体    // 转成繁体    WordsHelper.ToTraditionalChinese("我爱中国");    WordsHelper.ToTraditionalChinese("我爱中国",1);// 简体 转 港澳繁体    WordsHelper.ToTraditionalChinese("我爱中国",2);// 简体 转 台湾正体    // 转成全角    WordsHelper.ToSBC("abcABC123");    // 转成半角    WordsHelper.ToDBC("abcABC123");    // 数字转成中文大写    WordsHelper.ToChineseRMB(12345678901.12);    // 中文转成数字    WordsHelper.ToNumber("壹佰贰拾叁亿肆仟伍佰陆拾柒万捌仟玖佰零壹元壹角贰分");    // 获取全拼    WordsHelper.GetPinyin("我爱中国");//WoAiZhongGuo       WordsHelper.GetPinyin("我爱中国",",");//Wo,Ai,Zhong,Guo       WordsHelper.GetPinyin("我爱中国",true);//WǒÀiZhōngGuó    // 获取首字母    WordsHelper.GetFirstPinyin("我爱中国");//WAZG    // 获取全部拼音    WordsHelper.GetAllPinyin('传');//Chuan,Zhuan    // 获取姓名    WordsHelper.GetPinyinForName("单一一")//ShanYiYi    WordsHelper.GetPinyinForName("单一一",",")//Shan,Yi,Yi    WordsHelper.GetPinyinForName("单一一",true)//ShànYīYī
拼音匹配

PinyinMatch:方法有SetKeywordsSetIndexsFindFindIndex

PinyinMatch<T>:方法有SetKeywordsFuncSetPinyinFuncSetPinyinSplitCharFind

    string s = "北京|天津|河北|辽宁|吉林|黑龙江|山东|江苏|上海|浙江|安徽|福建|江西|广东|广西|海南|河南|湖南|湖北|山西|内蒙古|宁夏|青海|陕西|甘肃|新疆|四川|贵州|云南|重庆|西藏|香港|澳门|台湾";    PinyinMatch match = new PinyinMatch();    match.SetKeywords(s.Split('|').ToList());    var all = match.Find("BJ");    Assert.AreEqual("北京", all[0]);    Assert.AreEqual(1, all.Count);    all = match.Find("北J");    Assert.AreEqual("北京", all[0]);    Assert.AreEqual(1, all.Count);    all = match.Find("北Ji");    Assert.AreEqual("北京", all[0]);    Assert.AreEqual(1, all.Count);    all = match.Find("S");    Assert.AreEqual("山东", all[0]);    Assert.AreEqual("江苏", all[1]);    var all2 = match.FindIndex("BJ");    Assert.AreEqual(0, all2[0]);    Assert.AreEqual(1, all.Count);

 资源获取方式 

ToolGood.Words:

https://github.com/toolgood/ToolGood.Words

Demo地址:

https://github.com/jonechenug/ToolGood.Words.Sample