1、 敏感词过滤 技术文档目录1. 背景 32. 算法描述 33. 效率的瓶颈 54. 使用详解 64.1 public static IList GetKeyWordArray(IList KeyWordList, bool IsIgnoreCase) 64.2 public static IList GetKeyWordArray(string KeyWordList, bool IsIgnoreCase) 64.3 public string FilterSingleThread(string Content, IList SensitiveArray, char ReplaceChar,
2、bool IsIgnoreCase) 64.4 public string FilterSingleThread(string Content, IList SensitiveArray) .74.5 public string FilterMuliThread(string Content, IList SensitiveArray, int ThreadCount, char ReplaceChar, bool IsIgnoreCase) .74.6 public string FilterMuliThread(string Content, IList SensitiveArray, i
3、nt ThreadCount)74.7 public string Replace(string Content, IList KeyWordArray, string LeftStr,string RightStr, bool IsIgnoreCase)84.8 public string Replace(string Content, IList KeyWordArray, string LeftStr, string RightStr) 85. 一些细节 86. 其他 91.背景在天朝,很多东西都要被过滤。鉴于各大网站论坛博客等都有敏感词过滤,*网站的社区部分当然也要作相应处理。2.算法
4、描述首先要根据关键词表的首字符建立索引(将 char 转成 int 型) 。IList arr = new List65535;因为 Unicode 码最大是 65535 个,所以建立一个 65535 大小的数组,相同首字符的关键词放入 string 的泛型集合中。注意:关键词表最好是预先根据长度降序排好序的。比如, “法 O 功”和“法 O 功组织”都是关键字,需要过滤的文本内容为“今天法 O 功组织集合” ,如果前者比后者先执行过滤的话,过滤后就变成“今天*组织集合” ,但理想情况是“今天*集合” ,所以要把长的关键词放前面。接着逐个读取目标字符串的字符,转成 int 型后判断索引数组中是
5、否有值,为空则将该字符添加到结果集中(一个 StringBuilder) ;不为空则循环该首字符开头的关键词,逐个字符比较,有匹配的则以等长的特殊字符代替(默认是* 号) 。模拟步骤如下:关键词表为:“法 O 功组织” , “法 O 功” , “李洪志”字符“法”转成 int 为 27861字符“李”转成 int 为 26446因此索引数组的下标 27861 的值为长度 2 的泛型集合,下标 26446 的值为长度 1 的泛型集合,其余都为空。目标字符串为“今天法 O 功组织集合,李洪志发表李洪”1 逐个字符读取, “今”和“天”相应的索引数组的值为空,将它们放入结果集中。 (红色字符表示当前
6、下标读取的字符)目标:今天法 O 功组织集合,李洪志发表李洪结果:今天2 “法“对应的索引数组值不为空,则循环该泛型集合,逐个字符比对目标:今天法 O 功组织集 合,李洪志发表李洪泛型集合: 法 O 功组织完全匹配,则结果集中放入 5 个*号。结果:今天*3 “集” “合” “, ”对应的索引数组值都为空,将它们放入结果集中。目标:今天法 O 功组织集合,李洪志发表李洪结果:今天* 集合,4 “李“对应的索引数组值不为空,则循环该泛型集合,逐个字符比对目标:今天法 O 功组织集合,李洪志发表李洪泛型集合: 李洪志完全匹配,则结果集中放入 3 个*号。结果:今天* 集合,*5 “发” “表”对应
7、的索引数组值都为空,将它们放入结果集中。目标:今天法 O 功组织集合,李洪志发表李洪结果:今天* 集合,*发表6 “李“对应的索引数组值不为空,则循环该泛型集合,逐个字符比对目标:今天法 O 功组织集合,李洪志发表李洪泛型集合: 李洪志不匹配,将“李“放入结果集中。结果:今天* 集合,*发表李7 “洪”对应的索引数组值都为空,将它放入结果集中。目标:今天法 O 功组织集合,李洪志发表李洪( 读到末尾 )结果:今天* 集合,*发表李洪最后过滤结果就是:“今天*集合,* 发表李洪”当执行替换操作时,只是在关键字匹配的时候,在关键字左右加上给定的字符串,其余和过滤操作一样。比如将“周公”替换为“-周
8、公=”3.效率的瓶颈最开始的版本,判断当前下标后面的字符串时候与关键词匹配,用SubString 方法,过滤 200K 的文本需要 143 秒时间;然后改成逐个字符去比对,时间减少到 11 秒;接着用首字符作索引,减少到 0.078 秒;最后对new 结果集 StringBuilder 对象时指定了目标字符串的长度,并且逐个字符比对关键词时从下一个字符开始(因为当前字符可由对应的索引数组不为空得出肯定匹配的结论) ,最后耗时为 0.048 秒。后来考虑可以将目标字符串平均拆分,开启几个线程同时去过滤,但是效率提升效果不是很明显(详见 ) 。几 种 算 法 的 速 度 比较 .doc结论:Sub
9、String 方法效率极低,建议不要在循环次数很多的时候使用。如果能预先知道 StringBuilder 的长度或者是大概的范围, new 该对象的时候指定,能大大提高效率。 (因为如果不指定大小,它内部是预先设成长度为 16,当容量不够时,自动翻倍,这时会有一个数组扩容和复制内容的过程,耗时不小) ,这同样适用于集合,比如 List多线程不一定总是能提高效率,在数据量没达到某个数量级的时候,系统维护线程池所带来的开销不一定能抵过多线程带来的效率。4.使用详解4.1 public static IList GetKeyWordArray(IList KeyWordList, bool IsIg
10、noreCase)方法解释:静态方法,用于根据关键词列表的首字符获取索引数组。当设为不分大小写时,首字符的大写和小写对应的索引都会添加。索引数组建议缓存起来。参数:KeyWordList: 关键词列表IsIgnoreCase:是否不分大小写4.2 public static IList GetKeyWordArray(string KeyWordList, bool IsIgnoreCase)方法解释:静态方法,用于根据关键词列表的首字符获取索引数组。当设为不分大小写时,首字符的大写和小写对应的索引都会添加。索引数组建议缓存起来。参数:KeyWordList: 关键词列表IsIgnoreCas
11、e:是否不分大小写4.3 public string FilterSingleThread(string Content, IList SensitiveArray, char ReplaceChar, bool IsIgnoreCase)方法解释:单线程处理过滤,根据给定的敏感词过滤表数组过滤给定字符串,(适合在数据量小的时候),替换成指定字符。参数:Content: 需要处理的字符串SensitiveArray: 索引数组ReplaceChar: 替换后的字符IsIgnoreCase:是否不分大小写,注意这里最好与调用GetKeyWordArray 方法的 IsIgnoreCase 参数一
12、致。4.4 public string FilterSingleThread(string Content, IList SensitiveArray)方法解释:单线程处理过滤,根据给定的敏感词过滤表数组过滤给定字符串,不分大小写,(适合在数据量小的时候),替换成*号。参数:Content: 需要处理的字符串SensitiveArray: 索引数组4.5 public string FilterMuliThread(string Content, IList SensitiveArray, int ThreadCount, char ReplaceChar, bool IsIgnoreCase
13、)方法解释:多线程处理过滤,根据给定的敏感词过滤表数组过滤给定字符串,(适合在数据量大的时候),替换成指定字符。参数:Content: 需要处理的字符串SensitiveArray: 索引数组ThreadCount: 线程数量。仅测试,对 8M 以内的数据设为 2 就够了,多了效果也不明显。ReplaceChar: 替换后的字符IsIgnoreCase:是否不分大小写,注意这里最好与调用GetKeyWordArray 方法的 IsIgnoreCase 参数一致。4.6 public string FilterMuliThread(string Content, IList SensitiveA
14、rray, int ThreadCount)方法解释:多线程处理过滤,根据给定的敏感词过滤表数组过滤给定字符串,不分大小写,(适合在数据量大的时候),替换成*号。参数:Content: 需要处理的字符串SensitiveArray: 索引数组ThreadCount: 线程数量。仅测试,对 8M 以内的数据设为 2 就够了,多了效果也不明显。4.7 public string Replace(string Content, IList KeyWordArray, string LeftStr,string RightStr, bool IsIgnoreCase)方法解释:执行替换操作,根据给定的
15、关键词替换表数组替换给定字符串,对符合条件的关键词左右加上给定的字符串。替换操作目前不提供多线程版本。参数:Content: 需要处理的字符串KeyWordArray: 索引数组LeftStr: 在左边加上的字符串RightStr: 在右边加上的字符串IsIgnoreCase:是否不分大小写,注意这里最好与调用GetKeyWordArray 方法的 IsIgnoreCase 参数一致。4.8 public string Replace(string Content, IList KeyWordArray, string LeftStr, string RightStr)方法解释:执行替换操作,
16、根据给定的关键词替换表数组替换给定字符串,对符合条件的关键词左右加上给定的字符串,不分大小写。替换操作目前不提供多线程版本。参数:Content: 需要处理的字符串KeyWordArray: 索引数组LeftStr: 在左边加上的字符串RightStr: 在右边加上的字符串5.一些细节在以多线程作过滤,用 KeyWordThreadMOD 作为辅助实体类,记录某个线程中处理的目标字符串的起始下标、结束下标以及结果集、是否处理完毕的信息。当开了线程去执行时,以一个死循环来拖住主线程,在里面判断是否所有的线程都已处理完毕,处理完了才跳出死循环,如果没处理完,则让主线程睡眠 10 毫秒,然后继续死循环。粗略估计,数据量在 500K 以内采用单线程,500K 以上 8M 以下采用 2个线程比较好,更大数据可以多开几个线程。6.其他编写 时,还没有编写替换操作的方法;而替换操作与过几 种 算 法 的 速 度 比较 .doc滤操作的公共算法操作我写到一个方法内,这样导致过滤操作的结果会比该文档中的结果慢几十毫秒(操作 8.61M 内容) 。在对关键词逐个字符匹配的操作时,曾考虑过将 KMP 算法融合进来,但尝试了一下发现速度反而更慢。当关键词达到很大的数量时,可以考虑在以首字符作索引的前提下,再作二级索引,或者用树状结构代替现在的链式结构。