项目中有时需要对文案过滤屏蔽词,违禁网址等,或者在审核中展示高亮词,涉及到对关键字的查找。
这里介绍字典树的实现方法。
字典树是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串,所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
一 树的形状
举例说明:“女性”、“女性服务”、“女性服务员特殊” 是屏蔽词,“女”、“女性服务员” 是相近的合法词。
生成如下字典树,END=1标识屏蔽词,END=3标识白名单,查询时从根往下遍历。
array (
'女' =>
array (
'END' => 3,
'性' =>
array (
'END' => 1,
'服' =>
array (
'务' =>
array (
'END' => 1,
'员' =>
array (
'END' => 3,
'特' =>
array (
'殊' =>
array (
'END' => 1,
),
),
),
),
),
),
),
);
二 树的生成
/**
* @brief Trie字典树
* @note 生成
* @author jichenghan
* @version 1.0
* @since 2022-08-30
*/
class MakeTrie
{
//字典树
private $aTrie;
/* 从字典中读取词汇,初始化字典树文件 */
public function initTrieDDFile()
{
include getenv("INCPATH")."/co/dd/dd_prohibitedword.php";//1.屏蔽词
foreach ($dd_prohibitedword as $sCValue)
{
$sCValue = mb_strtolower($sCValue, "GBK");
$this->addToTrieDD($sCValue, 1);
}
include getenv("INCPATH")."/co/dd/dd_prohibitedword_whitelist.php";//3.白名单
foreach ($dd_prohibitedword_whitelist as $sCValue)
{
$sCValue = mb_strtolower($sCValue, "GBK");
$this->addToTrieDD($sCValue, 3);
}
if (! $this->writeTrieDDFile("array"))
{
return false;
}
return true;
}
/* 将关键字加到字典树中 */
private function addToTrieDD($p_sCValue, $p_sType, $sEncoding = 'GBK')
{
$iLength = mb_strlen($p_sCValue, $sEncoding);
$sWord = mb_substr($p_sCValue, 0, 1, $sEncoding);
//首字初始化
if (!isset($this->aTrie[$sWord]))
{
$this->aTrie[$sWord] = array();
}
//递归调用
if ($iLength > 1)
{
$p_sCValue = mb_substr($p_sCValue, 1, $iLength-1, $sEncoding);
$this->addToTrieDD($p_sCValue, $this->aTrie[$sWord], $p_sType, $sEncoding);
}
//结束标记
else
{
$this->aTrie[$sWord]['END'] = $p_sType;
}
}
/* 生成字典树文件 */
private function writeTrieDDFile($p_stype="array")
{
if ($p_stype == 'json')
{
$file = getenv("BASEPATH")."/in/dd/newprohibitedword.json";
$text = json_encode($this->aTrie);
}
else
{
$file = getenv("INCPATH")."/co/dd/dd_newprohibitedword.php";
$text = var_export($this->aTrie, true);
$text = '<?php return '.$text.';';
}
if (file_put_contents($file, $text) === false)
{
return false;
}
return true;
}
}
三 树的遍历
/**
* @brief Trie字典树
* @note 使用
* @author jichenghan
* @version 1.0
* @since 2022-08-30
*/
class UseTrie
{
public $aNewProhibitedWord;//屏蔽词
public $aWordOffset;//屏蔽词在文本中的位置偏移 array(偏移量,长度)
public function __construct()
{
//上面生成的字典树
$this->aNewProhibitedWord = getDict('dd_newprohibitedword');
}
public function checkProhibitedWord($p_sText)
{
$aResult = ['status' => 0, 'rowcount' => 0, 'errormsg'=>''];
if (empty($this->aNewProhibitedWord) || !is_array($this->aNewProhibitedWord))
{
$aResult['errormsg'] = '字典文件异常';
return $aResult;
}
$p_sText = mb_strtolower($p_sText, "GBK"); //转小写
$iLength = mb_strlen($p_sText, 'GBK');
$iFlag = false;
$this->aWordOffset = [];
for ($i = 0; $i < $iLength; $i++)
{
$this->checkTrieProhibitedWord($p_sText, $i, $iLength);
}
if (!empty($this->aWordOffset))
{
$aResult['status'] = 1;
$aResult['rowcount'] = count($this->aWordOffset);
}
else
{
$aResult['status'] = 2;
}
$aResult['wordoffset'] = $this->aWordOffset;
return $aResult;
}
/* 检查替换屏蔽词 */
private function checkTrieProhibitedWord($p_sText, &$iBeginIndex, &$iLength)
{
$aResult = $this->searchTrieProhibitedWord($p_sText, $iBeginIndex, $iLength);
if ($aResult === false)
{
return false;
}
$aWhiteWordLength = $aResult['whitewordlength'];
$aReplaceWord = $aResult['replaceword'];
$aReplaceWordLength = $aResult['replacewordlength'];
if (!empty($aWhiteWordLength))
{
/* 找到白名单,向前移动白名单的长度 */
$iCount = count($aWhiteWordLength);
$iWhiteWordLength = $aWhiteWordLength[$iCount-1];
/* 屏蔽词为空 或者 白名单的长度大于屏蔽词的长度 */
if (empty($aReplaceWordLength) || $iWhiteWordLength > $aReplaceWordLength[count($aReplaceWordLength)-1])
{
if ($iWhiteWordLength >= 1)
{
$iBeginIndex += $iWhiteWordLength-1;
}
return false;
}
}
$iCount = count($aReplaceWord);
/* 屏蔽词,从后往前遍历 */
for ($i=$iCount-1; $i >= 0; $i--)
{
$iProhibitedWordLength = $aReplaceWordLength[$i];
//修改下次循环起始位置
$this->aWordOffset[] = array($iBeginIndex, $iProhibitedWordLength);
$iBeginIndex += $iProhibitedWordLength-1;
return true;
}
return false;
}
private function searchTrieProhibitedWord($p_sText, $iBeginIndex, $iLength)
{
$sProhibitedWord = '';
$iWordLength = 0;
$aReplaceWord = array();
$aReplaceWordLength = array();
$aWhiteWordLength = array();
$iFlag = false;
$aNewProhibitedWord = $this->aNewProhibitedWord;
/* 从$iBeginIndex开始,查找是否有匹配的屏蔽词 */
for ($i = $iBeginIndex; $i < $iLength; $i++)
{
$sWord = mb_substr($p_sText, $i, 1, 'GBK'); //检验单个字
$sWord = mb_strtolower($sWord, "GBK"); //转小写
//如果树中不存在,结束
if (!isset($aNewProhibitedWord[$sWord]))
{
break;
}
//如果存在
$sProhibitedWord .= $sWord;
$iWordLength++;
$aNewProhibitedWord = $aNewProhibitedWord[$sWord];
if (isset($aNewProhibitedWord['END']))
{
/* 3-白名单 结束处理 */
if ($aNewProhibitedWord['END'] === 3)
{
$aWhiteWordLength[] = $iWordLength;
}
/* 1-屏蔽词 */
elseif ($aNewProhibitedWord['END'] === 1)
{
$aReplaceWord[] = $sProhibitedWord;
$aReplaceWordLength[] = $iWordLength;
}
$iFlag = true;
}
}
if ($iFlag == true)
{
$a = array(
'replaceword' => $aReplaceWord,
'replacewordlength' => $aReplaceWordLength,
'whitewordlength' => $aWhiteWordLength,
);
return $a;
}
return false;
}
}
三 使用方式
录入如下文字:
$aTrie = new UseTrie();
$aTrie->checkProhibitedWord('招聘女性服务员和女性服务员特殊,提供女性服务');
第一轮从“女”字开头遍历“女性服务员”得到的偏移量:
array (
'replaceword' => array ( 0 => '女性', 1 => '女性服务', ),
'replacewordlength' => array ( 0 => 2, 1 => 4, ),
'whitewordlength' => array ( 0 => 1, 1 => 5, ),
)
找到“女性”和“女性服务”两个屏蔽词,对应的长度是2和4,找到“女”和“女性服务员”两个白名单,对应的长度是1和5,因为白名单的最大长度5大于屏蔽词的最大长度4,所以“女性服务员”不算屏蔽词。
整体返回结果:
array (
'wordoffset' => array (
0 => array ( 0 => 8, 1 => 7, ),
1 => array ( 0 => 18, 1 => 4, ),
),
)
对应从下标为8开始长度为7的“女性服务员特殊”,和下标为18开始长度为4的“女性服务”。
这样就找到在一段文本中的屏蔽词,并且过滤了白名单。