
本文介绍一种基于 PCRE 动词 (*SKIP)(*FAIL) 的高效正则方案,用于将布尔搜索字符串(如 +test "hello world" (a b))精准切分为原子词元,正确处理引号内空格、独立括号及冒号键值对。
本文介绍一种基于 pcre 动词 `(*skip)(*fail)` 的高效正则方案,用于将布尔搜索字符串(如 `+test "hello world" (a b)`)精准切分为原子词元,正确处理引号内空格、独立括号及冒号键值对。
在构建高级全文搜索解析器(例如扩展 MySQL MATCH ... AGAINST 的布尔模式)时,常需将用户输入的类 Lucene 查询字符串(如 +test +word any -sample (+toto +titi "generic test") -column:"test this")分解为语义明确的词元(tokens)。关键挑战在于:空格仅在引号外作为分隔符;双引号包裹的子串必须整体保留(含内部空格);左右圆括号 ( 和 ) 必须各自成为独立词元;且需兼容 key:value 或 key:"value with spaces" 等列限定语法。
直接使用 S+ 或简单空格分割会破坏 "generic test" 的完整性;而传统引用匹配(如 "[^"]*")与空格分割组合又难以优雅分离括号。推荐解法是利用 PCRE 的控制动词 (*SKIP)(*FAIL) 实现“跳过引号内容,仅在引号外匹配分隔逻辑”:
$query = '+test +word any -sample (+toto +titi "generic test") -column:"test this" (+data id:1234)';
$tokens = preg_split(
'/".*?"(*SKIP)(*FAIL)|((|))| /',
$query,
-1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
);
// 输出: ['+test', '+word', 'any', '-sample', '(', '+toto', '+titi', '"generic test"', ')', '-column:"test this"', '(', '+data', 'id:1234', ')']
✅ 正则详解:
- ".*?"(*SKIP)(*FAIL):非贪婪匹配最短双引号字符串(如 "generic test"),随后 (*SKIP)(*FAIL) 强制引擎放弃该匹配并跳过其全部字符,不再回溯尝试其他分支;
- ((|)):捕获组匹配单个 ( 或 ),因启用 PREG_SPLIT_DELIM_CAPTURE,括号本身将作为独立 token 保留在结果中;
- ` `(空格):仅在未被引号覆盖的位置匹配空格,作为最终分隔符。
⚠️ 注意事项:
- 此方案不支持转义引号(如 "abc"def")。若需支持,应改用更健壮的引号匹配:"([^"\\]*|\\.)*"(但会显著增加复杂度);
- 输入字符串中引号必须成对且无嵌套;
- PREG_SPLIT_NO_EMPTY 防止空字符串进入结果(如连续空格);
- 若需进一步解析 column:"value" 结构,建议对每个 token 单独用 /:([^:]+):"([^"]*)"/ 或 /:([^:]+):(S+)/ 提取键值对。
该方法兼顾简洁性与可靠性,是构建搜索查询词法分析器(lexer)的理想起点——先分词,再交由语法分析器(parser)处理布尔逻辑、括号优先级和字段限定规则。











