[原创] Trie树 php 实现敏感词过滤
背景
项目中需要过滤用户发送的聊天文本, 由于敏感词有将近2W条, 如果用 str_replace
来处理会炸掉的.
网上了解了一下, 在性能要求不高的情况下, 可以自行构造 Trie树(字典树), 这就是本文的由来.
简介
Trie树是一种搜索树, 也叫字典树、单词查找树.
DFA可以理解为DFA(Deterministic Finite Automaton), 即
这里借用一张图来解释Trie树的结构:
Trie可以理解为确定有限状态自动机,即DFA。在Trie树中,每个节点表示一个状态,每条边表示一个字符,从根节点到叶子节点经过的边即表示一个词条。查找一个词条最多耗费的时间只受词条长度影响,因此Trie的查找性能是很高的,跟哈希算法的性能相当。
上面实际保存了
abcd
abd
b
bcd
efg
hij
特点:
- 所有词条的公共前缀只存储一份
- 只需遍历一次待检测文本
- 查找消耗时间只跟待检测文本长度有关, 跟字典大小无关
存储结构
PHP
在PHP中, 可以很方便地使用数组来存储树形结构, 以以下敏感词字典为例:
大傻子
大傻
傻子
↑ 内容纯粹是为了举例...游戏聊天日常屏蔽内容
则存储结构为
{
"大": {
"傻": {
"end": true
"子": {
"end": true
}
}
},
"傻": {
"子": {
"end": true
},
}
}
其他语言
简单点的可以考虑使用 HashMap 之类的来实现
或者参考 这篇文章 , 使用 Four-Array Trie,Triple-Array Trie和Double-Array Trie 结构来设计(名称与内部使用的数组个数有关)
字符串分割
无论是在构造字典树或过滤敏感文本时, 都需要将其分割, 需要考虑到unicode字符
有一个简单的方法:
$str = "a笨蛋123"; // 待分割的文本
$arr = preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY); // 分割后的文本
// 输出
array(6) {
[0]=>
string(1) "a"
[1]=>
string(3) "笨"
[2]=>
string(3) "蛋"
[3]=>
string(1) "1"
[4]=>
string(1) "2"
[5]=>
string(1) "3"
}
匹配规则需加
u
修饰符,/u
表示按unicode(utf-8)匹配(主要针对多字节比如汉字), 否则会无法正常工作, 如下示例 ↓$str = "a笨蛋123"; // 待分割的文本
$arr = preg_split("//", $str, -1, PREG_SPLIT_NO_EMPTY); // 分割后的文本
// array(10) {
[0]=>
string(1) "a"
[1]=>
string(1) "�"
[2]=>
string(1) "�"
[3]=>
string(1) "�"
[4]=>
string(1) "�"
[5]=>
string(1) "�"
[6]=>
string(1) "�"
[7]=>
string(1) "1"
[8]=>
string(1) "2"
[9]=>
string(1) "3"
}
示例代码 php
构建:
1. 分割敏感词
2. 逐个将分割后的次添加到树中
使用:
- 分割待处理词句
- 从Trie树根节点开始逐个匹配
class SensitiveWordFilter
{
protected $dict;
protected $dictFile;
/**
* @param string $dictFile 字典文件路径, 每行一句
*/
public function __construct($dictFile)
{
$this->dictFile = $dictFile;
$this->dict = [];
}
public function loadData($cache = true)
{
$memcache = new Memcache();
$memcache->pconnect("127.0.0.1", 11212);
$cacheKey = __CLASS__ . "_" . md5($this->dictFile);
if ($cache && false !== ($this->dict = $memcache->get($cacheKey))) {
return;
}
$this->loadDataFromFile();
if ($cache) {
$memcache->set($cacheKey, $this->dict, null, 3600);
}
}
/**
* 从文件加载字典数据, 并构建 trie 树
*/
public function loadDataFromFile()
{
$file = $this->dictFile;
if (!file_exists($file)) {
throw new InvalidArgumentException("字典文件不存在");
}
$handle = @fopen($file, "r");
if (!is_resource($handle)) {
throw new RuntimeException("字典文件无法打开");
}
while (!feof($handle)) {
$line = fgets($handle);
if (empty($line)) {
continue;
}
$this->addWords(trim($line));
}
fclose($handle);
}
/**
* 分割文本(注意ascii占1个字节, unicode...)
*
* @param string $str
*
* @return string[]
*/
protected function splitStr($str)
{
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
/**
* 往dict树中添加语句
*
* @param $wordArr
*/
protected function addWords($words)
{
$wordArr = $this->splitStr($words);
$curNode = &$this->dict;
foreach ($wordArr as $char) {
if (!isset($curNode)) {
$curNode[$char] = [];
}
$curNode = &$curNode[$char];
}
// 标记到达当前节点完整路径为"敏感词"
$curNode['end']++;
}
/**
* 过滤文本
*
* @param string $str 原始文本
* @param string $replace 敏感字替换字符
* @param int $skipDistance 严格程度: 检测时允许跳过的间隔
*
* @return string 返回过滤后的文本
*/
public function filter($str, $replace = '*', $skipDistance = 0)
{
$maxDistance = max($skipDistance, 0) + 1;
$strArr = $this->splitStr($str);
$length = count($strArr);
for ($i = 0; $i < $length; $i++) {
$char = $strArr[$i];
if (!isset($this->dict[$char])) {
continue;
}
$curNode = &$this->dict[$char];
$dist = 0;
$matchIndex = [$i];
for ($j = $i + 1; $j < $length && $dist < $maxDistance; $j++) {
if (!isset($curNode[$strArr[$j]])) {
$dist ++;
continue;
}
$matchIndex[] = $j;
$curNode = &$curNode[$strArr[$j]];
}
// 匹配
if (isset($curNode['end'])) {
// Log::Write("match ");
foreach ($matchIndex as $index) {
$strArr[$index] = $replace;
}
$i = max($matchIndex);
}
}
return implode('', $strArr);
}
/**
* 确认所给语句是否为敏感词
*
* @param $strArr
*
* @return bool|mixed
*/
public function isMatch($strArr)
{
$strArr = is_array($strArr) ? $strArr : $this->splitStr($strArr);
$curNode = &$this->dict;
foreach ($strArr as $char) {
if (!isset($curNode[$char])) {
return false;
}
}
// return $curNode['end'] ?? false; // php 7
return isset($curNode['end']) ? $curNode['end'] : false;
}
}
字典文件示例:
敏感词1
敏感词2
敏感词3
...
使用示例:
$filter = new SensitiveWordFilter(PATH_APP . '/config/dirty_words.txt');
$filter->loadData()
$filter->filter("测试123文本",'*', 2)
优化
缓存字典树
原始敏感词文件大小: 194KB(约20647行)
生成字典树后占用内存(约): 7MB
构建字典树消耗时间: 140ms+ !!!
php 的内存占用这点...先放着
构建字典树消耗时间这点是可以优化的: 缓存!
由于php脚本不是常驻内存类型, 每次新的请求到来时都需要构建字典树.
我们通过将生成好的字典树数组缓存(memcached 或 redis), 在后续请求中每次都从缓存中读取, 可以大大提高性能.
经过测试, 构建字典树的时间从 140ms+ 降低到 6ms 不到,
注意:
- memcached 默认会自动序列化缓存的数组(serialize), 取出时自动反序列化(unserialize)
- 若是redis, 则需要手动, 可选择 json 存取
序列化上述生成的Trie数组后的字符长度:
- serialize: 426KB
- json: 241KB
提示: 因此若整个字典过大, 导致存入memcached时超出单个value大小限制时(默认是1M), 可以考虑手动 json 序列化数组再保存.
↑ ...刚发现memcache存入value时提供压缩功能, 可以考虑使用
常驻服务
若是将过滤敏感字功能独立为一个常驻内存的服务, 则构建字典树这个过程只需要1次, 后续值需要处理过滤文本的请求即可.
如果是PHP, 可以考虑使用 Swoole
由于项目当前敏感词词库仅2W条左右, 而且访问瓶颈并不在此, 因此暂时使用上述方案.
ab测试时单个
若是词库达上百万条, 那估计得考虑一下弄成常驻内存的服务了
这里有一篇 文章 测试了使用 Swoole(
swoole_http_server
) + trie-filter 扩展, 词库量级200W
参考文章
关键词过滤扩展,用于检查一段文本中是否出现敏感词,基于Double-Array Trie 树实现
↑ 现成的php扩展, 同时支持 php5、php7
-
↑ 深入浅出讲解
trie_filter扩展 + swoole 实现敏感词过滤
↑ 简单的php高性能实现方式
[原创] Trie树 php 实现敏感词过滤的更多相关文章
- Trie性能分析之敏感词过滤golang
package util import ( "strings" ) type Node struct { //rune表示一个utf8字符 char rune Data inter ...
- 字典树Trie--实现敏感词过滤
序言 Trie树 资料 https://blog.csdn.net/m0_37907797/article/details/103272967?utm_source=apphttps://blog.c ...
- 转,敏感词过滤,PHP实现的Trie树
原文地址:http://blog.11034.org/2012-07/trie_in_php.html 项目需求,要做敏感词过滤,对于敏感词本身就是一个CRUD的模块很简单,比较麻烦的就是对各种输入的 ...
- [转载]敏感词过滤,PHP实现的Trie树
原文地址:http://blog.11034.org/2012-07/trie_in_php.html 项目需求,要做敏感词过滤,对于敏感词本身就是一个CRUD的模块很简单,比较麻烦的就是对各种输入的 ...
- DFA和trie特里实现敏感词过滤(python和c语言)
今天的项目是与完成python开展,需要使用做关键词检查,筛选分类,使用前c语言做这种事情.有了线索,非常高效,内存小了,检查快. 到达python在,第一个想法是pip基于外观的c语言python特 ...
- 用php实现一个敏感词过滤功能
周末空余时间撸了一个敏感词过滤功能,下边记录下实现过程. 敏感词,一方面是你懂的,另一方面是我们自己可能也要过滤一些人身攻击或者广告信息等,具体词库可以google下,有很多. 过滤敏感词,使用简单的 ...
- 浅析敏感词过滤算法(C++)
为了提高查找效率,这里将敏感词用树形结构存储,每个节点有一个map成员,其映射关系为一个string对应一个TreeNode. STL::map是按照operator<比较判断元素是否相同,以及 ...
- 转:鏖战双十一-阿里直播平台面临的技术挑战(webSocket, 敏感词过滤等很不错)
转自:http://www.infoq.com/cn/articles/alibaba-broadcast-platform-technology-challenges 鏖战双十一-阿里直播平台面临的 ...
- Jsp敏感词过滤
Jsp敏感词过滤 大部分论坛.网站等,为了方便管理,都进行了关于敏感词的设定. 在多数网站,敏感词一般是指带有敏感政治倾向(或反执政党倾向).暴力倾向.不健康色彩的词或不文明语,也有一些网站根据自身实 ...
随机推荐
- day25 map,filter,reduce 内置函数,作业
=====================作业一#用map来处理字符串列表啊,把列表中所有人都变成sb,比方alex_sbname=['alex','wupeiqi','yuanhao']###### ...
- gnuc与ansic
GNU c与标准c的区别 1) 零长度数组 struct var_data { int len; char data[0]; }test: int a; test.data -->a 2)cas ...
- 交互原型画得丑?29个优秀UI/UX线框草图
现在越来越多UI设计师都需要画一些交互的线框图,然而作为视觉专家,当然要把它画得靓靓的嘛,是不是?所以很多用户会使用<5款高效的原型设计工具>来绘制,或者直接手绘更有逼格. 今天达人手工整 ...
- Effective Java 中文版
始读于2014年8月2日10:15,整理完成于2014年8月20日23:14:42 一图一世界,<Effective Java >是Java领域大牛Joshua Bloch的获奖之作,去年 ...
- WCF技术剖析(卷1)WCF全面解析文摘
第1章 wcf简介 soa体现的是一种对关注点进行分解的思想,与技术无关 soa的基本思想: a.服务自治 独立部署,不依赖其他 b.依赖于开放的标准 采用xml,xsd,及wsdl作为服务描述的 ...
- mysql的.sql文件头部 /*!32312 IF NOT EXISTS*/;
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RE ...
- IO多路复用原理
(1)IO multiplexing(2)用在什么地方?多路非阻塞式IO.(3)select和poll(4)外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO IO多路复用原理:其实就是整个函数对外表现为 ...
- C++ 数据封装和抽象
C++ 数据抽象 数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节. 数据抽象是一种依赖于接口和实现分离的编程(设计)技术. 让我们举一个现实生活中的真实例子 ...
- Jmeter Cookie管理器 获取JSESSIONID
1.打开jmeter.抓包添加Web请求后,添加Cookie管理器.直接添加就行.值要不要都一样 添加值:${COOKIE_JSESSIONID 域:${server} 2.点击载入到当前脚本 3.到 ...
- linux数据库备份
linux数据库备份 服务端启用二进制日志 如果日志没有启开,必须启用binlog,要重启mysql,首先,关闭mysql,打开/etc/my.cnf,加入以下几行: [mysqld] log-bin ...