使用字典树实现敏感词过滤 | 青训营笔记

614 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第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
}

别忘了使用过滤器前先初始化!!!

go读写文件 - 简书 (jianshu.com)

把敏感词添加到前缀树中

//将一个敏感词添加到前缀树中
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指向前缀树、beginposition指向需要过滤的文本。

如果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: 互联网常用敏感词、停止词词库

成果展示

客户端:视频标题 image-20220609001807023.png

数据库:视频表:title字段 image-20220609001821835.png


客户端:评论列表 image-20220609002029335.png

数据库:评论表:content字段 image-20220609002115162.png