strpos循环撑不住500条敏感词是因为算法复杂度为o(n×m),qps超30即cpu持续>70%,且str_replace易误伤“和谐”等词;正确解法是dfa树,建树用mb_substr安全切分、索引数组存储、循环建树防溢出,缓存为php数组并确保权限与apcu开启。

为什么 strpos 循环在 ThinkPHP 6 中撑不住 500 条敏感词
不是代码写错了,是算法选错了。当词库超过 300 条,foreach + strpos 遍历每条评论,本质是 O(n×m) 时间复杂度 —— 每个字符都要扫一遍词表。QPS 上 30 就可能看到 CPU 持续 >70%,str_replace 还会误伤“和谐”“河蟹”这类变形词。
- 错误现象:
preg_match_all('/'.implode('|', $words).'/', $content)在词库过千时直接超时或内存溢出 - 别把敏感词塞进 validate 规则里 —— 它只校验字段格式,不负责语义匹配
- 数据库
WHERE word IN (...)查询不是过滤动作,只是查词是否存在,不能替代文本扫描 - 真正要解决的是“单次扫描完成全部匹配”,DFA 的 O(n) 才是解法
怎么手写一个轻量 DFA 树,不依赖第三方包
ThinkPHP 6 不自带 DFA 实现,但不需要引入完整 AC 自动机。核心就两个动作:建树 + 扫描,30 行内能跑通。重点不在功能多,而在内存省、中文稳、不递归。
- 节点必须用索引数组:
['is_end' => true, 'next' => []],不用对象,避免 PHP-FPM 下反复实例化开销 - 切分汉字必须用
mb_substr($word, $i, 1, 'utf-8'),str_split会把一个汉字拆成 3 个乱码字节,树就废了 - 建树禁用递归,改用
for循环 + 引用传递:&$currentNode,防超长词触发栈溢出 - 末尾节点加
'is_end' => true标识,中间节点也得显式设'is_end' => false,否则匹配逻辑会错判
如何把 DFA 树缓存到文件并复用
每次请求都重建树,等于把性能瓶颈从匹配阶段转移到初始化阶段。缓存不是可选项,是刚需 —— 尤其当你的词库每天只更新几次,而评论请求每秒几十次。
- 构建完成后,用
var_export($tree, true)导出为纯 PHP 数组,写入runtime/cache/dfa_tree.php - 后续请求直接
require该文件,比 JSON 解析快 3 倍以上,且无反序列化开销 - 新增/禁用敏感词后,必须主动删除该缓存文件,否则新词永远不生效
- 别用 APCu 缓存树结构 —— 如果
apcu.enabled=0,会导致每次请求重建,CPU 突增(这是线上最常被忽略的配置点)
在 ThinkPHP 6 中间件里怎么安全调用 DFA 过滤器
过滤逻辑不能散落在控制器里,也不能塞进 Session 或 Request 对象 —— 树是只读静态资源,必须由容器统一管理生命周期。
- 在
app/middleware/ContentFilterMiddleware.php的handle()方法开头require runtime/cache/dfa_tree.php,然后 newDfaFilter($root) - 调用
$filter->search($content)返回布尔值即可判断是否命中,不要等它返回所有词 —— 拦截场景只需“有/无” - 若需替换,先用
$filter->getPositions($content)获取所有[(start, end, word)],再按起始位置倒序替换,防止字符串偏移 - 禁止在中间件里做日志记录或跳转 —— 这些应交由异常处理器统一处理,保持中间件职责单一
最易被忽略的点:缓存文件路径权限和 APCu 开关状态。很多团队测着没问题,上线后 CPU 暴涨,最后发现是 runtime/cache 目录不可写,或服务器没开 APCu 却写了缓存逻辑。DFA 本身很稳,卡住的往往是这些外围细节。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











