連接已更新到第二版:html
敏感詞查找或者過濾是每一個天朝互聯網從業者都不能忽略的一件事情。node
寫以前已經參閱了博客園的大量敏感詞的查找或者過濾算法,發現沒用徹底符合本身需求的算法,因此本身花時間作了一個算法
需求主要有三點:post
一、高性能和可靠性,由於基於百萬級PV全站的敏感詞實時過濾,這個無疑是很致命的,能夠接受的程度是每一個頁面100k字節 關鍵詞庫1000個左右 必須在毫秒級別完成替換。CPU和內存無明顯增長。性能
二、最好能識別必定的干擾 如 「法-輪*功」 「F Uc K 」。url
三、能夠支持自定義高亮算法,方便用戶和編輯快速找到關鍵詞spa
算法要點code
一、構建一顆關鍵詞樹htm
/// <summary> /// 敏感詞樹 /// </summary> private class FilterKeyWordsNode { public Dictionary<char, FilterKeyWordsNode> Child; public bool IsEnd; }
二、關鍵詞和目標字符串作一樣的映射 把大小寫 全半角 簡繁體 甚至是異體字火星文統一映射到同一字符blog
/// <summary> /// 字符預處理 /// </summary> private static char[] Translation(String src) { char[] c = src.ToCharArray(); for (int i = 0; i < c.Length; i++) { /*全角=>半角*/ if (c[i] > 0xFF00 && c[i] < 0xFF5F) c[i] = (char)(c[i] - 0xFEE0); /*大寫=>小寫*/ if (c[i] > 0x40 && c[i] < 0x5b) c[i] = (char)(c[i] + 0x20); /*繁體=>簡體*/ if (c[i] > 0x4E00 && c[i] < 0x9FFF) { char chinese; if (TranslationChinese.TryGetValue(c[i], out chinese)) c[i] = chinese; } } return c; }
三、快速跳過特殊符號
/// <summary> /// 跳過特殊符號 ASCII範圍 排除 數字字母 /// </summary> private static bool IsSkip(char firstChar) { if (firstChar < '0') return true; if (firstChar > '9' && firstChar < 'A') return true; if (firstChar > 'Z' && firstChar < 'a') return true; if (firstChar > 'z' && firstChar < 128) return true; return false; }
四、查找的時候返回關鍵詞的偏移位置和長度便於替換
/// <summary> /// 位置查找 /// </summary> private static Dictionary<int, int> Find(string src) { if (_root == null) throw new InvalidOperationException("未初始化"); var findResult = new Dictionary<int, int>(); if (string.IsNullOrEmpty(src)) return findResult; var text = Translation(src); int start = 0; while (start < text.Length) { int length = 0; var firstChar = text[start + length]; var node = _root; //跳過特殊符號 while (IsSkip(firstChar) && (start + length + 1) < text.Length) { start++; firstChar = text[start + length]; } //不匹配首字符 移動起始位置 while (!node.Child.ContainsKey(firstChar) && start < text.Length - 1) { start++; firstChar = text[start + length]; } //部分匹配 移動結束位置 直到不匹配 while (node.Child != null && node.Child.ContainsKey(firstChar)) { node = node.Child[firstChar]; length++; if((start + length) == text.Length) break; firstChar = text[start + length]; //跳過忽略詞 while (IsSkip(firstChar) && !node.IsEnd && (start + length + 1) < text.Length) { length++; firstChar = text[start + length]; } } //完整匹配 把起始位置移到結束位置 if (node.IsEnd) { findResult.Add(start, length); start += length - 1; } start++; } return findResult; }
普通PC性能統計1000個關鍵詞庫
最差狀況 全是關鍵字 每秒10MB/s
正常字符 含少許關鍵詞 每秒30MB/s
最好狀況 全是忽略詞 每秒50MB/s
性能和文本長度呈線性增加關係 關鍵詞數量對性能影響不明顯。
源代碼
/// <summary> /// 敏感詞過濾 已忽略大小寫 全半角 簡繁體差別 排除特殊符號干擾 by http://passer.cnblogs.com /// </summary> public static class FilterKeyWords { private static readonly object LockObj = new object(); private static FilterKeyWordsNode _root; private const string TraditionalChinese = "皚藹礙愛翺襖奧壩罷擺敗頒辦絆幫綁鎊謗剝飽寶報鮑輩貝鋇狽備憊繃筆畢斃閉邊編貶變辯辮鼈癟瀕濱賓擯餅撥缽鉑駁蔔補參蠶殘慚慘燦蒼艙倉滄廁側冊測層詫攙摻蟬饞讒纏鏟産闡顫場嘗長償腸廠暢鈔車徹塵陳襯撐稱懲誠騁癡遲馳恥齒熾沖蟲寵疇躊籌綢醜櫥廚鋤雛礎儲觸處傳瘡闖創錘純綽辭詞賜聰蔥囪從叢湊竄錯達帶貸擔單鄲撣膽憚誕彈當擋黨蕩檔搗島禱導盜燈鄧敵滌遞締點墊電澱釣調疊諜疊釘頂錠訂東動棟凍鬥犢獨讀賭鍍鍛斷緞兌隊對噸頓鈍奪鵝額訛惡餓兒爾餌貳發罰閥琺礬釩煩範販飯訪紡飛廢費紛墳奮憤糞豐楓鋒風瘋馮縫諷鳳膚輻撫輔賦複負訃婦縛該鈣蓋幹趕稈贛岡剛鋼綱崗臯鎬擱鴿閣鉻個給龔宮鞏貢鈎溝構購夠蠱顧剮關觀館慣貫廣規矽歸龜閨軌詭櫃貴劊輥滾鍋國過駭韓漢閡鶴賀橫轟鴻紅後壺護滬戶嘩華畫劃話懷壞歡環還緩換喚瘓煥渙黃謊揮輝毀賄穢會燴彙諱誨繪葷渾夥獲貨禍擊機積饑譏雞績緝極輯級擠幾薊劑濟計記際繼紀夾莢頰賈鉀價駕殲監堅箋間艱緘繭檢堿鹼揀撿簡儉減薦檻鑒踐賤見鍵艦劍餞漸濺澗漿蔣槳獎講醬膠澆驕嬌攪鉸矯僥腳餃繳絞轎較稭階節莖驚經頸靜鏡徑痙競淨糾廄舊駒舉據鋸懼劇鵑絹傑潔結誡屆緊錦僅謹進晉燼盡勁荊覺決訣絕鈞軍駿開凱顆殼課墾懇摳庫褲誇塊儈寬礦曠況虧巋窺饋潰擴闊蠟臘萊來賴藍欄攔籃闌蘭瀾讕攬覽懶纜爛濫撈勞澇樂鐳壘類淚籬離裏鯉禮麗厲勵礫曆瀝隸倆聯蓮連鐮憐漣簾斂臉鏈戀煉練糧涼兩輛諒療遼鐐獵臨鄰鱗凜賃齡鈴淩靈嶺領餾劉龍聾嚨籠壟攏隴樓婁摟簍蘆盧顱廬爐擄鹵虜魯賂祿錄陸驢呂鋁侶屢縷慮濾綠巒攣孿灤亂掄輪倫侖淪綸論蘿羅邏鑼籮騾駱絡媽瑪碼螞馬罵嗎買麥賣邁脈瞞饅蠻滿謾貓錨鉚貿麼黴沒鎂門悶們錳夢謎彌覓綿緬廟滅憫閩鳴銘謬謀畝鈉納難撓腦惱鬧餒膩攆撚釀鳥聶齧鑷鎳檸獰甯擰濘鈕紐膿濃農瘧諾歐鷗毆嘔漚盤龐賠噴鵬騙飄頻貧蘋憑評潑頗撲鋪樸譜臍齊騎豈啓氣棄訖牽扡釺鉛遷簽謙錢鉗潛淺譴塹槍嗆牆薔強搶鍬橋喬僑翹竅竊欽親輕氫傾頃請慶瓊窮趨區軀驅齲顴權勸卻鵲讓饒擾繞熱韌認紉榮絨軟銳閏潤灑薩鰓賽傘喪騷掃澀殺紗篩曬閃陝贍繕傷賞燒紹賒攝懾設紳審嬸腎滲聲繩勝聖師獅濕詩屍時蝕實識駛勢釋飾視試壽獸樞輸書贖屬術樹豎數帥雙誰稅順說碩爍絲飼聳慫頌訟誦擻蘇訴肅雖綏歲孫損筍縮瑣鎖獺撻擡攤貪癱灘壇譚談歎湯燙濤縧騰謄銻題體屜條貼鐵廳聽烴銅統頭圖塗團頹蛻脫鴕馱駝橢窪襪彎灣頑萬網韋違圍爲濰維葦偉僞緯謂衛溫聞紋穩問甕撾蝸渦窩嗚鎢烏誣無蕪吳塢霧務誤錫犧襲習銑戲細蝦轄峽俠狹廈鍁鮮纖鹹賢銜閑顯險現獻縣餡羨憲線廂鑲鄉詳響項蕭銷曉嘯蠍協挾攜脅諧寫瀉謝鋅釁興洶鏽繡虛噓須許緒續軒懸選癬絢學勳詢尋馴訓訊遜壓鴉鴨啞亞訝閹煙鹽嚴顔閻豔厭硯彥諺驗鴦楊揚瘍陽癢養樣瑤搖堯遙窯謠藥爺頁業葉醫銥頤遺儀彜蟻藝億憶義詣議誼譯異繹蔭陰銀飲櫻嬰鷹應纓瑩螢營熒蠅穎喲擁傭癰踴詠湧優憂郵鈾猶遊誘輿魚漁娛與嶼語籲禦獄譽預馭鴛淵轅園員圓緣遠願約躍鑰嶽粵悅閱雲鄖勻隕運蘊醞暈韻雜災載攢暫贊贓髒鑿棗竈責擇則澤賊贈紮劄軋鍘閘詐齋債氈盞斬輾嶄棧戰綻張漲帳賬脹趙蟄轍鍺這貞針偵診鎮陣掙睜猙幀鄭證織職執紙摯擲幟質鍾終種腫衆謅軸皺晝驟豬諸誅燭矚囑貯鑄築駐專磚轉賺樁莊裝妝壯狀錐贅墜綴諄濁茲資漬蹤綜總縱鄒詛組鑽緻鐘麼為隻兇準啟闆裡靂餘鍊洩"; private const string SimplifiedChinese = "皚藹礙愛翱襖奧壩罷擺敗頒辦絆幫綁鎊謗剝飽寶報鮑輩貝鋇狽備憊繃筆畢斃閉邊編貶變辯辮鱉癟瀕濱賓擯餅撥鉢鉑駁卜補參蠶殘慚慘燦蒼艙倉滄廁側冊測層詫攙摻蟬饞讒纏鏟產闡顫場嘗長償腸廠暢鈔車徹塵陳襯撐稱懲誠騁癡遲馳恥齒熾衝蟲寵疇躊籌綢醜櫥廚鋤雛礎儲觸處傳瘡闖創錘純綽辭詞賜聰蔥囪從叢湊竄錯達帶貸擔單鄲撣膽憚誕彈當擋黨蕩檔搗島禱導盜燈鄧敵滌遞締點墊電澱釣調迭諜疊釘頂錠訂東動棟凍鬥犢獨讀賭鍍鍛斷緞兌隊對噸頓鈍奪鵝額訛惡餓兒爾餌貳發罰閥琺礬釩煩範販飯訪紡飛廢費紛墳奮憤糞豐楓鋒風瘋馮縫諷鳳膚輻撫輔賦復負訃婦縛該鈣蓋幹趕稈贛岡剛鋼綱崗皋鎬擱鴿閣鉻個給龔宮鞏貢鉤溝構購夠蠱顧剮關觀館慣貫廣規硅歸龜閨軌詭櫃貴劊輥滾鍋國過駭韓漢閡鶴賀橫轟鴻紅後壺護滬戶譁華畫劃話懷壞歡環還緩換喚瘓煥渙黃謊揮輝毀賄穢會燴匯諱誨繪葷渾夥獲貨禍擊機積飢譏雞績緝極輯級擠幾薊劑濟計記際繼紀夾莢頰賈鉀價駕殲監堅箋間艱緘繭檢鹼礆揀撿簡儉減薦檻鑑踐賤見鍵艦劍餞漸濺澗漿蔣槳獎講醬膠澆驕嬌攪鉸矯僥腳餃繳絞轎較秸階節莖驚經頸靜鏡徑痙競淨糾廄舊駒舉據鋸懼劇鵑絹傑潔結誡屆緊錦僅謹進晉燼盡勁荊覺決訣絕鈞軍駿開凱顆殼課墾懇摳庫褲誇塊儈寬礦曠況虧巋窺饋潰擴闊蠟臘萊來賴藍欄攔籃闌蘭瀾讕攬覽懶纜爛濫撈勞澇樂鐳壘類淚籬離裏鯉禮麗厲勵礫歷瀝隸倆聯蓮連鐮憐漣簾斂臉鏈戀煉練糧涼兩輛諒療遼鐐獵臨鄰鱗凜賃齡鈴凌靈嶺領餾劉龍聾嚨籠壟攏隴樓婁摟簍蘆盧顱廬爐擄滷虜魯賂祿錄陸驢呂鋁侶屢縷慮濾綠巒攣孿灤亂掄輪倫侖淪綸論蘿羅邏鑼籮騾駱絡媽瑪碼螞馬罵嗎買麥賣邁脈瞞饅蠻滿謾貓錨鉚貿麼黴沒鎂門悶們錳夢謎彌覓綿緬廟滅憫閩鳴銘謬謀畝鈉納難撓腦惱鬧餒膩攆捻釀鳥聶齧鑷鎳檸獰寧擰濘鈕紐膿濃農瘧諾歐鷗毆嘔漚盤龐賠噴鵬騙飄頻貧蘋憑評潑頗撲鋪樸譜臍齊騎豈啓氣棄訖牽扦釺鉛遷籤謙錢鉗潛淺譴塹槍嗆牆薔強搶鍬橋喬僑翹竅竊欽親輕氫傾頃請慶瓊窮趨區軀驅齲顴權勸卻鵲讓饒擾繞熱韌認紉榮絨軟銳閏潤灑薩鰓賽傘喪騷掃澀殺紗篩曬閃陝贍繕傷賞燒紹賒攝懾設紳審嬸腎滲聲繩勝聖師獅溼詩屍時蝕實識駛勢釋飾視試壽獸樞輸書贖屬術樹豎數帥雙誰稅順說碩爍絲飼聳慫頌訟誦擻蘇訴肅雖綏歲孫損筍縮瑣鎖獺撻擡攤貪癱灘壇譚談嘆湯燙濤絛騰謄銻題體屜條貼鐵廳聽烴銅統頭圖塗團頹蛻脫鴕馱駝橢窪襪彎灣頑萬網韋違圍爲濰維葦偉僞緯謂衛溫聞紋穩問甕撾蝸渦窩嗚鎢烏誣無蕪吳塢霧務誤錫犧襲習銑戲細蝦轄峽俠狹廈杴鮮纖鹹賢銜閒顯險現獻縣餡羨憲線廂鑲鄉詳響項蕭銷曉嘯蠍協挾攜脅諧寫瀉謝鋅釁興洶鏽繡虛噓須許緒續軒懸選癬絢學勳詢尋馴訓訊遜壓鴉鴨啞亞訝閹煙鹽嚴顏閻豔厭硯彥諺驗鴦楊揚瘍陽癢養樣瑤搖堯遙窯謠藥爺頁業葉醫銥頤遺儀彝蟻藝億憶義詣議誼譯異繹蔭陰銀飲櫻嬰鷹應纓瑩螢營熒蠅穎喲擁傭癰踊詠涌優憂郵鈾猶遊誘輿魚漁娛與嶼語籲御獄譽預馭鴛淵轅園員圓緣遠願約躍鑰嶽粵悅閱雲鄖勻隕運蘊醞暈韻雜災載攢暫贊贓髒鑿棗竈責擇則澤賊贈扎札軋鍘閘詐齋債氈盞斬輾嶄棧戰綻張漲賬帳脹趙蟄轍鍺這貞針偵診鎮陣掙睜猙幀鄭證織職執紙摯擲幟質鍾終種腫衆謅軸皺晝驟豬諸誅燭矚囑貯鑄築駐專磚轉賺樁莊裝妝壯狀錐贅墜綴諄濁茲資漬蹤綜總縱鄒詛組鑽致鍾麼爲只兇準啓板裏靂餘鏈泄"; private static readonly Dictionary<char, char> TranslationChinese = TraditionalChinese.Select((c, i) => new { c, i }).ToDictionary(p => p.c, p => SimplifiedChinese[p.i]); /// <summary> /// 初始化 使用前必須調用一次 /// </summary> /// <param name="keyWords">敏感詞列表</param> public static void Init(string[] keyWords) { if (_root != null) return; lock (LockObj) { _root = new FilterKeyWordsNode(); var list = keyWords.Select(p => new string(Translation(p))).Distinct().OrderBy(p => p).ThenBy(p => p.Length).ToList(); for (int i = list.Min(p => p.Length); i <= list.Max(p => p.Length); i++) { int i1 = i; var startList = list.Where(p => p.Length >= i1).Select(p => p.Substring(0, i1)).Distinct(); foreach (var startWord in startList) { var tmp = _root; for (int j = 0; j < startWord.Length; j++) { var t = startWord[j]; if (tmp.Child == null) tmp.Child = new Dictionary<char, FilterKeyWordsNode>(); if (!tmp.Child.ContainsKey(t)) { tmp.Child.Add(t, new FilterKeyWordsNode { IsEnd = list.Contains(startWord.Substring(0, 1 + j)) }); } tmp = tmp.Child[t]; } } } } } /// <summary> /// 查找含有的關鍵詞 /// </summary> public static bool Find(string text, out string[] keyWords) { keyWords = Find(text).Select(p => text.Substring(p.Key, p.Value)).Distinct().ToArray(); return keyWords.Length > 0; } /// <summary> /// 簡單快速替換 /// </summary> public static string Replace(string text) { var dic = Find(text); var list = text.ToArray(); foreach (var i in dic) { for (int j = i.Key; j < i.Key + i.Value; j++) { list[j] = '*'; } } return new string(list.ToArray()); } /// <summary> /// 自定義過濾 /// </summary> public static string Replace(string text, ReplaceDelegate replaceAction) { var dic = Find(text); var list = text.ToList(); var offset = 0; foreach (var i in dic) { list.RemoveRange(i.Key + offset, i.Value); var newText = replaceAction(text.Substring(i.Key, i.Value), i.Key, i.Value); list.InsertRange(i.Key + offset, newText); offset = offset + newText.Length - i.Value; } return new string(list.ToArray()); } /// <summary> /// 位置查找 /// </summary> private static Dictionary<int, int> Find(string src) { if (_root == null) throw new InvalidOperationException("未初始化"); var findResult = new Dictionary<int, int>(); if (string.IsNullOrEmpty(src)) return findResult; var text = Translation(src); int start = 0; while (start < text.Length) { int length = 0; var firstChar = text[start + length]; var node = _root; //跳過特殊符號 while (IsSkip(firstChar) && (start + length + 1) < text.Length) { start++; firstChar = text[start + length]; } //不匹配首字符 移動起始位置 while (!node.Child.ContainsKey(firstChar) && start < text.Length - 1) { start++; firstChar = text[start + length]; } //部分匹配 移動結束位置 直到不匹配 while (node.Child != null && node.Child.ContainsKey(firstChar)) { node = node.Child[firstChar]; length++; if((start + length) == text.Length) break; firstChar = text[start + length]; //跳過忽略詞 while (IsSkip(firstChar) && !node.IsEnd && (start + length + 1) < text.Length) { length++; firstChar = text[start + length]; } } //完整匹配 把起始位置移到結束位置 if (node.IsEnd) { findResult.Add(start, length); start += length - 1; } start++; } return findResult; } /// <summary> /// 字符預處理 /// </summary> private static char[] Translation(String src) { char[] c = src.ToCharArray(); for (int i = 0; i < c.Length; i++) { /*全角=>半角*/ if (c[i] > 0xFF00 && c[i] < 0xFF5F) c[i] = (char)(c[i] - 0xFEE0); /*大寫=>小寫*/ if (c[i] > 0x40 && c[i] < 0x5b) c[i] = (char)(c[i] + 0x20); /*繁體=>簡體*/ if (c[i] > 0x4E00 && c[i] < 0x9FFF) { char chinese; if (TranslationChinese.TryGetValue(c[i], out chinese)) c[i] = chinese; } } return c; } /// <summary> /// 跳過特殊符號 ASCII範圍 排除 數字字母 /// </summary> private static bool IsSkip(char firstChar) { if (firstChar < '0') return true; if (firstChar > '9' && firstChar < 'A') return true; if (firstChar > 'Z' && firstChar < 'a') return true; if (firstChar > 'z' && firstChar < 128) return true; return false; } /// <summary> /// 敏感詞樹 /// </summary> private class FilterKeyWordsNode { public Dictionary<char, FilterKeyWordsNode> Child; public bool IsEnd; } /// <summary> /// 自定義過濾方法 /// </summary> /// <param name="text">找到的字符串</param> /// <param name="offset">起始位置</param> /// <param name="length">字符串長度</param> /// <returns>替換後的</returns> public delegate string ReplaceDelegate(string text, int offset, int length); }