H5 DFA 敏感词过滤

前言

最近在做游戏的聊天功能,需要在客户端接入敏感词过滤,较低成本的实现方法有字典匹配和正则表达式匹配,但效率上较低。大致 google 了一遍,发现 DFA 算法是实现敏感词过滤效率较高的选择,下面是具体实现过程。

DFA 算法须知

DFA 算法,即 Deterministic Finite Automaton ,中文翻译是有穷自动机。它是通过 event 和当前 state 得到下一个 state 的,即 event + curState = nextState 。此算法没有复杂的计算,基本上都是控制状态的转移。

设计思路

  • 首先,可以通过文本或者表格的形式配置敏感词库,在代码中通过读取配置文件得到一个敏感词列表 Array<string>

  • 其次,将敏感词列表转化为一个状态机,其实需要将每个文字都对应到一个状态上,但实际上起始字相同的词可以共用状态,如下:

    words = [
          '小' => [
              '日' => [
                  '本' => true,
              ],
          ],
          '日' => [
             '本' => [
                 '鬼' => [
                      '子' => true,
                  ],
                  '人' => true,
              ],
          ],
      ];

    这其实就是树结构,并且在枝叶表示词尾;

  • 将需要检测的字符串分割成字符,然后遍历与脏词引用树进行匹配,并替换敏感字为指定字符。

具体实现

本质上这里主要是构建好脏词树即完成了大部分的工作,检测过程其实很简单,这里需要创建一个树结构体:

// 敏感词树结构
class WordNode {
    // 是否是敏感词词尾字
    public isEnd = false;
    // 父节点
    public parentNode: WordNode;
    // 子节点
    private _children: Map<string, WordNode>;
    // 字
    public value: string;
​
    public constructor() {
        this._children = new Map<string, WordNode>();
    }
​
    public getChild(name: string): WordNode {
        return this._children.get(name);
    }
​
    public addChild(char: string): WordNode {
        let node = new WordNode();
        node.value = char;
        node.parentNode = this;
        this._children.set(char, node);
        return node;
    }
}

从配置表中读取读取敏感词列表,这里我们使用的是符合 protobuf 语法的配表方式,如下:

CS CS
required required
uint32 string
WordID DirtyWord
脏字ID 脏字列表
1 敏感词1
2 敏感词2
... ...
n 敏感词n

然后通过工具导出为二进制格式,在代码中解析读取数据,得到一个 Array<string>

public static getDirtyArray() {
    let wordArray = new Array<string>();
    for (let dirty of this.dirtyArray.items) {
        wordArray.push(dirty.DirtyWord);
    }
    return wordArray;
}

将此列表用于初始化敏感词索引树,这里我将其封装成一个单例的工具类来使用:

// 敏感词过滤器(基于 DFA 算法)
class DirtyWordFilter {
    // 单例
    private static _instance: DirtyWordFilter;
    public static get instance() {
        if (!this._instance) {
            this._instance = new DirtyWordFilter();
        }
        if (!this._instance._inited) {
            this._instance.initTreeConfs();
        }
        return this._instance;
    }
​
    // 是否已初始化
    private _inited: boolean;
    // 脏词库
    private dirtyWordArray: Array<string>;
    // 检测源字符串
    private sourceWord: string;
    // 代替敏感字的字符
    private repChar = '*';
    // 脏词库构造的 DFA 敏感词索引树结构
    private treeRoot: WordNode;
​
    public constructor() {
        this.dirtyWordArray = null;
    }
​
    public clean() {
        this.dirtyWordArray = null;
        this.treeRoot = null;
        this._inited = false;
    }
​
    // 从脏词表中读出脏词库
    private initTreeConfs() {
        this.dirtyWordArray = DirtyConfig.getDirtyArray();
        if (this.dirtyWordArray.length > 0) {
            // 最外层树根
            this.treeRoot = new WordNode();
            this.treeRoot.value = '';
            let word: string;
            let charCount = 0;
            let char: string;
            let node: WordNode;
            for (let i = 0; i < this.dirtyWordArray.length; i++) {
                word = this.dirtyWordArray[i];
                charCount = word.length;
                if (charCount > 0) {
                    node = this.treeRoot;
                    for (let j = 0; j < charCount; j++) {
                        char = word.slice(j, j + 1);
                        let tmpNode = node.getChild(char);
                        if (tmpNode) {
                            node = tmpNode;
                        } else {
                            // 树根
                            node = node.addChild(char);
                        }
                    }
                    // 词尾标识
                    node.isEnd = true;
                }
            }
        }
        this.dirtyWordArray = null;
        this._inited = true;
    }
​
    /**
     * 检测一个词并返回是否带敏感词和替换敏感词之后的结果
     * @param word 检测的词
     * @param repChar 代替敏感字的字符
     */
    public filterWord(word: string, repChar?: string): { hasDirty: boolean, filteredWord: string } {
        let has_disty = false;
        this.sourceWord = word;
        let filtered_word = word;
        let charCount = filtered_word.length;
        // 确保敏感词索引树有内容
        if (charCount > 0 && this.treeRoot) {
            let char: string;
            // 敏感字替换字符
            let _repChar = repChar ? repChar : this.repChar;
            let node = this.treeRoot;
            let childNode: WordNode;
            let dirtyWord: string;
            for (let i = 0; i < charCount; i++) {
                char = this.sourceWord.slice(i, i + 1);
                childNode = node.getChild(char);
                if (childNode) {
                    dirtyWord += childNode.value;
                    // 检测到词尾,表示匹配到一个敏感词
                    if (childNode.isEnd) {
                        has_disty = true;
                        if (dirtyWord.length > 0) {
                            // 替换敏感字
                            filtered_word = filtered_word.replace(dirtyWord, this.getReplaceStr(_repChar, dirtyWord.length));
                        }
                    }
                    node = childNode;
                } else {
                    dirtyWord = '';
                    // 重新开始下个敏感词检测
                    node = this.treeRoot;
                }
            }
        }
        return { hasDirty: has_disty, filteredWord: filtered_word };
    }
​
    // 替换字符串
    public getReplaceStr(repChar: string, length: number) {
        let result = '';
        for (let i = 0; i < length; i++) {
            result += repChar;
        }
        return result;
    }
}

调用接口来检测字符串如下:

let { hasDirty, filteredWord } = DirtyWordFilter.instance.filterWord("检测的字符串");

参考

猜你喜欢

转载自blog.csdn.net/linshuhe1/article/details/81288063