这是我参与「第五届青训营 」伴学笔记创作活动的第 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)
}