PHP中PRGE正则函数的学习
正则表达式的作用想必不用我多说了,大家在日常的开发中或多或少都会接触到。特别是对于一些登录(邮箱、手机号)以及网页爬虫来说,正则表达式就是神器一般的存在。在 PHP 中,有两种处理正则表达式的函数,今天我们就来学习其中的一种。
PCRE 与 POSIX
前面说到,有两种处理正则的函数库,一个是 POSIX 为主的 ereg_xxx 这种函数,不过它们已经被淘汰了,并不是很推荐使用。而另一种就是基于 PCRE 的以 preg_xxx 开头的这种函数库。今天我们主要学习的就是这类型的正则处理函数库。
POSIX 类型的正则函数库不是二进制安全的,并且对 utf8 的支持也不好,所以从 PHP5.3 开始如果使用 ereg_xxx 这类的函数就会报一个 E_DEPRECATED 错误。PCRE 的函数库对 perl 支持非常友好,同时,它也是支持 POSIX 扩展语法的正则表达式。具体的正则语法规则和模式修饰符相关的信息可以在文末的链接中查阅。关于模式修饰符的作用这里就不多说了,不清楚的小伙伴自己查找相关的资料哦。
另外,PCRE 与 POSIX 和 perl 也是有一些不同的,这些内容也都在文末的官方文档链接中可以看到。
正则匹配
好了,话不多说,我们直接进入主题,其实大部分内容相信不少同学都是接触过的,我们就来一一演示学习一下。
$str = "a@qq.com,b@sina.COM,c@yahoo.com,一堆测试数据。Test Txt.";
preg_match_all("/(.*)@(.*)\.(.*),/iU", $str, $out);
print_r($out);
// Array
// (
// [0] => Array
// (
// [0] => a@qq.com,
// [1] => b@sina.com,
// [2] => c@yahoo.com,
// )
// [1] => Array
// (
// [0] => a
// [1] => b
// [2] => c
// )
// [2] => Array
// (
// [0] => qq
// [1] => sina
// [2] => yahoo
// )
// [3] => Array
// (
// [0] => com
// [1] => com
// [2] => com
// )
// )
preg_match_all("/(.*)@(.*)\.(.*),/iU", $str, $out, PREG_SET_ORDER);
print_r($out);
// Array
// (
// [0] => Array
// (
// [0] => a@qq.com,
// [1] => a
// [2] => qq
// [3] => com
// )
// [1] => Array
// (
// [0] => b@sina.COM,
// [1] => b
// [2] => sina
// [3] => COM
// )
// [2] => Array
// (
// [0] => c@yahoo.com,
// [1] => c
// [2] => yahoo
// [3] => com
// )
// )
preg_match_all("/(.*)@(.*)\.(.*),/iU", $str, $out, PREG_OFFSET_CAPTURE);
print_r($out);
// Array
// (
// [0] => Array
// (
// [0] => Array
// (
// [0] => a@qq.com,
// [1] => 0
// )
// [1] => Array
// (
// [0] => b@sina.COM,
// [1] => 9
// )
// [2] => Array
// (
// [0] => c@yahoo.com,
// [1] => 20
// )
// )
// [1] => Array
// (
// [0] => Array
// (
// [0] => a
// [1] => 0
// )
// [1] => Array
// (
// [0] => b
// [1] => 9
// )
// [2] => Array
// (
// [0] => c
// [1] => 20
// )
// )
// [2] => Array
// (
// [0] => Array
// (
// [0] => qq
// [1] => 2
// )
// [1] => Array
// (
// [0] => sina
// [1] => 11
// )
// [2] => Array
// (
// [0] => yahoo
// [1] => 22
// )
// )
// [3] => Array
// (
// [0] => Array
// (
// [0] => com
// [1] => 5
// )
// [1] => Array
// (
// [0] => COM
// [1] => 16
// )
// [2] => Array
// (
// [0] => com
// [1] => 28
// )
// )
// )
preg_match_all() 函数用于完全的匹配,也就是文本中的内容全都匹配出来,并且将结果放到一个引用数组中。注意它最后的那个可选参数,默认情况下,数组的 0 下标是所有匹配到的字符内容,而剩下的索引内容是括号内部匹配的结果,可以对应到后面我们学习的替换函数中的 $1
、$2
这些插值中。
如果将最后一个参数设置为 PREG_SET_ORDER ,那么数据会以分组的形式展示,一级数组中就是每一个匹配到的内容,二级数组的 0 下标就是这个完全的文本内容,而后面的数据就是对应于这个完全匹配内容的括号内部匹配数据。
设置为 PREG_OFFSET_CAPTURE 的话,在格式上其实和默认情况下是一样的,只是每个数组内部又多了一个表示匹配位置的数字下标值。
说实话,这三个属性原来还真的没有了解过,很多时候需要这些功能的时候反而是自己又重新去写算法进行操作,这下也算是开了眼界。
preg_match("/(.*)@(.*)\.(.*),/iU", $str, $out);
print_r($out);
// Array
// (
// [0] => a@qq.com,
// [1] => a
// [2] => qq
// [3] => com
// )
preg_match("/(.*)@(.*)\.(.*),/iU", $str, $out, PREG_OFFSET_CAPTURE, 2);
print_r($out);
// Array
// (
// [0] => Array
// (
// [0] => qq.com,b@sina.COM,
// [1] => 2
// )
// [1] => Array
// (
// [0] => qq.com,b
// [1] => 2
// )
// [2] => Array
// (
// [0] => sina
// [1] => 11
// )
// [3] => Array
// (
// [0] => COM
// [1] => 16
// )
// )
preg_match() 函数就比较简单了,它只返回第一个与正则相匹配的数据。当然,它也有一些可选的参数。最后一个可选参数的作用就是偏移量,我们从第 2 个字符以后开始匹配,这里匹配到的数据和第一条中的就不一样了。
字符串分割
就像 explode() 和 str_split() 函数一样,正则中也有将字符串分割为数组的函数,它一般会作用于更复杂的分割条件。
print_r(preg_split("/@(.*)\.(.*),/iU", $str));
// Array
// (
// [0] => a
// [1] => b
// [2] => c
// [3] => 一堆测试数据。Test Txt.
// )
print_r(preg_split("/@(.*)\.(.*),/iU", $str, 2, PREG_SPLIT_OFFSET_CAPTURE));
// Array
// (
// [0] => Array
// (
// [0] => a
// [1] => 0
// )
// [1] => Array
// (
// [0] => b@sina.COM,c@yahoo.com,一堆测试数据。Test Txt.
// [1] => 9
// )
// )
这里我们是通过 @xxx.xxx, 来作为分隔符,所以分隔后的结果就是不包含这个分隔符的数组数据。preg_split() 这个函数的默认使用也是比较简单的,它同样有一些可选参数,比如第二条,第三个可选参数的作用是限制分割的数量,这里我们限制只分割成两个数组,所以文本后面的内容都会放到一个数组中,并且通过最后一个参数来指定返回查找到的数据的位置在字符串中的下标。
正则替换
关于替换的内容就比较多了,可以说,除了第一个我们介绍的 preg_match_all() 之外,最常用的就是 preg_replace() 这个函数了。它的作用当然也是和 str_replace() 类似的,只不过使用正则的话条件能够更丰富,也更加强大。
普通替换
echo preg_replace("/@(.*)\.(.*),/iU", '@$1.$2.cn, ',$str), PHP_EOL;
// a@qq.com.cn, b@sina.COM.cn, c@yahoo.com.cn, 一堆测试数据。Test Txt.
echo preg_replace("/[\x{4E00}-\x{9FFF}]+/u", 'Many Test Info.',$str, -1, $count), PHP_EOL;
echo $count, PHP_EOL;
// a@qq.com,b@sina.COM,c@yahoo.com,Many Test Info.。Test Txt.
// 3
echo preg_replace("/@(.*)\.(.*),/iU", '@$1.$2.cn, ',$str, 2, $count), PHP_EOL;
echo $count, PHP_EOL;
// a@qq.com.cn, b@sina.COM.cn, c@yahoo.com,一堆测试数据。Test Txt.
// 2
普通的 preg_replace() 函数也是非常简单的,它的可选参数其实和 str_replace() 也是类似的,第 4 个参数指定替换数量,比如第二条设置为 -1 也就是默认值,这样就是全部替换,而第三条就是设置为 2 ,只会替换两条匹配的内容。最后一个参数是返回匹配替换的数量,它是一个引用参数,也就是文本中我们一共替换掉了多少内容,或者说是我们匹配到了多少条信息。
另外还有一个函数和 preg_replace() 非常类似。我们直接来看它和 preg_replace() 的区别。
echo preg_filter("/@(.*)\.(.*),/iU", '@$1.$2.cn, ',$subStr), PHP_EOL;
$subject = array('1', 'a', '2', 'b', '3', 'A', 'B', '4');
$pattern = array('/\d/', '/[a-z]/', '/[1a]/');
$replace = array('A:$0', 'B:$0', 'C:$0');
echo "preg_filter 的结果:", PHP_EOL;
print_r(preg_filter($pattern, $replace, $subject));
// preg_filter 的结果:
// Array
// (
// [0] => A:C:1
// [1] => B:C:a
// [2] => A:2
// [3] => B:b
// [4] => A:3
// [7] => A:4
// )
echo "preg_replace 的结果:", PHP_EOL;
print_r(preg_replace($pattern, $replace, $subject));
// preg_replace 的结果:
// Array
// (
// [0] => A:C:1
// [1] => B:C:a
// [2] => A:2
// [3] => B:b
// [4] => A:3
// [5] => A
// [6] => B
// [7] => A:4
// )
从上面的代码中可以看出,preg_filter() 函数最后返回的结果会是匹配到结果的内容,而 preg_replace() 如果字符中没有匹配到结果,也会返回原始的内容。它们两个的参数是完全相同的。
在这段测试代码中,我们使用了数组作为替换的前三个参数,它们的匹配规则是 pattern 对应 replace 的一个一个去匹配。也就是说,0 号下标的 /\d/
对应的匹配规则是 A:$0
,如果缺少了替换或者匹配规则的话,不会报错,但替换的结果可能就不是你想要的了。
注意,只有替换类的函数是可以这样接收数组作为参数的。
回调替换
除了上面的替换之外,PRGE 的函数库中还有回调式替换的函数,也就是能让我们自定义替换之后的返回结果。
print_r(preg_replace_callback($pattern, function($matches){
print_r($matches);
return strtolower($matches[0]);
}, $subject));
// Array
// (
// [0] => 1
// )
// Array
// (
// [0] => 1
// )
// Array
// (
// [0] => a
// )
// Array
// (
// [0] => a
// )
// Array
// (
// [0] => 2
// )
// Array
// (
// [0] => b
// )
// Array
// (
// [0] => 3
// )
// Array
// (
// [0] => 4
// )
// Array
// (
// [0] => 1
// [1] => a
// [2] => 2
// [3] => b
// [4] => 3
// [5] => A
// [6] => B
// [7] => 4
// )
print_r(preg_replace_callback('/(.*)@(.*)\.(.*),/iU', function($matches){
return strtoupper($matches[0]);
}, $str));
// A@QQ.COM,B@SINA.COM,C@YAHOO.COM,一堆测试数据。Test Txt.
preg_replace_callback() 的第二个参数其实就是相当于把 preg_replace() 中的替换字符串换成一个匿名回调函数了。这个函数中的参数就是匹配到的结果,上面的测试代码中我们全部打印了出来。然后给这个函数一个 return 返回值,就是对应地去把替换的结果返回到原值中。
preg_replace_callback() 最终的返回值是根据传递给它的原始数据来确定的,如果是数组就返回数组,如果是字符串就返回的字符串。
另外还有一种更复杂的回调函数。
print_r(preg_replace_callback_array(
[
'/(.*)@(.*)\.(.*),/iU' => function ($matches) {
echo 'one:', $matches[0], PHP_EOL;
return strtoupper($matches[0]);
},
'/Test Txt./iU' => function ($matches) {
echo 'two:', $matches[0], PHP_EOL;
return strtoupper($matches[0]);
}
],
$str
));
// one:a@qq.com,
// one:b@sina.COM,
// one:c@yahoo.com,
// two:Test Txt.
// A@QQ.COM,B@SINA.COM,C@YAHOO.COM,一堆测试数据。TEST TXT.
没错,在一个函数中进行两种正则模式的匹配。是不是感觉很高大上。这个函数的使用场景就不多了,而且需要注意的是,如果第一条正则匹配到数据了,第二条正则就不会有匹配的结果了,这个大家可以自己测试一下。
匹配验证及字符串模式格式转换
匹配验证就是验证我们的正则表达式是否能匹配到的内容。
print_r(preg_grep("/\d/", [$str]));
// Array
// (
// )
print_r(preg_grep("/\d/", [$str], PREG_GREP_INVERT));
// Array
// (
// [0] => a@qq.com,b@sina.COM,c@yahoo.com,一堆测试数据。Test Txt.
// )
它只返回能够匹配到的数据,也就是第二个参数。这个参数必须是一个数组,可以验证多条数据是否能够通过这个正则匹配到内容,但不返回具体的匹配内容信息。可以用作在正式的 preg_match_all() 或者替换、分割操作之前的判断验证。它的最后一个参数如果设置为 PREG_GREP_INVERT 的话,就是反向地获取不能和正则匹配的数据。
print_r(preg_quote("(.*).(.*),"));
// \(\.\*\)\.\(\.\*\),
preg_quote() 函数其实是有点类似于 addslashes() 函数,它是针对正则中的特殊符号添加转义斜杠的。
错误信息
最后我们再看看错误信息的展示,对于正则匹配的错误,在 PHP8 之前仅有一个错误号,作用不大。
preg_match("///", $str);
print_r(preg_last_error());
// Warning: preg_match(): Delimiter must not be alphanumeric or backslash in /Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/03/source/6.PHP中PRGE正则函数的学习.php on line 332
// 1
// print_r(preg_last_error_msg()); // php8
而在 PHP8 之后,新增加了一个 preg_last_error_msg() 可以返回错误信息。不过我的电脑上还没有安装 PHP8 所以这块内容就不展示了。
总结
PHP 中正则操作的函数就这些,但正则真正的精髓其实是在于正则表达式怎么写这一块。不好的正则可能会产生严重的回溯导致性能的急剧下降,所以在做业务开发的时候,能不用正则其实还是尽量不要用的。不过相对来说,像是登录的用户验证之类的功能,正则简直不要太好用,这个就完全可以让正则好好发挥啦!另外,用好模式修饰符也是能够有效地提升正则效率的,这些都是值得我们深入去研究的东西,有兴趣的小伙伴多多阅读官方文档,一定能找到让你惊喜的地方。
测试代码:
参考文档: