这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
简介
我们小组选择抖音项目,本次笔记分享如何使用字典树实现敏感词过滤,本功能可用于对评论内容和视频标题进行敏感词过滤。
关键词:字典树
需求说明
给定敏感词,将指定文本中的敏感词替换为特殊符号。
敏感词:nt、赌博
替换词:**
输入:只有nt才赌博。
输出:只有**才**。
字典树
别名:前缀树、单词查找树、Tire
利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。
208. 实现 Trie (前缀树) - 力扣(LeetCode)
编码
1. 定义字典树
type trieNode struct {
isKeywordEnd bool // 关键词结束标识
subNodes map[rune]*trieNode // 子节点(key是下级字符,value是下级节点)
}
//trieNode的构造函数
func newTrieNode() *trieNode {
subNode := new(trieNode)
subNode.isKeywordEnd = false
subNode.subNodes = make(map[rune]*trieNode)
return subNode
}
为什么map的数据类型为rune?
go中字符类型有两种:byte(uint8、ASCII码字符、1个字节)和rune(uint32、Unicode字符、4个字节),而我们不止要对英文进行敏感词过滤,还要对中文进行敏感词过滤。
注意这个rune后面还要考。
2. 初始化字典树
初始化要干的事就是:不断拿敏感词,每拿到一个敏感词就把它放到字典树中
敏感词的存储
我们需要事先确定需要过滤的敏感词有哪些,通常这些词的数量比较大,我们可以使用数据库或其他方式把这些词存储在硬盘上,初始化的时候再从硬盘读取。
这里我使用一个名为sensitive-words.txt的txt文件存储这些敏感词,一行存储一个敏感词。
sb
nt
滚
赌博
从文件中读取敏感词
var rootNode *trieNode //根节点
func Init() error {
rootNode = newTrieNode()
//从文件中读取敏感词
filepath := "./sensitive-words.txt"
file, err := os.OpenFile(filepath, os.O_RDWR, 0666)
if err != nil {
return err
}
defer file.Close()
buf := bufio.NewReader(file)
for {
line, err := buf.ReadString('\n')
line = strings.TrimSpace(line)
//把敏感词添加到前缀树中
addKeyWord(line)
if err == io.EOF {
break
} else if err != nil {
return err
}
}
return nil
}
别忘了使用过滤器前先初始化!!!
把敏感词添加到前缀树中
//将一个敏感词添加到前缀树中
func addKeyWord(originalKeyword string) {
dummyNode := rootNode
keyword := []rune(originalKeyword)
for i := 0; i < len(keyword); i++ {
c := keyword[i]
subNode := dummyNode.subNodes[c]
if subNode == nil {
//初始化子节点
subNode = newTrieNode()
dummyNode.subNodes[c] = subNode
}
//指向子节点,进入下一轮循环
dummyNode = subNode
//设置敏感词结束标识
if i == len(keyword)-1 {
dummyNode.isKeywordEnd = true
}
}
}
注意:因为originalKeyword中包中文,所以在遍历originalKeyword中的字符时,要先将string类型的originalKeyword转为[]rune。
go 字符串遍历方式_光九的博客-CSDN博客_go 遍历字符串
3. 过滤器
const REPLACEMENT = "**" //敏感词的替换词
Filter函数的输入是待过滤的字符串,输出的过滤完成的字符串。
使用三个指针:dummyNode指向前缀树、begin和position指向需要过滤的文本。
如果begin是敏感词的开头,position是敏感词的结尾,就说明我们发现了一个敏感词,用REPLACEMENT将begin~position字符串替换掉
func Filter(originalText string) string {
if originalText == "" {
return ""
}
dummyNode := rootNode //前缀树的指针
begin := 0 //text的指针
position := 0 //text的指针
var res bytes.Buffer //存放过滤结果
text := []rune(originalText)
for position < len(text) {
c := text[position]
//检查下级节点
dummyNode = dummyNode.subNodes[c]
if dummyNode == nil {
// 以begin开头,position结尾的字符串不是敏感词
res.WriteString(string(text[begin]))
// 进入下一个位置
begin++
position = begin
// 重新指向trie根节点
dummyNode = rootNode
} else if dummyNode.isKeywordEnd {
// 发现敏感词,将begin~position字符串替换掉
res.WriteString(REPLACEMENT)
// 进入下一个位置
position++
begin = position
// 重新指向Trie根节点
dummyNode = rootNode
} else {
// 检查下一个字符
position++
}
}
res.WriteString(string(text[begin:]))
return res.String()
}
注意:遍历待过滤的字符串前,先将string变为[]rune类型。
go 拼接字符串的方法_小布丁吃西瓜的博客-CSDN博客_go 字符串拼接
最后
使用
我感觉用户上传信息时不应该使用过滤器,我们应该在数据库中保存用户上传的原始信息。
可以在数据返回给前端时对数据进行过滤,这样我们就拥有了原始数据,而用户使用时仍然有过滤的效果。
下面的代码位于controller层,是获取评论列表功能中的一部分
comment := Comment{
Id: int64(originalComment.ID),
User: user,
Content: tool.Filter(originalComment.Content), //使用过滤器过滤评论内容
CreateDate: originalComment.CreateDate.Format("01-02"),
}
originalComment是从数据库中查到的数据,comment是要返回给客户端的数据
敏感词词库
GitHub - fwwdn/sensitive-stop-words: 互联网常用敏感词、停止词词库
成果展示
客户端:视频标题
数据库:视频表:title字段
客户端:评论列表
数据库:评论表:content字段