本文目录导读:

在PHP项目中实现敏感词过滤,通常有以下几种常见方案,按推荐程度排序:
Trie树(前缀树)方案(推荐)
这是性能最好的方案,适合大规模敏感词库:
<?php
class SensitiveWordFilter {
private $trieTree = [];
/**
* 构建敏感词树
*/
public function loadWords(array $words) {
foreach ($words as $word) {
$this->addWord($word);
}
}
/**
* 添加单个敏感词到Trie树
*/
private function addWord($word) {
$node = &$this->trieTree;
$len = mb_strlen($word, 'UTF-8');
for ($i = 0; $i < $len; $i++) {
$char = mb_substr($word, $i, 1, 'UTF-8');
if (!isset($node[$char])) {
$node[$char] = [];
}
$node = &$node[$char];
}
// 标记敏感词结束
$node['is_end'] = true;
}
/**
* 过滤敏感词(替换为指定字符)
*/
public function filter($text, $replaceChar = '*') {
$result = '';
$len = mb_strlen($text, 'UTF-8');
$i = 0;
while ($i < $len) {
$node = $this->trieTree;
$matched = false;
$endPos = $i;
for ($j = $i; $j < $len; $j++) {
$char = mb_substr($text, $j, 1, 'UTF-8');
if (!isset($node[$char])) {
break;
}
$node = $node[$char];
if (isset($node['is_end'])) {
$endPos = $j;
$matched = true;
}
}
if ($matched) {
// 替换敏感词
$wordLen = $endPos - $i + 1;
$result .= str_repeat($replaceChar, $wordLen);
$i = $endPos + 1;
} else {
// 保留原字符
$result .= mb_substr($text, $i, 1, 'UTF-8');
$i++;
}
}
return $result;
}
/**
* 检查是否包含敏感词
*/
public function hasSensitiveWord($text) {
$len = mb_strlen($text, 'UTF-8');
for ($i = 0; $i < $len; $i++) {
$node = $this->trieTree;
for ($j = $i; $j < $len; $j++) {
$char = mb_substr($text, $j, 1, 'UTF-8');
if (!isset($node[$char])) {
break;
}
$node = $node[$char];
if (isset($node['is_end'])) {
return true;
}
}
}
return false;
}
}
// 使用示例
$filter = new SensitiveWordFilter();
$filter->loadWords(['敏感词1', '敏感词2', '测试']);
$text = '这是一段包含敏感词1和测试的文本';
$result = $filter->filter($text, '*');
echo $result; // 输出: 这是一段包含***和**的文本
if ($filter->hasSensitiveWord($text)) {
echo '包含敏感词';
}
DFA算法(确定有限自动机)
与Trie树类似,但有更灵活的匹配规则:
<?php
class DFAFilter {
private $wordTree = [];
public function loadWords(array $words) {
foreach ($words as $word) {
$this->addWord($word);
}
}
private function addWord($word) {
$len = mb_strlen($word, 'UTF-8');
$node = &$this->wordTree;
for ($i = 0; $i < $len; $i++) {
$char = mb_substr($word, $i, 1, 'UTF-8');
if (!isset($node[$char])) {
$node[$char] = [
'is_end' => false,
'child' => []
];
}
if ($i == $len - 1) {
$node[$char]['is_end'] = true;
}
$node = &$node[$char]['child'];
}
}
public function filter($text, $replace = '***') {
$result = '';
$textLen = mb_strlen($text, 'UTF-8');
for ($start = 0; $start < $textLen; $start++) {
$node = $this->wordTree;
$matchLen = 0;
$tempResult = '';
for ($i = $start; $i < $textLen; $i++) {
$char = mb_substr($text, $i, 1, 'UTF-8');
if (!isset($node[$char])) {
break;
}
$tempResult .= $char;
$matchLen++;
if ($node[$char]['is_end']) {
$result .= $replace;
$start = $i;
$matchLen = 0;
break;
}
$node = &$node[$char]['child'];
}
if ($matchLen == 0) {
$result .= mb_substr($text, $start, 1, 'UTF-8');
}
}
return $result;
}
}
正则表达式方案(简单但性能差)
<?php
class RegexFilter {
private $pattern = '';
public function loadWords(array $words) {
// 转义特殊字符
$escaped = array_map(function($word) {
return preg_quote($word, '/');
}, $words);
$this->pattern = '/(' . implode('|', $escaped) . ')/u';
}
public function filter($text, $replace = '*') {
return preg_replace($this->pattern, $replace, $text);
}
public function hasSensitiveWord($text) {
return preg_match($this->pattern, $text) === 1;
}
}
// 使用示例
$filter = new RegexFilter();
$filter->loadWords(['敏感词1', '敏感词2']);
$result = $filter->filter('这是一段包含敏感词1的文本', '***');
使用第三方库(推荐生产环境)
COS-AC自动机库
composer require lustre/multibyte-cjk-regular
use Multibyte\CJKRegular;
$filter = new CJKRegular();
$filter->addDictionary(['敏感词1', '敏感词2']);
$filter->addFromFile('/path/to/words.txt');
$result = $filter->replace('这是敏感词1文本', '***');
Redis+布隆过滤器(大规模高并发)
<?php
class BloomFilter {
private $redis;
private $key = 'sensitive_words_bloom';
public function __construct($redis) {
$this->redis = $redis;
}
public function addWord($word) {
$positions = $this->getHashPositions($word);
foreach ($positions as $position) {
$this->redis->setBit($this->key, $position, 1);
}
}
public function exists($word) {
$positions = $this->getHashPositions($word);
foreach ($positions as $position) {
if ($this->redis->getBit($this->key, $position) == 0) {
return false;
}
}
return true; // 有误判率
}
private function getHashPositions($str) {
// 多个哈希函数
$positions = [];
$positions[] = crc32($str) % 100000;
$positions[] = md5($str) % 100000;
$positions[] = sha1($str) % 100000;
return $positions;
}
}
实践建议
敏感词库管理
// 从文件加载
$words = file('sensitive_words.txt', FILE_IGNORE_NEW_LINES);
// 或者从数据库加载
$words = $db->query("SELECT word FROM sensitive_words")->fetchAll(PDO::FETCH_COLUMN);
性能优化
// 使用单例模式
class FilterManager {
private static $instance;
private $filter;
private function __construct() {
$this->filter = new SensitiveWordFilter();
$this->filter->loadWords($this->getWords());
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
}
完整过滤器类
<?php
class SensitiveFilter {
use SingletonTrait; // 假设有单例trait
private $filter;
private $cacheTime = 3600; // 缓存1小时
public function __construct() {
$this->filter = new DFAFilter();
$this->loadWords();
}
private function loadWords() {
// 优先从缓存加载
$words = Cache::get('sensitive_words');
if (!$words) {
$words = $this->fetchWordsFromDB();
Cache::set('sensitive_words', $words, $this->cacheTime);
}
$this->filter->loadWords($words);
}
public function filter($text) {
return $this->filter->filter($text);
}
public function hasSensitive($text) {
return $this->filter->hasSensitiveWord($text);
}
}
推荐方案
- 小型项目:Trie树方案(自己实现)
- 中型项目:第三方库如
lustre/multibyte-cjk-regular - 大型项目:Redis + Trie树 + Bloom Filter 组合方案
- 实时处理:AC自动机(Aho-Corasick)
选择方案时要考虑:敏感词库大小、文本长度、并发量、允许的误判率等因素。