JS 敏感词识别-模式匹配算法 DFA
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 模式匹配
但截止日前笔者还不敢直接在线上库当中直接使用分词库,市面上大多数的敏感词过滤也不会上分词库的,或许是担心性能问题,也存在宁杀错无放过的心态吧。如果有人有更好的办法,请不吝赐教啊。