DFA(Deterministic Finite Automaton,即確定有窮自動機。其原理為:有一個有限狀態集合和一些從一個狀態通向另一個狀態的邊,每條邊上標記有一個符號,其中一個狀態是初態,某些狀態是終態。但不同於不確定的有限自動機,DFA中不會有從同一狀態出發的兩條邊標志有相同的符號。
舉例有限狀態機字典如下
@startuml
(王) -> (八)
(八) -> (蛋)
(八) -> (羔)
(羔) -> (子)
@enduml
{
'王': {
'八': {
'蛋': {
empty: true
}
}
'羔': {
'子': {
empty: true
}
}
}
}
輸入為王八端時,逐字對比有限狀態機的字典
- 王字命中
- 八字命中
- 端沒有命中,則返回八字繼續開始匹配
- 八字沒有命中有限狀態機,over
輸入為王八蛋時
- 王字命中
- 八字命中
- 蛋命中
- 下一個 empty 為 true 命中,匹配成功。
實現
找出敏感詞代碼
'use strict';
// 特殊符號過濾邏輯
const ignoreChars = " \t\r\n~!@#$%^&*()_+-=【】、{}|;':\",。、《》?αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩①②③④⑤⑥⑦⑧⑨⑩⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇≈≡≠=≤≥<>≮≯∷±+-×÷/∫∮∝∞∧∨∑∏∪∩∈∵∴⊥∥∠⌒⊙≌∽√§№☆★○●◎◇◆□℃‰€■△▲※→←↑↓〓¤°#&@\︿_ ̄―♂♀┌┍┎┐┑┒┓─┄┈├┝┞┟┠┡┢┣│┆┊┬┭┮┯┰┱┲┳┼┽┾┿╀╁╂╃└┕┖┗┘┙┚┛━┅┉┤┥┦┧┨┩┪┫┃┇┋┴┵┶┷┸┹┺┻╋╊╉╈╇╆╅╄";
const ignoreObj = {};
for (let i = 0, j = ignoreChars.length; i < j; i++) {
ignoreObj[ignoreChars.charCodeAt(i)] = true;
}
// 構建有限狀態機
// 生成的數據結構形制如下
/**
* {
* '王': {
* '八': {
* '蛋': {
* empty: true
* }
* '羔': {
* '子': {
* empty: true
* }
* }
* }
* }
* }
*/
function buildMap(wordList) {
const result = {};
for (let i = 0, len = wordList.length; i < len; ++i) {
let map = result;
const word = wordList[i];
for (let j = 0; j < word.length; ++j) {
const ch = word.charAt(j).toLowerCase();
if (map[ch]) {
map = map[ch];
if (map.empty) {
break;
}
} else {
if (map.empty) {
delete map.empty;
}
map[ch] = {
empty: true,
};
map = map[ch];
}
}
}
return result;
}
// 獲取敏感詞列表
function getSensitiveWords() {
/*
let words = [];
if (words.length === 0) {
const data = fs.readFileSync(path.join(__dirname, './words'), 'utf8');
words = data.split('\n');
}
return words.filter(item => !!item);
*/
return [
'王八蛋',
'王八羔子'
]
}
// 獲取敏感詞庫對象
const sensitiveWords = getSensitiveWords();
const map = buildMap(sensitiveWords);
// 具體檢測代碼。
function check(map, content) {
const result = [];
let stack = [];
let point = map;
for (let i = 0, len = content.length; i < len; ++i) {
const code = content.charCodeAt(i);
if (ignoreObj[code]) {
continue;
}
const ch = content.charAt(i);
const item = point[ch.toLowerCase()];
if (!item) {
i = i - stack.length;
stack = [];
point = map;
} else if (item.empty) {
stack.push(ch);
result.push(stack.join(''));
stack = [];
point = map;
} else {
stack.push(ch);
point = item;
}
}
return result;
}
function checkSensitive(content) {
const words = check(map, content);
return words;
}
module.exports = checkSensitive;
測試
checkSensitive('老板黃鶴*王&八&(&蛋,吃喝嫖賭,欠下了3.5個億,帶着他的小姨子跑了')
// ['王八蛋']
缺陷
若用火星文、異體字、同音字來替代,這類算法沒什么好的辦法能識別
checkSensitive('王八羔仔')
// []
若敏感詞短,則誤傷很大,若前文設置的是 王八
作為敏感詞。
checkSensitive('虎軀一震,散發一陣王八之氣');
// ['王八']
自然還有大名鼎鼎的 江陰毛紡織廠
怕也會很容易被誤傷。所以,有更智能的辦法是先做分詞,參照分詞庫, 比如說 Node 庫的 nodejieba,Python 的 jieba
可以將 江陰毛紡織廠
,拆解為 江陰
,毛紡織廠
的詞后,在進行 DFA 模式匹配
但截止日前筆者還不敢直接在線上庫當中直接使用分詞庫,市面上大多數的敏感詞過濾也不會上分詞庫的,或許是擔心性能問題,也存在寧殺錯無放過的心態吧。如果有人有更好的辦法,請不吝賜教啊。