Files
bl/common/utils/go-sensitive-word-1.3.3/docs/dfa.md

151 lines
6.6 KiB
Go
Raw Normal View History

# DFA 算法实现敏感词过滤基于 Trie 的实现
敏感词过滤是许多应用中必不可少的功能用于防止敏感或不当内容的出现本文档介绍基于 Trie前缀树的敏感词过滤实现借鉴 DFA 思想包括构建流程匹配逻辑示意图实现细节与注意点文档贴合已有 Go 语言代码实现包含行为细节边界说明以及与 Aho-Corasick 算法的比较便于开发者正确使用和扩展
## 基本思想
- Trie/DFA 思想的多模式匹配将敏感词逐字符插入到树中 rune 为边匹配时从文本的每个起点开始沿树向下扫描检测是否存在敏感词
- Aho-Corasick 的区别
- Aho-Corasick Trie 的基础上增加 **failure **实现整体文本的线性扫描复杂度 O(n + totalPatternLength)
- 本实现采用 **逐起点尝试匹配** 的方式最坏情况下复杂度可能达到 O(n × m)n 为文本长度m 为敏感词最大长度
- 优点逻辑直观实现简单插入与查询容易理解
- 适用场景中小规模敏感词过滤大规模场景建议优化为 Aho-Corasick
## 组成部分
- 敏感词库存储Store保存所有敏感词可从文件数据库或远程接口加载提供添加删除功能
- DFA 模型Filter Trie 结构存储敏感词并完成匹配与处理每个节点用 `children map[rune]*dfaNode` 表示子节点 `isLeaf` bool 标记词尾
## 数据结构
```go
type dfaNode struct {
children map[rune]*dfaNode
isLeaf bool
}
type DfaModel struct {
root *dfaNode
}
```
- children map[rune]*dfaNode以字符为边存储当前节点的所有子节点
- isLeaf bool标记从 root 到当前节点的路径是否构成一个完整敏感词
- root空前缀状态所有匹配从这里开始
注意相同字符在不同父路径下会对应不同 dfaNode 实例路径唯一决定节点不会全局复用
## 构建过程AddWord / AddWords
- **初始化**创建根节点 `root`非叶子
- **插入流程**
1. 将敏感词转换为 []rune支持多字节字符如中文emoji
2. root 出发
- 若当前字符已存在子节点沿该节点前进
- 否则新建子节点并连接
3. 遍历结束后将最后节点标记为 isLeaf = true
- **批量添加**对多个词重复该过程共享前缀的词会复用节点
- **时间与空间复杂度**
- 时间O(L)L 为词长
- 空间新增节点数约等于新增字符数共享前缀可节省空间总空间与所有词字符总数相关
## 节点树示意图
### 简单示例
敏感词"敏感词""敏锐""铭记"
```mermaid
graph TD
Root((root))
Root --> ["敏"]
Root --> ["铭"]
--> ["感"]
--> ["词 (isLeaf)"]
--> ["锐 (isLeaf)"]
--> ["记 (isLeaf)"]
```
说明每条路径 root -> ... -> (isLeaf) 表示一个敏感词相同字符在不同路径下对应不同节点实例
### 复杂示例
敏感词"台海国""台海独立""台海总统""台海帝国""台界帝国"
```mermaid
graph TD
Root((root))
Root --> ["台"]
--> ["海"]
--> 海国["国 (isLeaf)"]
--> 海独["独"]
海独 --> 海独立["立 (isLeaf)"]
--> 海总["总"]
海总 --> 海总统["统 (isLeaf)"]
--> 海帝["帝"]
海帝 --> 海帝国["国 (isLeaf)"]
--> ["界"]
--> 界帝["帝"]
界帝 --> 界帝国["国 (isLeaf)"]
```
要点`` 虽然是同一字符但在 Trie 中依赖父节点而存在多个不同节点实例并非全局唯一
## 匹配逻辑
**总体思路**按文本中的每个起点 start 向下沿 Trie 尝试匹配若在某位置遇到 isLeaf 则记录匹配若在某字符处无法继续匹配则把起点向右滑动一个位置重试 start++从文本的第一个字符开始根据字符在 Trie 中的转移逐步遍历整个文本如果当前字符找不到对应的转移则回到根节点重新开始匹配下一个字符如果匹配到叶子节点则表示找到了一个敏感词记录下该敏感词并继续匹配当匹配完成整个文本后返回所有匹配到的敏感词列表
### 核心变量
- runes输入文本转为 []rune
- start当前起点索引
- pos当前扫描位置
- parentTrie 中当前节点起点时为 root
- now当前字符对应的子节点
### 搜索伪代码 FindAll 为例
```go
start := 0
parent := root
for pos := 0; pos < length; pos++ {
now, found = parent.children[runes[pos]]
if !found {
// 当前路径没有继续,重置为新的起点
parent = root
pos = start // 重置 pos 为 start注意后续循环会再自动 pos++
start++
continue
}
// 找到子节点
if now.isLeaf && start <= pos {
// 在 [start, pos] 区间上匹配到一个完整敏感词
record match runes[start: pos + 1]
}
if pos == length - 1 {
// 到达文本末尾,重置以便从下一个起点开始
parent = root
pos = start
start++
continue
}
// 继续沿 Trie 向下匹配
parent = now
}
```
控制流要点未找到匹配时parent 重置为 root并将 pos = start, start++下一轮循环 pos++ 正好移动到下一个起点整体效果对每个起点尽可能延长匹配失败则向后滑动一位
### 匹配复杂度
- 本实现最坏时间复杂度O(n × m)n 文本长度m 敏感词最大长度
- 若需要严格线性时间可扩展为 Aho-Corasick failure-link
## 示例FindAll 执行过程
敏感词"敏锐", "敏感词", "铭记"
文本"我们要铭记敏锐的观察和敏感词出现"
- "铭" 开始匹配到 "铭记"
- 继续扫描匹配到 "敏锐"
- 再次匹配到 "敏感词"
- 返回结果"铭记", "敏锐", "敏感词"顺序可能随实现而异
## 复杂度分析
- 构建AddWord
- 时间O(L)
- 空间与总词长相关共享前缀节省空间
- 匹配FindAll
- 最坏O(n × m)
- Aho-CorasickO(n + totalPatternLength)
## 总结
- 本实现基于 Trie DFA 思想逻辑清晰易于理解和实现
- 缺点匹配效率在大规模词库时不如 Aho-Corasick
- 扩展建议添加 failure 链以优化性能支持更多字符类型和动态词库更新