news 2026/10/3 1:34:24

PHP的preg_split函数分割字符串时保留分隔符怎么实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP的preg_split函数分割字符串时保留分隔符怎么实现

前言

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 = 'name=php;age=13'; // 逗号、分号、等号都当作分隔符,且要保留 $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 的固定语法,写起来比捕获组长,但好处是不需要额外常量,而且零宽匹配不会往结果里塞空元素。

思路 C:preg_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_types=1); 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]

常见坑点


  1. ❌preg_split('/(,)/', $s)以为加了括号就会保留 ✅ 补上PREG_SPLIT_DELIM_CAPTURE


括号只负责「捕获」,要不要把捕获结果放进返回值,由标志位决定。


  1. ❌ 模式里写非捕获组(?:,),同时加了PREG_SPLIT_DELIM_CAPTURE✅ 换成捕获组(,)


非捕获组不产生捕获结果,标志位再对也没东西可放。


  1. ❌ 以为limit = 0表示「返回 0 个元素」 ✅ 想要只切一刀就用limit = 1


-1、0、null三种值都表示不限制。


  1. ❌ 用PREG_SPLIT_OFFSET_CAPTURE拿到的偏移去调mb_substr()✅ 那是字节偏移,配substr()或先换算


中文内容上这个错误会稳定出现,且偏移看起来「差不多对」,很难一眼看穿。


  1. ❌ 直接foreach遍历preg_split()的返回值 ✅ 先判断是否=== false


正则写错或语法不支持时返回false并发出警告,对false做foreach会在 PHP 8 里直接报错。


  1. ❌ 分隔符忘了转义,写成'/./'✅ 写'/\./'


点号是通配符,结果是每个字符都成了分隔符,输出一堆空串。


  1. ❌ 多个候选分隔符各写一个捕获组,如把逗号和分号分成两个分支各带括号 ✅ 合并成一个字符类([,;])


多捕获组会产生空字符串元素,破坏「片段与分隔符交替」的规律,遍历时索引全乱。


  1. ❌ 在循环里反复对同一长字符串调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这个必须显式判断的返回值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:34:05

智慧城市APP内嵌碳普惠平台:碳积分记账与权益兑换落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:33:24

电力系统数字孪生落地实战:从SCADA到可计算孪生体

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:32:47

TJA1145A CAN收发器休眠唤醒技术详解与低功耗应用实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:32:18

FSMC驱动ILI9341 LCD的时序契约与中文显示全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:32:00

脉冲编码器专业公司选型指南:核心技术门槛与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:30:50

Storm实时处理方案架构:高吞吐低延迟场景下的确定性调度实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华