gin和gorm进阶功能(9) | 青训营笔记

144 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

本文将介绍敏感词过滤

敏感词过滤

正则表达式的效率比较低,在大规模的敏感词过滤场景中可能存在性能问题。使用Trie树作为敏感词过滤的算法可以提高效率,同时在过滤时也比较容易实现。

前缀树

特点

  • 前缀树
    • 名称:Trie、字典树、查找树
    • 特点:查找效率高,消耗内存大
    • 应用:字符串检索、词频统计、字符串排序等
  • 敏感词过滤器
    • 定义前缀树
    • 根据敏感词,初始化前缀树
    • 编写过滤敏感词的方法

原理

敏感词过滤就是词库匹配,你定义一个词库,里面有很多敏感词,匹配到了就说明这个词是敏感词。 所以最简单的办法就是建立一个list,先把所有的敏感词读进这个list,然后再利用list的contains方法,就可以判断某一句话中是否有敏感词,如果有就弹个提示,告诉用户语句中有敏感词,禁止用户发送,但是如果须要把把敏感词屏蔽掉(比如用” * “号代替)这个时候contains方法就不行了,得自己写算法判断敏感词所在的位置并屏蔽掉,实现起来并不那么简单。

优缺点

优点: 插入和查询的效率很高,都为O(m),其中 m 是待插入/查询的字符串的长度。 缺点: 当 hash 函数很好时,Trie树的查找效率会低于哈希搜索。 空间消耗比较大

代码

正则表达式(不推荐)

//1.  读取敏感词列表:
sensitiveWords := []string{"敏感词1", "敏感词2", "敏感词3"}

//2.  构造正则表达式:
regexStr := "("
for i, word := range sensitiveWords {
    if i == 0 {
        regexStr += word
    } else {
        regexStr += "|" + word
    }
}
regexStr += ")"
sensitiveRegex, err := regexp.Compile(regexStr)
if err != nil {
    panic(err)
}

//3.  对文本进行过滤:
func filterText(text string) string {
    return sensitiveRegex.ReplaceAllString(text, "***")
}

前缀树(推荐)

在项目中,需要先调用buildTrieTree方法,将敏感词列表构造成Trie树,然后再使用filterText方法对文本进行过滤。

//1.  定义Trie树节点:
type TrieNode struct {
    children map[rune]*TrieNode
    end      bool
}

func NewTrieNode() *TrieNode {
    return &TrieNode{children: make(map[rune]*TrieNode)}
}

//2.  构造Trie树:
func buildTrieTree(words []string) *TrieNode {
    root := NewTrieNode()
    for _, word := range words {
        node := root
        for _, r := range word {
            if _, ok := node.children[r]; !ok {
                node.children[r] = NewTrieNode()
            }
            node = node.children[r]
        }
        node.end = true
    }
    return root
}

//3.  对文本进行过滤:
func filterText(root *TrieNode, text string) string {
    var result []rune
    for i, r := range text {
        node := root
        j := i
        for node != nil {
            node = node.children[r]
            if node != nil && node.end {
                for ; j < len(text); j++ {
                    result = append(result, '*')
                }
                break
            }
            if j < len(text) - 1 {
                result = append(result, r)
                j++
                r = []rune(text)[j]
            } else {
                result = append(result, r)
                break
            }
        }
    }
    return string(result)
}

工具类

最后将前缀树写成工具类代码如下

package sensitive_word_filter  
  
const replaceString = "***"  
  
// TrieNode 前缀树的结点  
type TrieNode struct {  
   children map[rune]*TrieNode  
   isEnd    bool  
}  
  
// Trie 前缀树  
type Trie struct {  
   root *TrieNode  
}  
  
// NewTrie 创建新的前缀树  
func NewTrie() *Trie {  
   return &Trie{  
      root: &TrieNode{  
         children: make(map[rune]*TrieNode),  
         isEnd:    false,  
      },  
   }  
}  
  
// Insert 插入一个敏感词  
func (t *Trie) Insert(word string) {  
   node := t.root  
   for _, char := range word {  
      if _, ok := node.children[char]; !ok {  
         node.children[char] = &TrieNode{  
            children: make(map[rune]*TrieNode),  
            isEnd:    false,  
         }  
      }  
      node = node.children[char]  
   }  
   node.isEnd = true  
}  
  
// Search 查找敏感词  
func (t *Trie) Search(word string) bool {  
   node := t.root  
   for _, char := range word {  
      if _, ok := node.children[char]; !ok {  
         return false  
      }  
      node = node.children[char]  
   }  
   return node.isEnd  
}  
  
// Filter 过滤敏感词  
func (t *Trie) Filter(text string) string {  
   // 加上一个字符,方便敏感词在最后的处理  
   text = text + "!"  
   // 设置当前节点为根节点  
   node := t.root  
   // 记录单词的开始位置  
   start := 0  
   // 用来存储过滤后的字符串  
   var result []rune  
   // 遍历字符串  
   for i, char := range text {  
      // 如果当前字符不在字典树中,说明当前子串不存在于字典树中。  
      if _, ok := node.children[char]; !ok {  
         // 如果当前单词的起始位置不等于结束位置,说明之前已经找到了一个单词,则需要将该单词替换成 replaceString         if start != i && node.isEnd {  
            result = append(result, []rune(replaceString)...)  
         }  
         // 直接加入过滤后的字符串  
         result = append(result, char)  
         // 重置当前节点为根节点,起始位置为下一个字符位置  
         node = t.root  
         start = i + 1  
      } else {  
         // 如果当前 Trie 节点存在该字符,说明存在包含该字符的单词  
         // 如果该 Trie 节点是一个单词的结尾,则将单词的起始位置设为当前字符位置  
         if node.isEnd {  
            result = append(result, []rune(replaceString)...)  
            start = i + 1  
         }  
         node = node.children[char]  
      }  
   }  
   res := string(result)  
   // 去掉刚才的字符  
   return res[:len(res)-1]  
}

单元测试

//测试敏感词过滤  
func TestSensitive(t *testing.T) {  
   // 创建前缀树  
   trie := sensitive_word_filter.NewTrie()  
  
   // 从文件中读取敏感词  
   file, _ := os.Open("../config/sensitive_words.txt")  
   defer file.Close()  
   scanner := bufio.NewScanner(file)  
   for scanner.Scan() {  
      trie.Insert(scanner.Text())  
   }  
  
   // 过滤敏感词  
   text := "这是一段评论,快手,里面有敏感词"  
   fmt.Println("原始评论:", text)  
   filteredText := trie.Filter(text)  
   fmt.Println("过滤后评论:", filteredText)  
}