
前言explode()和preg_split()默认都会把分隔符丢掉。这在大多数场景下没问题但有几类需求偏偏要留着它给搜索结果里的关键词做高亮、写一个简单的表达式求值器需要拿到运算符、统计用哪种分隔符分隔了多少次、把切开的片段重新无损拼回去。这时候explode()完全无能为力而preg_split()可以前提是你得加上那个经常被忘记的常量。最常见的症状是这样的开发者知道「正则里加括号就是捕获」于是写了preg_split(/(,)/, $s)跑出来一看逗号还是没了。或者更曲折一点模式里写的是非捕获组(?:,)即使加了PREG_SPLIT_DELIM_CAPTURE也没有效果因为非捕获组根本不产生捕获结果。还有一种「看起来是对的但结果错位」的情况多个候选分隔符各带一个捕获组数组里突然多出几个空字符串元素遍历时索引全乱。本文讲清preg_split()保留分隔符的完整条件、三种实现思路的差异以及那些会让人查半天的细节字节偏移、limit参数取 0 的含义、正则写错时返回false而不是抛异常。一、先看签名和三个常量preg_split()的签名是preg_split(string $pattern, string $subject, int $limit -1, int $flags 0): array|false其中$flags是三个常量按位或的组合。三者都和「保不保留分隔符」这件事有关但作用完全不同。常量作用关键注意点PREG_SPLIT_NO_EMPTY丢弃切出来的空字符串元素只对切分结果生效不影响分隔符捕获PREG_SPLIT_DELIM_CAPTURE把分隔模式中捕获组匹配到的内容也作为独立元素返回必须配合捕获组单个条件满足没用PREG_SPLIT_OFFSET_CAPTURE每个元素变成[子串, 字节偏移]的两元素数组偏移是字节偏移不是字符偏移关于$limit文档里写得很清楚但很容易记反-1、0和null都表示不限制。也就是说limit 0不是「返回 0 个元素」而是「全都要」。如果想要「只切一刀」正确的值是1。二、保留分隔符的两种正则思路思路 A捕获组 PREG_SPLIT_DELIM_CAPTURE。分隔符成为独立元素元素总数是「片段数 × 2 - 1」这个规律在只有一个捕获组时成立。?php $s namephp;age13; // 逗号、分号、等号都当作分隔符且要保留 $parts preg_split(/([;])/, $s, -1, PREG_SPLIT_DELIM_CAPTURE); // [name, , php, ;, age, , 13] var_dump($parts); var_dump(implode(, $parts) $s); // true可无损还原关键点是两个条件同时成立模式里必须有捕获组$flags里必须有PREG_SPLIT_DELIM_CAPTURE。少任何一个分隔符都不出现。思路 B零宽断言让分隔符黏在相邻片段上。不用常量用前瞻或后顾把一个「空位置」变成切分点分隔符就留在前一段或后一段里。?php // 前瞻在分隔符之前切分隔符黏在右边 var_dump(preg_split(/(?,)/, a,b,c)); // [a, ,b, ,c] // 后顾在分隔符之后切分隔符黏在左边 var_dump(preg_split(/(?,)/, a,b,c)); // [a,, b,, c]这两种断言的尖括号形式属于 PCRE 的固定语法写起来比捕获组长但好处是不需要额外常量而且零宽匹配不会往结果里塞空元素。思路 Cpreg_match_all()拿全部片段和偏移。当你既要知道「切出了什么」又要知道「它在原文的哪个位置」时preg_match_all()比preg_split()更直接。方案结果结构需不需要标志位适合场景A 捕获组 DELIM_CAPTURE片段与分隔符交替出现需要高亮、还原、表达式分词B 零宽断言分隔符附着在相邻片段不需要只想让分隔符不丢不关心它独立成项Cpreg_match_all每次匹配的完整信息不需要需要偏移、需要保留原始下标三、偏移、组合标志与还原三种标志可以叠加。最常用的是「保留分隔符 记录偏移」?php $s a,b; $parts preg_split(/(,)/, $s, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); // [[a, 0], [,, 1], [b, 2]] var_dump($parts);这里的偏移是字节偏移。中文,文这种字符串里逗号的字节偏移是 6而它的字符序号是 2。拿字节偏移去调mb_substr()会直接切错位置必须用substr()它也是字节语义或者先用mb_strlen()做一次换算。这是PREG_SPLIT_OFFSET_CAPTURE最典型的翻车点。还原也很简单只要保留的分隔符是完整且按原顺序排列的implode(, $parts)就等价于原串。这也是一句很强的自检——在写分词器时先把结果拼回去和原文比一次能立刻暴露正则写漏的情况。四、完整可运行示例一个微型表达式分词器下面这个脚本需要PHP 8.0 或更高用到str_contains、构造器属性提升。它把算术表达式切成 token 序列保留运算符和括号并用「拼回去等于原文」来自检正确性。?php declare(strict_types1); final class Tokenizer { /** 捕获组里是全部需要保留的分隔符四则运算符与括号 */ private const PATTERN /([\-*\/()])/; /** * return string[] 分词结果按原顺序排列 */ public function tokenize(string $expr): array { $parts preg_split( self::PATTERN, $expr, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); if ($parts false) { throw new RuntimeException(正则表达式编译失败); } // 去掉片段两侧的空白再丢弃纯空白项 $tokens []; foreach ($parts as $part) { $part trim($part); if ($part ! ) { $tokens[] $part; } } return $tokens; } } $expr 12 3 * (num - 4) / 2; $tokenizer new Tokenizer(); $tokens $tokenizer-tokenize($expr); echo 原文: {$expr}\n; echo token 数: , count($tokens), \n; foreach ($tokens as $i $token) { printf( [%d] %s\n, $i, $token); } // 自检把 token 之间的空白按原样补回观察能否还原 $compact preg_replace(/\s/, , $expr); $rebuilt implode(, $tokens); echo \n去空白后是否与拼接结果一致: , $rebuilt $compact ? 是 : 否, \n; if ($rebuilt ! $compact) { throw new RuntimeException(分词结果无法还原: {$rebuilt}); }运行后可以看到、-、*、/、(、)全部作为独立 token 出现在数组里而不是被吞掉。这正是做表达式解析、语法高亮、模板引擎时想要的结果。再补一个「统计分隔符用量」的小例子说明捕获组方式在统计场景下也顺手?php $csv php,python;ruby,go;rust; $parts preg_split(/([,;])/, $csv, -1, PREG_SPLIT_DELIM_CAPTURE); $counts [, 0, ; 0]; foreach ($parts as $part) { if (isset($counts[$part])) { $counts[$part]; } } print_r($counts); // [, 2, ; 2]常见坑点❌preg_split(/(,)/, $s)以为加了括号就会保留 ✅ 补上PREG_SPLIT_DELIM_CAPTURE括号只负责「捕获」要不要把捕获结果放进返回值由标志位决定。❌ 模式里写非捕获组(?:,)同时加了PREG_SPLIT_DELIM_CAPTURE✅ 换成捕获组(,)非捕获组不产生捕获结果标志位再对也没东西可放。❌ 以为limit 0表示「返回 0 个元素」 ✅ 想要只切一刀就用limit 1-1、0、null三种值都表示不限制。❌ 用PREG_SPLIT_OFFSET_CAPTURE拿到的偏移去调mb_substr()✅ 那是字节偏移配substr()或先换算中文内容上这个错误会稳定出现且偏移看起来「差不多对」很难一眼看穿。❌ 直接foreach遍历preg_split()的返回值 ✅ 先判断是否 false正则写错或语法不支持时返回false并发出警告对false做foreach会在 PHP 8 里直接报错。❌ 分隔符忘了转义写成/./✅ 写/\./点号是通配符结果是每个字符都成了分隔符输出一堆空串。❌ 多个候选分隔符各写一个捕获组如把逗号和分号分成两个分支各带括号 ✅ 合并成一个字符类([,;])多捕获组会产生空字符串元素破坏「片段与分隔符交替」的规律遍历时索引全乱。❌ 在循环里反复对同一长字符串调preg_split()只为取第一段 ✅ 用explode()配limit或加PREG_SPLIT_NO_EMPTY切分产生的数组是整个字符串的副本长文本上代价不小。总结需求写法结果分隔符独立成元素捕获组 PREG_SPLIT_DELIM_CAPTURE片段与分隔符交替出现分隔符黏在左片段PREG_SPLIT配后顾断言a,/b,/c分隔符黏在右片段PREG_SPLIT配前瞻断言a/,b/,c需要位置信息叠加PREG_SPLIT_OFFSET_CAPTURE每项为子串与字节偏移丢弃空片段叠加PREG_SPLIT_NO_EMPTY空项被过滤只想切一刀limit 1后半段整体保留保留分隔符的核心就一句话捕获组负责「抓到」PREG_SPLIT_DELIM_CAPTURE负责「放进结果」两者缺一不可。理解这一点之后剩下要小心的就是偏移的字节语义、limit取值的反直觉规则以及正则编译失败时返回false这个必须显式判断的返回值。