IllegalWordDetection.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.Concurrent;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. namespace CSharpUtil.Text
  8. {
  9. /// <summary>
  10. /// 此算法思想来源于“http://www.cnblogs.com/sumtec/archive/2008/02/01/1061742.html”,
  11. /// 经测试,检测 "屄defg东正教dsa SofU ckd臺灣青年獨立聯盟daoiuq 样什么J& b玩意 日你先人"
  12. /// 这个字符串并替换掉敏感词平均花费2.7ms
  13. /// importor:gwang 2017.03.18
  14. /// from: https://github.com/NewbieGameCoder/IllegalWordsDetection (by MIT License)
  15. /// author:596809147@qq.com (Unity3d技术群)
  16. /// </summary>
  17. static public class IllegalWordDetection
  18. {
  19. #region ' 变量 '
  20. /// <summary>
  21. /// 存了所有的长度大于1的敏感词汇
  22. /// </summary>
  23. static HashSet<string> wordsSet = new HashSet<string>();
  24. /// <summary>
  25. /// 存了某一个词在所有敏感词中的位置,(超出8个的截断为第8个位置)
  26. /// </summary>
  27. static byte[] fastCheck = new byte[char.MaxValue];
  28. /// <summary>
  29. /// 存了所有敏感词的长度信息,“Key”值为所有敏感词的第一个词,敏感词的长度会截断为8
  30. /// </summary>
  31. static byte[] fastLength = new byte[char.MaxValue];
  32. /// <summary>
  33. /// 保有所有敏感词汇的第一个词的记录,可用来判断是否一个词是一个或者多个敏感词汇的“第一个词”,
  34. /// 且可判断以某一个词作为第一个词的一系列的敏感词的最大的长度
  35. /// </summary>
  36. static byte[] startCache = new byte[char.MaxValue];
  37. static char[] dectectedBuffer = null;
  38. static string SkipList = " \t\r\n~!@#$%^&*()_+-=【】、{}|;':\",。、《》?αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩①②③④⑤⑥⑦⑧⑨⑩⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇≈≡≠=≤≥<>≮≯∷±+-×÷/∫∮∝∞∧∨∑∏∪∩∈∵∴⊥∥∠⌒⊙≌∽√§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\︿_ ̄―♂♀┌┍┎┐┑┒┓─┄┈├┝┞┟┠┡┢┣│┆┊┬┭┮┯┰┱┲┳┼┽┾┿╀╁╂╃└┕┖┗┘┙┚┛━┅┉┤┥┦┧┨┩┪┫┃┇┋┴┵┶┷┸┹┺┻╋╊╉╈╇╆╅╄";
  39. static BitArray SkipBitArray = new BitArray(char.MaxValue);
  40. /// <summary>
  41. /// 保有所有敏感词汇的最后一个词的记录,仅用来判断是否一个词是一个或者多个敏感词汇的“最后一个词”
  42. /// </summary>
  43. static BitArray endCache = new BitArray(char.MaxValue);
  44. #endregion
  45. #region ' 内部 函数 '
  46. unsafe public static void Init(string[] badwords)
  47. {
  48. if (badwords == null || badwords.Length == 0)
  49. return;
  50. int wordLength = 0;
  51. int maxWordLength = int.MinValue;
  52. for (int stringIndex = 0, len = badwords.Length; stringIndex < len; ++stringIndex)
  53. {
  54. if (string.IsNullOrEmpty(badwords[stringIndex]))
  55. continue;
  56. string strBadWord = OriginalToLower(badwords[stringIndex]);
  57. //求得单个的敏感词汇的长度
  58. wordLength = strBadWord.Length;
  59. maxWordLength = Math.Max(wordLength, maxWordLength);
  60. fixed (char* pWordStart = strBadWord)
  61. {
  62. for (int i = 0; i < wordLength; ++i)
  63. {
  64. //准确记录8位以内的敏感词汇的某个词在词汇中的“位置”
  65. if (i < 7)
  66. fastCheck[*(pWordStart + i)] |= (byte)(1 << i);
  67. else//8位以外的敏感词汇的词直接限定在第8位
  68. fastCheck[*(pWordStart + i)] |= 0x80;//0x80在内存中即为1000 0000,因为一个byte顶多标示8位,故超出8位的都位或上0x80,截断成第8位
  69. }
  70. //缓存敏感词汇的长度
  71. int cachedWordslength = Math.Min(8, wordLength);
  72. char firstWord = *pWordStart;
  73. //记录敏感词汇的“大致长度(超出8个字的敏感词汇会被截取成8的长度)”,“key”值为敏感词汇的第一个词
  74. fastLength[firstWord] |= (byte)(1 << (cachedWordslength - 1));
  75. //缓存出当前以badWord第一个字开头的一系列的敏感词汇的最长的长度
  76. if (startCache[firstWord] < cachedWordslength)
  77. startCache[firstWord] = (byte)(cachedWordslength);
  78. //存好敏感词汇的最后一个词汇的“出现情况”
  79. endCache[*(pWordStart + wordLength - 1)] = true;
  80. //将长度大于1的敏感词汇都压入到字典中
  81. if (!wordsSet.Contains(strBadWord))
  82. wordsSet.Add(strBadWord);
  83. }
  84. }
  85. // 初始化好一个用来存检测到的字符串的buffer
  86. dectectedBuffer = new char[maxWordLength];
  87. // 记录应该跳过的不予检测的词
  88. fixed (char* start = SkipList)
  89. {
  90. char* itor = start;
  91. char* end = start + SkipList.Length;
  92. while (itor < end)
  93. SkipBitArray[*itor++] = true;
  94. }
  95. LogHelper.Log($"敏感词初始化完毕. 线程{Thread.CurrentThread.ManagedThreadId}");
  96. }
  97. unsafe static string OriginalToLower(string text)
  98. {
  99. fixed (char* newText = text)
  100. {
  101. char* itor = newText;
  102. char* end = newText + text.Length;
  103. char c;
  104. while (itor < end)
  105. {
  106. c = *itor;
  107. if ('A' <= c && c <= 'Z')
  108. {
  109. *itor = (char)(c | 0x20);
  110. }
  111. ++itor;
  112. }
  113. }
  114. return text;
  115. }
  116. unsafe static bool EnsuranceLower(string text)
  117. {
  118. fixed (char* newText = text)
  119. {
  120. char* itor = newText;
  121. char* end = newText + text.Length;
  122. char c;
  123. while (itor < end)
  124. {
  125. c = *itor;
  126. if ('A' <= c && c <= 'Z')
  127. {
  128. return true;
  129. }
  130. ++itor;
  131. }
  132. }
  133. return false;
  134. }
  135. #endregion
  136. #region ' 方法 '
  137. /// <summary>
  138. /// 过滤字符串,默认遇到敏感词汇就以'*'代替
  139. /// </summary>
  140. /// <param name="text"></param>
  141. /// <param name="mask"></param>
  142. /// <returns></returns>
  143. unsafe public static string Filter(string text, string mask = "*")
  144. {
  145. Dictionary<int, int> dic = DetectIllegalWords(text);
  146. //如果没有敏感词汇,则直接返回出去
  147. if (dic.Count == 0)
  148. return text;
  149. fixed (char* newText = text, cMask = mask)
  150. {
  151. var itor = newText;
  152. Dictionary<int, int>.Enumerator enumerator = dic.GetEnumerator();
  153. //开始替换敏感词汇
  154. while (enumerator.MoveNext())
  155. {
  156. //偏移到敏感词出现的位置
  157. itor = newText + enumerator.Current.Key;
  158. for (int index = 0; index < enumerator.Current.Value; index++)
  159. {
  160. //屏蔽掉敏感词汇
  161. *itor++ = *cMask;
  162. }
  163. }
  164. enumerator.Dispose();
  165. }
  166. return text;
  167. }
  168. /// <summary>
  169. /// 判断text是否有敏感词汇,如果有返回敏感的词汇的位置,利用指针操作来加快运算速度
  170. /// </summary>
  171. /// <param name="text"></param>
  172. /// <returns></returns>
  173. unsafe public static Dictionary<int, int> DetectIllegalWords(string text)
  174. {
  175. var findResult = new Dictionary<int, int>();
  176. if (string.IsNullOrEmpty(text))
  177. return findResult;
  178. if (EnsuranceLower(text))
  179. text = text.ToLower();
  180. fixed (char* ptext = text, detectedStrStart = dectectedBuffer)
  181. {
  182. //缓存字符串的初始位置
  183. char* itor = (fastCheck[*ptext] & 0x01) == 0 ? ptext + 1 : ptext;
  184. //缓存字符串的末尾位置
  185. char* end = ptext + text.Length;
  186. while (itor < end)
  187. {
  188. //如果text的第一个词不是敏感词汇或者当前遍历到了text第一个词的后面的词,则循环检测到text词汇的倒数第二个词,看看这一段子字符串中有没有敏感词汇
  189. if ((fastCheck[*itor] & 0x01) == 0)
  190. {
  191. while (itor < end - 1 && (fastCheck[*(++itor)] & 0x01) == 0)
  192. ;
  193. }
  194. //如果有只有一个词的敏感词,且当前的字符串的“非第一个词”满足这个敏感词,则先加入已检测到的敏感词列表
  195. if (startCache[*itor] != 0 && (fastLength[*itor] & 0x01) > 0)
  196. {
  197. //返回敏感词在text中的位置,以及敏感词的长度,供过滤功能用
  198. findResult.Add((int)(itor - ptext), 1);
  199. }
  200. char* strItor = detectedStrStart;
  201. *strItor++ = *itor;
  202. int remainLength = (int)(end - itor - 1);
  203. int skipCount = 0;
  204. //此时已经检测到一个敏感词的“首词”了,记录下第一个检测到的敏感词的位置
  205. //从当前的位置检测到字符串末尾
  206. for (int i = 1; i <= remainLength; ++i)
  207. {
  208. char* subItor = itor + i;
  209. // 跳过一些过滤的字符,比如空格特殊符号之类的
  210. if (SkipBitArray[*subItor])
  211. {
  212. ++skipCount;
  213. continue;
  214. }
  215. //如果检测到当前的词在所有敏感词中的位置信息中没有处在第i位的,则马上跳出遍历
  216. if ((fastCheck[*subItor] >> Math.Min(i - skipCount, 7)) == 0)
  217. {
  218. break;
  219. }
  220. *strItor++ = *subItor;
  221. //如果有检测到敏感词的最后一个词,并且此时的“检测到的敏感词汇”的长度也符合要求,则才进一步查看检测到的敏感词汇是否是真的敏感
  222. if ((fastLength[*itor] >> Math.Min(i - 1 - skipCount, 7)) > 0 && endCache[*subItor])
  223. {
  224. //如果此子字符串在敏感词字典中存在,则记录。做此判断是避免敏感词中夹杂了其他敏感词的单词,而上面的算法无法剔除,故先用hash数组来剔除
  225. //上述算法是用于减少大部分的比较消耗
  226. if (wordsSet.Contains(new string(dectectedBuffer, 0, (int)(strItor - detectedStrStart))))
  227. {
  228. int curDectectedStartIndex = (int)(itor - ptext);
  229. findResult[curDectectedStartIndex] = i + 1;
  230. itor = subItor;
  231. break;
  232. }
  233. }
  234. else if (i - skipCount > startCache[*itor] && startCache[*itor] < 0x80)
  235. {//如果超过了以该词为首的一系列的敏感词汇的最大的长度,则不继续判断(前提是该词对应的所有敏感词汇没有超过8个词的)
  236. break;
  237. }
  238. }
  239. ++itor;
  240. }
  241. }
  242. return findResult;
  243. }
  244. #endregion
  245. }
  246. }