using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace CSharpUtil.Text
{
///
/// 此算法思想来源于“http://www.cnblogs.com/sumtec/archive/2008/02/01/1061742.html”,
/// 经测试,检测 "屄defg东正教dsa SofU ckd臺灣青年獨立聯盟daoiuq 样什么J& b玩意 日你先人"
/// 这个字符串并替换掉敏感词平均花费2.7ms
/// importor:gwang 2017.03.18
/// from: https://github.com/NewbieGameCoder/IllegalWordsDetection (by MIT License)
/// author:596809147@qq.com (Unity3d技术群)
///
static public class IllegalWordDetection
{
#region ' 变量 '
///
/// 存了所有的长度大于1的敏感词汇
///
static HashSet wordsSet = new HashSet();
///
/// 存了某一个词在所有敏感词中的位置,(超出8个的截断为第8个位置)
///
static byte[] fastCheck = new byte[char.MaxValue];
///
/// 存了所有敏感词的长度信息,“Key”值为所有敏感词的第一个词,敏感词的长度会截断为8
///
static byte[] fastLength = new byte[char.MaxValue];
///
/// 保有所有敏感词汇的第一个词的记录,可用来判断是否一个词是一个或者多个敏感词汇的“第一个词”,
/// 且可判断以某一个词作为第一个词的一系列的敏感词的最大的长度
///
static byte[] startCache = new byte[char.MaxValue];
static char[] dectectedBuffer = null;
static string SkipList = " \t\r\n~!@#$%^&*()_+-=【】、{}|;':\",。、《》?αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩①②③④⑤⑥⑦⑧⑨⑩⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇≈≡≠=≤≥<>≮≯∷±+-×÷/∫∮∝∞∧∨∑∏∪∩∈∵∴⊥∥∠⌒⊙≌∽√§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\︿_ ̄―♂♀┌┍┎┐┑┒┓─┄┈├┝┞┟┠┡┢┣│┆┊┬┭┮┯┰┱┲┳┼┽┾┿╀╁╂╃└┕┖┗┘┙┚┛━┅┉┤┥┦┧┨┩┪┫┃┇┋┴┵┶┷┸┹┺┻╋╊╉╈╇╆╅╄";
static BitArray SkipBitArray = new BitArray(char.MaxValue);
///
/// 保有所有敏感词汇的最后一个词的记录,仅用来判断是否一个词是一个或者多个敏感词汇的“最后一个词”
///
static BitArray endCache = new BitArray(char.MaxValue);
#endregion
#region ' 内部 函数 '
unsafe public static void Init(string[] badwords)
{
if (badwords == null || badwords.Length == 0)
return;
int wordLength = 0;
int maxWordLength = int.MinValue;
for (int stringIndex = 0, len = badwords.Length; stringIndex < len; ++stringIndex)
{
if (string.IsNullOrEmpty(badwords[stringIndex]))
continue;
string strBadWord = OriginalToLower(badwords[stringIndex]);
//求得单个的敏感词汇的长度
wordLength = strBadWord.Length;
maxWordLength = Math.Max(wordLength, maxWordLength);
fixed (char* pWordStart = strBadWord)
{
for (int i = 0; i < wordLength; ++i)
{
//准确记录8位以内的敏感词汇的某个词在词汇中的“位置”
if (i < 7)
fastCheck[*(pWordStart + i)] |= (byte)(1 << i);
else//8位以外的敏感词汇的词直接限定在第8位
fastCheck[*(pWordStart + i)] |= 0x80;//0x80在内存中即为1000 0000,因为一个byte顶多标示8位,故超出8位的都位或上0x80,截断成第8位
}
//缓存敏感词汇的长度
int cachedWordslength = Math.Min(8, wordLength);
char firstWord = *pWordStart;
//记录敏感词汇的“大致长度(超出8个字的敏感词汇会被截取成8的长度)”,“key”值为敏感词汇的第一个词
fastLength[firstWord] |= (byte)(1 << (cachedWordslength - 1));
//缓存出当前以badWord第一个字开头的一系列的敏感词汇的最长的长度
if (startCache[firstWord] < cachedWordslength)
startCache[firstWord] = (byte)(cachedWordslength);
//存好敏感词汇的最后一个词汇的“出现情况”
endCache[*(pWordStart + wordLength - 1)] = true;
//将长度大于1的敏感词汇都压入到字典中
if (!wordsSet.Contains(strBadWord))
wordsSet.Add(strBadWord);
}
}
// 初始化好一个用来存检测到的字符串的buffer
dectectedBuffer = new char[maxWordLength];
// 记录应该跳过的不予检测的词
fixed (char* start = SkipList)
{
char* itor = start;
char* end = start + SkipList.Length;
while (itor < end)
SkipBitArray[*itor++] = true;
}
LogHelper.Log($"敏感词初始化完毕. 线程{Thread.CurrentThread.ManagedThreadId}");
}
unsafe static string OriginalToLower(string text)
{
fixed (char* newText = text)
{
char* itor = newText;
char* end = newText + text.Length;
char c;
while (itor < end)
{
c = *itor;
if ('A' <= c && c <= 'Z')
{
*itor = (char)(c | 0x20);
}
++itor;
}
}
return text;
}
unsafe static bool EnsuranceLower(string text)
{
fixed (char* newText = text)
{
char* itor = newText;
char* end = newText + text.Length;
char c;
while (itor < end)
{
c = *itor;
if ('A' <= c && c <= 'Z')
{
return true;
}
++itor;
}
}
return false;
}
#endregion
#region ' 方法 '
///
/// 过滤字符串,默认遇到敏感词汇就以'*'代替
///
///
///
///
unsafe public static string Filter(string text, string mask = "*")
{
Dictionary dic = DetectIllegalWords(text);
//如果没有敏感词汇,则直接返回出去
if (dic.Count == 0)
return text;
fixed (char* newText = text, cMask = mask)
{
var itor = newText;
Dictionary.Enumerator enumerator = dic.GetEnumerator();
//开始替换敏感词汇
while (enumerator.MoveNext())
{
//偏移到敏感词出现的位置
itor = newText + enumerator.Current.Key;
for (int index = 0; index < enumerator.Current.Value; index++)
{
//屏蔽掉敏感词汇
*itor++ = *cMask;
}
}
enumerator.Dispose();
}
return text;
}
///
/// 判断text是否有敏感词汇,如果有返回敏感的词汇的位置,利用指针操作来加快运算速度
///
///
///
unsafe public static Dictionary DetectIllegalWords(string text)
{
var findResult = new Dictionary();
if (string.IsNullOrEmpty(text))
return findResult;
if (EnsuranceLower(text))
text = text.ToLower();
fixed (char* ptext = text, detectedStrStart = dectectedBuffer)
{
//缓存字符串的初始位置
char* itor = (fastCheck[*ptext] & 0x01) == 0 ? ptext + 1 : ptext;
//缓存字符串的末尾位置
char* end = ptext + text.Length;
while (itor < end)
{
//如果text的第一个词不是敏感词汇或者当前遍历到了text第一个词的后面的词,则循环检测到text词汇的倒数第二个词,看看这一段子字符串中有没有敏感词汇
if ((fastCheck[*itor] & 0x01) == 0)
{
while (itor < end - 1 && (fastCheck[*(++itor)] & 0x01) == 0)
;
}
//如果有只有一个词的敏感词,且当前的字符串的“非第一个词”满足这个敏感词,则先加入已检测到的敏感词列表
if (startCache[*itor] != 0 && (fastLength[*itor] & 0x01) > 0)
{
//返回敏感词在text中的位置,以及敏感词的长度,供过滤功能用
findResult.Add((int)(itor - ptext), 1);
}
char* strItor = detectedStrStart;
*strItor++ = *itor;
int remainLength = (int)(end - itor - 1);
int skipCount = 0;
//此时已经检测到一个敏感词的“首词”了,记录下第一个检测到的敏感词的位置
//从当前的位置检测到字符串末尾
for (int i = 1; i <= remainLength; ++i)
{
char* subItor = itor + i;
// 跳过一些过滤的字符,比如空格特殊符号之类的
if (SkipBitArray[*subItor])
{
++skipCount;
continue;
}
//如果检测到当前的词在所有敏感词中的位置信息中没有处在第i位的,则马上跳出遍历
if ((fastCheck[*subItor] >> Math.Min(i - skipCount, 7)) == 0)
{
break;
}
*strItor++ = *subItor;
//如果有检测到敏感词的最后一个词,并且此时的“检测到的敏感词汇”的长度也符合要求,则才进一步查看检测到的敏感词汇是否是真的敏感
if ((fastLength[*itor] >> Math.Min(i - 1 - skipCount, 7)) > 0 && endCache[*subItor])
{
//如果此子字符串在敏感词字典中存在,则记录。做此判断是避免敏感词中夹杂了其他敏感词的单词,而上面的算法无法剔除,故先用hash数组来剔除
//上述算法是用于减少大部分的比较消耗
if (wordsSet.Contains(new string(dectectedBuffer, 0, (int)(strItor - detectedStrStart))))
{
int curDectectedStartIndex = (int)(itor - ptext);
findResult[curDectectedStartIndex] = i + 1;
itor = subItor;
break;
}
}
else if (i - skipCount > startCache[*itor] && startCache[*itor] < 0x80)
{//如果超过了以该词为首的一系列的敏感词汇的最大的长度,则不继续判断(前提是该词对应的所有敏感词汇没有超过8个词的)
break;
}
}
++itor;
}
}
return findResult;
}
#endregion
}
}