up
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""敏感词服务:DFA 算法,词库 Redis 缓存 60s(后台维护后即时生效,对齐《02-技术文档》6.7)"""
|
||||
import json
|
||||
import time
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.common import SensitiveWord
|
||||
|
||||
CACHE_KEY = "words:sensitive"
|
||||
CACHE_TTL = 60
|
||||
|
||||
|
||||
class DFAMatcher:
|
||||
"""确定性有穷自动机敏感词匹配器"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.root: dict = {}
|
||||
|
||||
def add(self, word: str) -> None:
|
||||
node = self.root
|
||||
for ch in word.strip().lower():
|
||||
if not ch:
|
||||
continue
|
||||
node = node.setdefault(ch, {})
|
||||
node["\x00"] = True # 词结束标记
|
||||
|
||||
def find(self, text: str) -> str | None:
|
||||
"""返回命中的第一个敏感词,未命中返回 None"""
|
||||
text = text.lower()
|
||||
n = len(text)
|
||||
for i in range(n):
|
||||
node = self.root
|
||||
j = i
|
||||
while j < n and text[j] in node:
|
||||
node = node[text[j]]
|
||||
j += 1
|
||||
if "\x00" in node:
|
||||
return text[i:j]
|
||||
return None
|
||||
|
||||
|
||||
# 进程内缓存(matcher, 构建时间戳)
|
||||
_local_cache: tuple[DFAMatcher, float] | None = None
|
||||
|
||||
|
||||
async def get_matcher(db: AsyncSession, redis: aioredis.Redis) -> DFAMatcher:
|
||||
"""获取 DFA 匹配器:进程缓存 → Redis → DB,60s 重建"""
|
||||
global _local_cache
|
||||
now = time.time()
|
||||
if _local_cache and now - _local_cache[1] < CACHE_TTL:
|
||||
return _local_cache[0]
|
||||
|
||||
words: list[str] = []
|
||||
cached = await redis.get(CACHE_KEY)
|
||||
if cached:
|
||||
words = json.loads(cached)
|
||||
else:
|
||||
rows = (await db.execute(select(SensitiveWord.word))).scalars().all()
|
||||
words = [w for w in rows if w]
|
||||
await redis.set(CACHE_KEY, json.dumps(words, ensure_ascii=False), ex=CACHE_TTL)
|
||||
|
||||
matcher = DFAMatcher()
|
||||
for w in words:
|
||||
matcher.add(w)
|
||||
_local_cache = (matcher, now)
|
||||
return matcher
|
||||
|
||||
|
||||
async def find_sensitive(db: AsyncSession, redis: aioredis.Redis, text: str) -> str | None:
|
||||
"""检查文本,返回命中的敏感词或 None"""
|
||||
if not text:
|
||||
return None
|
||||
matcher = await get_matcher(db, redis)
|
||||
return matcher.find(text)
|
||||
|
||||
|
||||
async def invalidate_cache(redis: aioredis.Redis) -> None:
|
||||
"""后台增删词库后调用:清 Redis 缓存,进程缓存等 60s 内自然过期"""
|
||||
global _local_cache
|
||||
_local_cache = None
|
||||
await redis.delete(CACHE_KEY)
|
||||
Reference in New Issue
Block a user