摘要:在树中,每个节点表示一个状态,每条边表示一个字符,从根节点到叶子节点经过的边即表示一个词条。查找一个词条最多耗费的时间只受词条长度影响,因此的查找性能是很高的,跟哈希算法的性能相当。
Last-Modified: 2019年5月10日15:25:35
参考文章c++ 使用map实现Trie树
关键词过滤扩展,用于检查一段文本中是否出现敏感词,基于Double-Array Trie 树实现
↑ 现成的php扩展, 同时支持 php5、php7
从Trie到Double Array Trie
↑ 深入浅出讲解
前缀树匹配 Double Array Trie
trie_filter扩展 + swoole 实现敏感词过滤
↑ 简单的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" }
示例代码 php匹配规则需加 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" }
构建:
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
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/31442.html
摘要:问龙哥,还有什么更好,更轻量级的方案么龙哥用树,数据会膨胀文档数标题长度这么多,标题越长,文档数越多,内存占用越大。 一、需求缘起某并发量很大,数据量适中的业务线需要实现一个标题检索的功能:(1)并发量较大,每秒20w次(2)数据量适中,大概200w数据(3)是否需要分词:是(4)数据是否实时更新:否 二、常见潜在解决方案及优劣(1)数据库搜索法具体方法:将标题数据存放在数据库中,使用...
摘要:我们可以先用待查单词建立一个字典树,这样我们在从矩阵中某个点开始深度优先搜索时,可以直接用字典树判断当前组成的字符串是否是某个单词的前缀。字典树同样也提供接口,所以同样可以用于判断是否已经搜索到这个词了。 Word Search I 更新的思路与解法请访问:https://yanjia.me/zh/2018/11/... Given a 2D board and a word, f...
摘要:异步事件处理本项目涉及到多种异步事件的处理。即是的粉丝,是的关注对象。模式定义优缺点推事件触发后广播给所有粉丝。具体来说,推模式就是事件触发后产生,触发事件的用户下所有粉丝的实现中都存入该的。 项目源代码已托管在 Github,欢迎 Star、Fork。 Q & A 问答社区 QA 是一个基于 B/S 架构而设计开发的社区网站。 showImg(https://segmentfault...
阅读 3489·2021-11-25 09:43
阅读 1245·2021-09-08 09:45
阅读 2624·2021-09-07 09:59
阅读 1453·2021-08-09 13:45
阅读 3290·2019-08-30 15:54
阅读 657·2019-08-29 18:35
阅读 475·2019-08-29 17:18
阅读 974·2019-08-29 14:10