前几天在项目中遇到一个问题:
在后台页面编辑模板消息内容,可以添加占位符,使其在代码中调用时,能够使用变量去替换占位符。
当时真是头疼了一把,一直纠结每个微信模板消息的内容都不一样,变量也不一样。调用的地方不知道要传递多少个变量;替换时,也不知道替换哪些变量。
思考了几种方式,当考虑到正则时,该问题迎刃而解,在本文后面会给出解决方法。同时,通过解决这个问题,也让自己复习了正则知识。
正则表达式简述
通常我们口中的正则是指正则表达式(regular expression)。正则表达式是一种文本模式,包括普通字符(如a-z的字母)和特殊字符(也称元字符)。
如果你看到这,开始有点迷糊,那你可以认为正则表达式就是一个字符串:
$pattern = '/^1\d{10}$/'; // 检查是否是 1 开头的 11 位手机号
其中的变量$pattern
就是一个正则表达式。其中正斜杠(/)是正则表达式的分隔符,正则表达式真正的内容是除它之外的部分。
正则表达式常用来:
检查一个字符串是否含有某种子串(查找)
将匹配的子串替换成另一个子串(替换)
从某个串中取出符合某种条件的子串(提取)
根据匹配的子串分割一个字符串(分割)
…
深入正则表达式
正则表达式组成
前文说到正则表达式的本质就是文本,而文本主要由字符构成。
元字符
在正则表达式中一些字符被赋予特殊的涵义,这类字符称为元字符。
可以在模式中方括号外使用
字符 | 含义 |
---|---|
\ | 一般用于转义字符 |
^ | 断言目标的开始位置 |
$ | 断言目标的结束位置 |
. | 匹配除换行符外的任何字符 |
[ ] | 开始和结束字符类定义 |
| | 开始一个可选分支 |
( ) | 子组的开始标记与结束标记 |
? | 量词,0 次或一次匹配。位于量词后面用于改变量词的贪婪特性。 |
* | 量词,0 次或多次匹配 |
+ | 量词,1 次或多次匹配 |
{ } | 自定义量词开始和结束标记 |
可以在模式中方括号中使用
字符 | 含义 |
---|---|
\ | 一般用于转义字符 |
^ | 仅在作为第一个字符(方括号内)时,表明字符类取反 |
- | 标记字符范围 |
转义序列(反斜线)
反斜线在我们的印象中,第一反应恐怕已经不是反斜线本身,而是它所代表的另一个意思——转义。而在正则表达式中,反斜线扮演着一个十分重要的角色。
1. 取消特殊字符所代表的特殊涵义
如果反斜线紧接着是一个非字母数字字符,表明取消该字符所代表的特殊涵义。这种将反斜线作为转义字符的用法在字符类内部和外部都可用。
比如,如果你希望匹配一个*
字符,就需要在模式中写为 \*
。
对于大多数特殊字符来说,直接在特殊字符前加上一个反斜线即可。但是对于反斜线本身来说就不是那么简单了。
PHP 中反斜线在单引号字符串和双引号字符串中都有特殊含义。
$pattern = '/\\/' // 第一步是 PHP 字符串解析 // 由于在单引号中反斜线表示转义,那么该字符串解析结果是 $pattern = '/\/' // 第二步是正则表达式引擎解析 // 由于反斜线表示转义,那么它会将分隔符 / 进行转义, 从而得到的是一个错误
综上所述,正确的写法是'\\\\'
,四个反斜线。
2. 表示非打印字符
虽然不可打印字符不少,但是常见就那么几个。
字符 | 含义 |
---|---|
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 水平制表符 |
3. 表示特定的字符类
有时候我们想匹配一个变量名字,这个时候问题来了。变量名由字母、数字、下划线组成,前两种类型的可选值众多,我们不可能把全部的字母和数字都列出来吧,我们有没有其他方法呢?字符类就是用来解决这种问题的。
字符 | 含义 |
---|---|
\d | 任意十进制数字 |
\D | 任意非十进制数字 |
\s | 任意空白字符 |
\S | 任意非空白字符 |
\w | 任意单词字符(字母、数字、下划线) |
\W | 任意非单词字符 |
4. 表示简单的断言
一个断言指定一个必须在特定位置匹配的条件, 它们不会从目标字符串中消耗任何字符。
字符 | 含义 |
---|---|
\b | 单词边界 |
\B | 非单词边界 |
模式修饰符
模式修饰符相当于一个正则表达式额外设置选项,可以帮助我们轻松处理一些特殊情况。比如当我们写了一个正则表达式来匹配一个单词,但是我们不确定所有的单词全部是小写,这个时候采用下面这种写法可以满足我们的需求:
$pattern = '/word/i';
其中的 i 就是一个模式修饰符,表示忽略大小写,匹配全部。
一些常见的模式修饰符:
字符 | 含义 |
---|---|
i | 模式中的字母会进行大小写不敏感匹配。 |
m | “行首”和”行末”会匹配目标字符串中任意换行符之前或之后,另外, 还分别匹配目标字符串的最开始和最末尾位置。 |
s | 模式中的点号元字符匹配所有字符,包含换行符。 |
u | 模式和目标字符串都被认为是 utf-8 的。 |
x | 模式中的没有经过转义的或不在字符类中的空白数据字符总会被忽略, 并且位于一个未转义的字符类外部的#字符和下一个换行符之间的字符也被忽略。 |
U | 这个修饰符逆转了量词的”贪婪”模式。 使量词默认为非贪婪的,通过量词后紧跟? 的方式可以使其成为贪婪的。这和 perl 是不兼容的。 它同样可以使用 模式内修饰符设置 (?U)进行设置, 或者在量词后以问号标记其非贪婪(比如.*?)。 |
贪婪与懒惰模式
在聊贪婪和懒惰模式之前,我们先回顾一下量词。元素的重复次数是通过量词指定的。
量词主要有以下几种:
?:匹配 0 次或 1 次,等价于 {0, 1}
+:匹配 1 次或以上,等价于 {1,}
*:匹配 0 次或以上,等价于 {0,}
{}:自定义量词
默认情况下,量词都是”贪婪”的,也就是说, 它们会在不导致模式匹配失败的前提下,尽可能多的匹配字符(直到最大允许的匹配次数)。 这种问题的典型示例就是尝试匹配C语言的注释。
出现在
/*
和*/
之间的所有内容都被认为是注释, 在注释中间, 可以允许出现单独的*
和/
。 对C注释匹配的一个尝试是使用模式/\*.*\*/
, 假设将此模式应用在字符串"/* first comment*/ not comment /*second comment*/"
它会匹配到错误的结果,也就是整个字符串, 这是因为量词的贪婪性导致的,它会尝试尽可能多的匹配字符。
然而,如果一个量词紧跟着一个?
(问号) 标记,它就会成为懒惰(非贪婪)模式, 它不再尽可能多的匹配,而是尽可能少的匹配。 因此模式/\*.*?\*/
在 C 的注释匹配上将会正确的执行。 各个量词自身的意义并不会改变,而是由于加入了?
使其首选的匹配次数发生改变。
不要将 ?
的这个用法和它作为量词的用法混淆。因为它有两种用法, 因此有时它会出现量词,比如 \d??\d
会更倾向于匹配一个数字, 但同时如果为了达到整个模式匹配的目的,它也可以接受两个数字的匹配。
后向引用
在一个字符类外面, 反斜线紧跟一个大于 0 (可能还有一位数)的数字就是一个到模式中之前出现的某个捕获组的后向引用。
如果紧跟反斜线的数字小于 10, 它总是一个后向引用, 并且如果在模式中没有这么多的捕获组会引发一个错误。
如果是第一次听到“后向引用”这个概念,那一定感到很模糊,肯定很难理解。我们先来看一个小例子,或许能帮助理解。
问题
使用正则表达式找出
I amam am a a boy boy!
中相连的单词
解答
$pattern = '/(\b[a-z]+\b)\s\1/i';
上面模式中出现的 \1
就是一个后向引用,它实际代表了前面一个子组匹配到的值。如果前面还有另外一个子组,我们可以使用 \2
来表示。
对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。
缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
PCRE 库
PCRE 库是 PHP 中一个实现了与 perl 5 在语法和语义上略有差异的正则表达式模式匹配功能的函数集。
函数 | 说明 |
---|---|
preg_filter | 执行一个正则表达式搜索和替换 |
preg_replace | 执行一个正则表达式的搜索和替换 |
preg_grep | 返回匹配模式的数组条目 |
preg_last_error | 返回最后一个PCRE正则执行产生的错误代码 |
preg_match | 执行匹配正则表达式 |
preg_match_all | 执行一个全局正则表达式匹配 |
preg_quote | 转义正则表达式字符 |
preg_replace_callback_array | 执行正则表达式搜索并使用回调进行替换 |
preg_replace_callback | 执行正则表达式搜索并使用回调进行替换 |
preg_split | 通过一个正则表达式分隔字符串 |
这么多正则函数,我们该怎么用呢?让我们换个角度来想这个问题。回到原点,正则为什么会出现,我们是用它来解决问题的,主要用来解决有关字符串的一些问题。
一般我们处理字符串时,会遇到几个基本问题,字符串查找、替换、提取以及分割。
处理数组的函数
虽然正则主要是用来处理字符串,但是PHP中也提供了一个处理数组的函数。其实,数组里的元素还是字符串。
array preg_grep ( string $pattern , array $input [, int $flags = 0 ] ) // 返回所有包含浮点数的元素 $fl_array = preg_grep("/^(\d+)?\.\d+$/", $array);
处理字符串的函数
如果查找或提取操作:
preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ) preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] )
如果替换操作:
preg_filter ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
这两个函数基本相同,只有一个区别:
preg_filter:当没有匹配值时,$subject 置为空或 null(做了一点过滤操作)
preg_replace:当没有匹配值时,$subject 值不变(没有做其他操作)
如果分割操作:
preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] )
其他处理的函数
preg_replace_callback
和 preg_replace_callback_array
这两个函数并不常用,但是它们确实很强大。在本文开始,我提到在工作中遇到了一个问题,而解决方法正是使用了 preg_replace_callback
这个函数。
preg_replace_callback ( mixed $pattern , callable $callback , mixed $subject [, int $limit = -1 [, int &$count ]] ) preg_replace_callback_array ( array $patterns_and_callbacks , mixed $subject [, int $limit = -1 [, int &$count ]] )
微信模板消息替换方法
$str = '{{name}}顾客,感谢的光临{{shop}}!'; $arg = ['name' => '小明', 'shop' => '光明店']; $pattern = '/\{\{(\w+)\}\}/'; preg_replace_callback( $pattern, function($matches) use ($arg) { $replace = ''; foreach ($matches as $index => $match) { if ($index > 0 && isset($arg[$match])) { $replace = $arg[$match]; break; } } return $replace; }, $str );
经典正则实例
中文匹配
UTF-8 汉字编码范围:0x4e00-0x9fa5
ANSI(gb2312)汉字编码范围:0xb0-0xf7,0xa1-0xfe
$str = '我是中文'; $pattern = '/[\x{4e00}-\x{9fa5}]+/u'; preg_match($pattern, $str, $match);