在做模板解析、消息推送占位符替换或者配置文件渲染时,经常会遇到形如 @@token@@ 的动态令牌。这类双字符边界的占位符看起来简单,但实际提取时却有不少细节容易踩坑,比如正则写得贪婪导致一次匹配吞掉多个令牌、令牌内容包含特殊字符时匹配失败、空令牌该不该匹配等。本文围绕PHP的PCRE函数,完整讲解如何稳定地从字符串中提取所有 @@token@@ 形式的令牌。

一、基础写法:用 preg_match_all 提取所有令牌
最直接的做法是使用 preg_match_all 配合捕获组。双@包裹的令牌可以用正则 /@@([^@]+)@@/ 来匹配,其中 ([^@]+) 是捕获组,表示令牌内容由一个或多个非@字符组成。匹配完成后,结果会存放在第三个参数引用的数组中,$matches[1] 就是所有令牌内容的列表。
$text = '欢迎 @@username@@,您的订单 @@order_id@@ 已发货,物流单号 @@tracking_no@@';
if (preg_match_all('/@@([^@]+)@@/', $text, $matches)) {
// $matches[1] 中保存的就是去掉@@之后的内容
print_r($matches[1]);
} else {
echo '没有匹配到任何令牌';
}上面的代码输出会是 ['username', 'order_id', 'tracking_no']。这里有个关键点:PCRE默认是回溯型引擎,遇到 @@a@@ 和 @@b@@ 连续出现的情况时,[^@]+ 这种排除型字符类会在第一个 @@ 处停下,不会越过边界去吞掉后面的令牌,这正是我们想要的效果。
如果写成 /@@(.+)@@/,就会出问题。点号加贪婪量词会一路匹配到字符串中最后一个 @@,导致一次匹配吞掉中间所有内容。虽然可以用非贪婪的 /@@(.+?)@@/ 缓解,但它在某些嵌套或紧邻的场景下仍可能出现歧义,所以排除型字符类 [^@]+ 才是最稳妥的选择。
二、处理边界情况:空令牌、特殊字符与命名捕获
实际业务中令牌内容往往不只是字母数字。假设令牌允许包含点号、下划线甚至中文,比如 @@user.name@@、@@订单状态@@,前面的正则都能正常工作,因为它们不含@字符。但如果两个@@之间为空,写成 @@@@,+ 量词不会匹配,这个行为通常符合预期。如果你确实希望空令牌也算合法,把 + 改成 * 即可,但要小心 @@@@ 本身也可能是四个分隔符连排导致的误判。
为了让代码可读性更好,推荐使用命名捕获组 (?<token>...),这样取结果时不用依赖数字下标:
$pattern = '/@@(?<token>[^@]+)@@/u';
// 结尾的u修饰符按UTF-8处理,中文令牌不会被截断
preg_match_all($pattern, $text, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
echo '找到令牌: ' . $m['token'] . PHP_EOL;
}注意两个细节:一是加了 PREG_SET_ORDER 标志后,每个完整匹配会作为一个子数组,遍历时语义更清晰;二是处理中文时务必加上 u 修饰符,否则 [^@]+ 在多字节字符上按字节匹配,可能把一个汉字从中间截断,得到乱码结果。
三、提取之后:用 preg_replace_callback 完成动态替换
提取令牌通常不是最终目的,把令牌映射成动态值才是。比起先提取再循环 str_replace,preg_replace_callback 一次遍历就能完成匹配和替换,效率更高,尤其是当模板很长、令牌很多的时候。下面是一个可以直接复用的工具函数:
function renderTokens(string $template, array $data): string
{
return preg_replace_callback(
'/@@([^@]+)@@/u',
function ($m) use ($data) {
$key = $m[1];
// 令牌在数据中存在则替换,否则保留原文,方便排查
return array_key_exists($key, $data) ? (string)$data[$key] : $m[0];
},
$template
);
}
echo renderTokens(
'亲爱的@@username@@,您购买的 @@product@@ 共计 @@amount@@ 元',
['username' => '张三', 'product' => '机械键盘', 'amount' => 399]
);
// 输出:亲爱的张三,您购买的 机械键盘 共计 399 元这个实现有两个值得注意的设计:其一,未找到对应数据时保留原始令牌而不是替换成空字符串,这样调试模板时能一眼看出哪个变量缺失;其二,回调函数内用 array_key_exists 而不是 isset,因为变量值可能是 null,isset 会误判为不存在。
如果数据量非常大且同一个令牌在模板中重复出现很多次,还可以先在回调里做一层本地缓存,或者在正则中用 (?>...) 原子组减少回溯,进一步提升匹配性能。对绝大多数业务场景来说,preg_replace_callback 加排除型字符类的组合已经足够高效且不容易出错,可以作为模板占位符替换的标准方案。
PHP正则表达式 preg_match_all 动态令牌提取修改时间:2026-09-13 11:42:31