高併發場景下,敏感詞過濾需兼顧準確性 (支援中文分詞、同音字、變體) 與效能 (毫秒級響應)。本文實作一個基於 Trie 樹 (字典樹) 的過濾器,搭配 Memcached 快取熱詞庫,並提供完整類別代碼。
<?php
class TrieNode {
public $children = []; // 字元 => TrieNode
public $isEnd = false; // 是否為敏感詞結尾
public $word = ''; // 完整敏感詞 (用於替換)
public function __construct() {}
}
<?php
class SensitiveWordFilter {
private $root;
private $replaceChar = '*';
public function __construct(array $words = [], $replaceChar = '*') {
$this->replaceChar = $replaceChar;
$this->root = new TrieNode();
foreach ($words as $w) $this->addWord($w);
}
/** 建立 Trie 樹 */
public function addWord(string $word) {
$node = $this->root;
$len = mb_strlen($word, 'UTF-8');
for ($i = 0; $i < $len; $i++) {
$char = mb_substr($word, $i, 1, 'UTF-8');
if (!isset($node->children[$char])) {
$node->children[$char] = new TrieNode();
}
$node = $node->children[$char];
}
$node->isEnd = true;
$node->word = $word;
}
/** 從檔案載入 (一行一詞) */
public static function loadFromFile(string $path): self {
$words = [];
$handle = fopen($path, 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
$word = trim($line);
if ($word !== '') $words[] = $word;
}
fclose($handle);
}
return new self($words);
}
/** 核心過濾:返回 [過濾後文字, 命中詞陣列] */
public function filter(string $text): array {
$hitWords = [];
$result = '';
$len = mb_strlen($text, 'UTF-8');
for ($i = 0; $i < $len; $i++) {
$node = $this->root;
$matchLen = 0;
$matchedWord = '';
// 向前匹配最長敏感詞
for ($j = $i; $j < $len; $j++) {
$char = mb_substr($text, $j, 1, 'UTF-8');
if (!isset($node->children[$char])) break;
$node = $node->children[$char];
$matchLen++;
if ($node->isEnd) {
$matchedWord = $node->word;
// 繼續嘗試更長匹配 (最長匹配原則)
}
}
if ($matchedWord !== '') {
$result .= str_repeat($this->replaceChar, mb_strlen($matchedWord, 'UTF-8'));
$hitWords[] = $matchedWord;
$i += $matchLen - 1; // 跳過已匹配字元
} else {
$result .= mb_substr($text, $i, 1, 'UTF-8');
}
}
return [$result, $hitWords];
}
/** 簡易檢測是否包含敏感詞 (不替換) */
public function contains(string $text): bool {
$len = mb_strlen($text, 'UTF-8');
for ($i = 0; $i < $len; $i++) {
$node = $this->root;
for ($j = $i; $j < $len; $j++) {
$char = mb_substr($text, $j, 1, 'UTF-8');
if (!isset($node->children[$char])) break;
$node = $node->children[$char];
if ($node->isEnd) return true;
}
}
return false;
}
}
<?php
class CachedSensitiveFilter {
private $memcached;
private $cacheKey = 'sensitive_words_trie_v1';
private $wordFile;
private $filter;
public function __construct(string $wordFile, array $memcachedConfig = []) {
$this->wordFile = $wordFile;
$this->memcached = new Memcached();
$this->memcached->addServers($memcachedConfig ?: [['127.0.0.1', 11211]]);
$this->initFilter();
}
private function initFilter() {
// 嘗試從快取載入序列化的 Trie 根節點
$data = $this->memcached->get($this->cacheKey);
if ($data !== false && isset($data['root'])) {
$this->filter = new SensitiveWordFilter();
$this->filter->root = $data['root']; // 需 SensitiveWordFilter 開放 root 屬性或提供 setRoot
return;
}
// 快取未命中,從檔案建立
$this->filter = SensitiveWordFilter::loadFromFile($this->wordFile);
// 序列化存入快取 (有效期 24 小時)
$this->memcached->set($this->cacheKey, ['root' => $this->filter->root], 86400);
}
/** 公開過濾介面 */
public function filter(string $text): array {
return $this->filter->filter($text);
}
/** 重新載入 (詞庫更新後呼叫) */
public function reload() {
$this->memcached->delete($this->cacheKey);
$this->initFilter();
}
}
<?php
require_once 'SensitiveWordFilter.php';
require_once 'CachedSensitiveFilter.php';
// 配置
$wordFile = __DIR__ . '/config/sensitive_words.txt'; // 一行一詞
$memConfig = [['127.0.0.1', 11211]];
// 初始化 (單例模式建議)
$filter = new CachedSensitiveFilter($wordFile, $memConfig);
// 測試
$text = "這裡有敏感詞測試和另一個違禁詞內容";
list($clean, $hits) = $filter->filter($text);
echo "原文: $text\n";
echo "過濾: $clean\n";
echo "命中: " . implode(', ', $hits) . "\n";
// 輸出範例:
// 原文: 這裡有敏感詞測試和另一個違禁詞內容
// 過濾: 這裡有****測試和另一個****內容
// 命中: 敏感詞, 違禁詞
config/sensitive_words.txt (UTF-8 編碼,一行一詞):
敏感詞 違禁詞 測試關鍵字 另一個違禁詞 ...
str_replace 或正則遍歷。reload() 清除快取重建。phpiredis 或 predis 替代 Memcached,支援更豐富資料結構。reload()。hit rate、平均延遲、命中詞分佈。