0
点赞
收藏
分享

微信扫一扫

正则表达式的金牌辅助——模式修饰符

烟中雯城 2021-09-21 阅读 81

首先列出当前可用的PCRE修正符、对应的名称以及作用的简单介绍,然后再对这些模式修正符进行举例说明。

i(PCRE_CASELESS)

忽略大小写

<?php
// 要匹配开头的 This 
$str = "This is a example";

$pattern = '/^this/';

$pattern_i = '/^this/';

$res = preg_match($pattern,$str,$matches);
$res_i = preg_match($pattern_i,$str,$matches_i);
echo "res=>",$res,"\n";
echo "res_i=>",$res_i,"\n";
res=>0
res_i=>1

m (PCRE_MULTILINE)
将目标字符串按照换行符'\n'进行分行匹配(默认情况下,PCRE认为目标字符串是单行的,实际上很多情况下是包含多行的)。比如说 This is an example \n That is great!,这个字符串其实是两行,如果不指定m修正符,则PCRE在进行匹配的时候默认是按照一行匹配的。也就是说 "行首"元字符 ^ 是从整个字符串的开始位置进行匹配,而 "行末" 元字符 $ 是匹配整个字符串的末尾。 如果指定了m修正符,则字符串是按照换行符\n进行分行,^$是匹配每一行的开始和结尾位置。 所以说,如果目标字符串中没有包含换行符\n,那么设置m修正符是没任何意义的;或者是正则表达式中没有出现 ^或者$,该修正符也不产生任何影响。

<?php

$str = "<p>This is not an example</p>\n<a>That is not mine</a>";

// 不指定 m 修正符
$pattern = '/^<p>([^<]+)<\/p>$/';

// 指定 m 修正符
$pattern_m = '/^<p>([^<]+)<\/p>$/m';

$res = preg_match($pattern,$str,$matches);
$res_m = preg_match($pattern_m,$str,$matches_m);

print_r($matches);
print_r($matches_m);

上面的执行结果

// 未指定 m
Array
(
)
// 指定 m
Array
(
    [0] => <p>This is not an example</p>
    [1] => This is not an example
)

上面的正则是在行首行末匹配<p></p> ,目标字符串按照一行来匹配的话,行末是</a>。所以不能匹配。 指定m之后,目标字符串被分成多行,^$是匹配每行的开始和结尾位置,所以就可以将内容匹配出来。


s (PCRE_DOTALL)

有很多地方介绍该修饰符是将多行转换成一行。其实这种说法不太准确。确切来说,应该是如果设置了这个修饰符,模式中的点号元字符匹配所有字符,包含换行符。如果没有这个 修饰符,点号.不匹配换行符。但是对于取反字符来说,比如[^<],是可以匹配换行符的,不管是否设置了这个修饰符。

<?php

$str = "<p>This is not \n an example</p>";

// 不指定 `s`
$pattern = '/^<p>(.*)<\/p>$/';

// 指定 `s`
$pattern_s = '/^<p>(.*)<\/p>$/s';
$res = preg_match($pattern,$str,$matches);
$res_s = preg_match($pattern_s,$str,$matches_s);

print_r($matches);
print_r($matches_s);
// 不指定 s
Array
(
)

// 指定 s
Array
(
    [0] => <p>This is not 
 an example</p>
    [1] => This is not 
 an example
)

可以看出,如果不指定s 点号. 是不能匹配到换行符\n 所以对于第一个结果是匹配不到。对于取反字符就不受s修饰符的限制,即使不设置也能匹配出换行符。

<?php

$str = "<p>This is not \n an example</p>";
$pattern = '/^<p>([^<]+)<\/p>$/';
$res = preg_match($pattern,$str,$matches);
print_r($matches);
Array
(
    [0] => <p>This is not 
 an example</p>
    [1] => This is not 
 an example
)

x (PCRE_EXTENDED)

如果设置了这个修饰符,正则表达式中出现的空白的数据会被忽略。

<?php

$str = "Hello Example!";

$str_nospace = "HelloExample";

$pattern = "/Hello Example/";
$pattern_x = "/Hello Example/x";  // 空格会被忽略
$pattern_x_newline = "/Hello \n Example/x"  // 换行符也会被忽略

$res = preg_match($pattern,$str,$matches);
print_r($matches);
/*
Array
(
    [0] => Hello Example
)
*/

$res = preg_match($pattern_x,$str,$matches);
print_r($matches);
/*
Array
(
)
*/

$res = preg_match($pattern_x,$str_nospace,$matches);
print_r($matches);
/*
Array
(
    [0] => HelloExample
)
*/

$res = preg_match($pattern_x_newline,$str_nospace,$matches);
print_r($matches);
/*
Array
(
    [0] => HelloExample
)
*/

除了上面介绍的空白数据(空格,换行符等)会被忽略之外,对于正则表达式中的未转义的#和下一个换行符之间的字符也会被忽略。 这样就可以对复杂的正则表达式添加注释了。

<?php

$str = "<p>This is an example</p>";

$pattern = "/^<p> # 匹配开始位置<p>
            (.*)  # 匹配标签内容并捕获
            <\/p>$ # 匹配结尾的<\/p>
            /x";
// 使用换行符也可 $pattern = "/^<p> # 匹配开始位置<p> \n (.*)  # 匹配标签内容并捕获 \n <\/p>$ # 匹配结尾的<\/p> \n/x";  为了格式清楚,便于阅读,不推荐使用换行符的形式`\n`而把所有的都写在一行。

$res = preg_match($pattern,$str,$matches);

print_r($matches);

是能匹配到内容

Array
(
    [0] => <p>This is an example</p>
    [1] => This is an example
)

注意:这仅用于数据字符。 空白字符 还是不能在模式的特殊字符序列中出现,比如序列 (?( 引入了一个条件子组(译注: 这种语法定义的 特殊字符序列中如果出现空白字符会导致编译错误。 比如(? <name>就会导致错误)。

<?php

$str = "<p>This is an example</p>";

$pattern = "/^<p>(?<name>.*)<\/p>$/";
$pattern_space = "/^<p>(? <name>.*)<\/p>$/x";

$res = preg_match($pattern,$str,$matches);
$res = preg_match($pattern_space,$str,$matches_space);

print_r($matches);
print_r($matches_space);

上面例子我们在给子组命名,第一个?<name> 之间没有空格;第二个?<name>存在一个空格,并且设置了x。执行结果如下

// 没空格
Array
(
    [0] => <p>This is an example</p>
    [name] => This is an example
    [1] => This is an example
)

// 有空格的就会产生 Warning 错误
PHP Warning:  preg_match(): Compilation failed: unrecognized character after (? or (?- at offset 6 in ......

e (PREG_REPLACE_EVAL)

该修饰符已经被PHP7版本弃用了。如果设置了修饰符, preg_replace() 在进行了对替换字符串的 后向引用替换之后, 将替换后的字符串作为php 代码评估执行(eval 函数方式),并使用执行结果 作为实际参与替换的字符串。单引号、双引号、反斜线()和 NULL 字符在 后向引用替换时会被用反斜线转义。

<?php

$str = "<p>This is an example</p>";

$pattern = "/^<p>(.*)<\/p>$/e";

$str = preg_replace($pattern,"add_tr('\\1')",$str);

var_dump($str);

function add_tr($str)
{
    return "<tr>$str</tr>";
}

这里要使用 php5+ 版本来执行,而不能使用php7 版本。执行结果

string(27) "<tr>This is an example</tr>"
PHP Deprecated:  preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in /Users/liuhanzeng/workspace/php/reg.php on line 7

Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in /Users/liuhanzeng/workspace/php/reg.php on line 7
string(27) "<tr>This is an example</tr>"

如果不指定e$pattern = '/^<p>(.*)<\/p>$/' 那结果就成了 add_tr('This is an example')。也就是不能使用回调函数了,直接当成字符串去把目标字符串替换掉。
如果说设置了e之后是使用回调函数的话,这种说法是不正确的。而是将preg_replace函数的第二个参数使用eval()函数来执行,然后将执行后的结果替换掉目标字符串。 下面再举几个例子来说明

<?php

$str = "<p>This is an example</p>";
$pattern = "/^<p>(.*)<\/p>$/e";

$str = preg_replace($pattern,"\\1",$str);

当匹配成功以后,\\1是捕获的第一个子组的内容——This is an example。 设置了e,就会将\\1eval函数执行。所以就会报错

Fatal error: preg_replace(): Failed evaluating code:
This is an example in ...

因为This is an example 不是一段有效的PHP代码。

<?php

$str = "<p>This is an example</p>";
$pattern = "/^<p>(.*)<\/p>$/e";

$str = preg_replace($pattern,"'\\1'",$str);
var_dump($str);

正确替换了目标字符串

string(18) "This is an example"

所以说我们回过头来再看下面这个例子

<?php

$str = "<p>This is an example</p>";

$pattern = "/^<p>(.*)<\/p>$/e";

$str = preg_replace($pattern,"add_tr('\\1')",$str);

var_dump($str);

function add_tr($str)
{
    return "<tr>$str</tr>";
}

仔细看的话发现这种形式并不是严格意义上的指定一个回调函数,它就是一个对函数的调用代码。因为通常的指定回调函数是不能加小括号()的。
由于ePHP7中已经被废弃了,所以要想使用上面那种回调函数,可以用preg_replace_callback来代替。

<?php

$str = "<p>This is an example</p>";

$pattern = "/^<p>(.*)<\/p>$/";

$str = preg_replace_callback($pattern,function ($matches) {
    return add_tr($matches[1]);
},$str);

var_dump($str);

function add_tr($str)
{
    return "<tr>$str</tr>";
}

使用PHP7执行结果就正常了

string(27) "<tr>This is an example</tr>"

通过上面preg_replace_callback的回调函数,然后对比之前例子中使用e指定的回调函数,发现形式是不是不一样的。
其实,是不是可以认为e就是使用的eval的第一个字符。就是为了对preg_replace第二个参数使用eval函数执行。由此也知道,e只是在使用preg_replace函数的时候设置,其他函数和它就没有关系了。
由于eval函数是存在很大的风险的,容易造成远程执行任何代码。所以不推荐使用。 这也是为什么废弃它的原因。


A (PCRE_ANCHORED)

如果设置了这个修饰符,模式被强制为"锚定"模式,也就是说约束匹配使其仅从目标字符串的开始位置搜索。这个效果同样可以使用适当的模式构造出来,并且 这也是 perl 中实现这种模式的唯一途径。

<?php

$str = "<br><a>This is a href</a>";

// 不指定 A
$pattern = '/<a>([^<]+)<\/a>$/';

// 指定 A

$pattern_A = '/<a>([^<]+)<\/a>$/A';

$res = preg_match($pattern,$str,$matches);
$res_A = preg_match($pattern,$str,$matches_A);
print_r($matches);
print_r($matches_A);

不指定A,可以匹配到。 指定A之后,相当于是强制将 <a>从目标字符串的开始位置匹配,而目标字符串的开始位置为<br> ,所以匹配不到。

// 不指定 `A`
Array
(
    [0] => <a>This is a href</a>
    [1] => This is a href
)

// 指定 `A` 
Array
(
)

D (PCRE_DOLLAR_ENDONLY)

如果这个修饰符被设置,模式中的元字符 $ 仅仅匹配目标字符串的末尾。当字符串以一个换行符结尾时,如果这个修饰符没有设置, $不会匹配该换行符;如果这个修饰符被设置,模式中的元字符 $ 仅仅匹配目标字符串的末尾,会去匹配末尾的这个换行符\n。 如果设置了修饰符m,这个修饰符被忽略。在 perl 中没有与此修饰符等同的修饰符。其实,总结来说D是控制$是否匹配目标字符串的结尾的换行符\n的(注意,这里是整个目标字符串)。前面在介绍修饰符m的时候说过,设置了m之后,会根据\n对目标字符串进行分行,$ 是匹配每行的末尾。所以说如果设置了m, 那么D的作用就消失了。因此D不能和m同时设置。

<?php

$str = "<p>This is not \n an example</a>\n";

// 不设置 D
$pattern = '/<p>([^<]+)<\/a>$/';

$res = preg_match($pattern,$str,$matches);

print_r($matches);

在没有设置D的情况下,$不会去匹配末尾的换行符\n。 所以上面的正则匹配不到任何内容。

Array
(
    [0] => <p>This is not an example</a>
    [1] => This is not an example
)

设定 D之后,就不会忽略末尾的换行符

<?php

$str = "<p>This is not \n an example</a>\n";

// 不设置 D
$pattern = '/<p>([^<]+)<\/a>$/D';

$res = preg_match($pattern,$str,$matches);

print_r($matches);

匹配不到内容

Array
(
)

S
当一个模式需要多次使用的时候,为了得到匹配速度的提升,值得花费一些时间 对其进行一些额外的分析。如果设置了这个修饰符,这个额外的分析就会执行。当前, 这种对一个模式的分析仅仅适用于非锚定模式的匹配(即没有单独的固定开始字符)。


U (PCRE_UNGREEDY)

这个修饰符逆转了量词的“贪婪”模式。 使量词默认为“非贪婪”的,通过量词后紧跟? 的方式可以使其成为贪婪的。这和 perl 是不兼容的。 它同样可以使用 模式内修饰符设置 (?U)进行设置, 或者在量词后以问号标记其非贪婪(比如.*?)。 也就是说U的作用是,如果正则是"贪婪"模式设置U之后就变成了"非贪婪";如果正则是"非贪婪",U则使其变成"贪婪"。

<?php

$str = "<p>This is not an example</p></p>";

// 贪婪模式
$pattern_greedy = '/^<p>.*<\/p>/';

// 设置 `U` 变成了 非贪婪模式
$pattern = '/^<p>.*<\/p>/U';

$res = preg_match($pattern_greedy,$str,$matches_greedy);
$res = preg_match($pattern,$str,$matches);

print_r($matches_greedy);
print_r($matches);

上面正则如果不加U,就是“贪婪”模式,所以会匹配到字符串最后的</p>;加上U,则匹配到字符串中的第一个<\p> 就停止了。

// 未设置`U`
Array
(
    [0] => <p>This is not an example</p></p>
)

// 设置 `U`
Array
(
    [0] => <p>This is not an example</p>
)

下面我们看另一个例子,正则在不设置U情况下是“非贪婪”的,加上U变成“贪婪”。

<?php

$str = "<p>This is not an example</p></p>";

// 非贪婪模式
$pattern = '/^<p>.*?<\/p>/';

// 设置 `U` 变成了 贪婪模式
$pattern_greedy = '/^<p>.*?<\/p>/U';

$res = preg_match($pattern,$str,$matches);
$res = preg_match($pattern_greedy,$str,$matches_greedy);

print_r($matches);
print_r($matches_greedy);

结果如下

// 未设置 `U`
Array
(
    [0] => <p>This is not an example</p>
)

// 设置 `U`
Array
(
    [0] => <p>This is not an example</p></p>
)

X (PCRE_EXTRA)

这个修饰符打开了 PCRE 与 perl 不兼容的附加功能。如果设置了该修饰符,那么在正则中如果出现了反斜线后面紧跟着一个没有特殊含义的字符,比如说\T\q 等,那么程序就会报错。 默认情况下,在 perl 中,反斜线紧跟一个没有特殊含义的字符被认为是该字符的原文。 举个例子

<?php

$str = "<p>This is not an example</p></p>";

$pattern = '/^<p>\T.*<\/p>/';

$res = preg_match($pattern,$str,$matches);

print_r($matches);

在没有设置X的情况下,上面的正则是能匹配到内容的。

Array
(
    [0] => <p>This is not an example</p></p>
)

但是,如果设置了X,那就会产生错误了。

<?php

$str = "<p>This is not an example</p></p>";

$pattern = '/^<p>\T.*<\/p>/X';

$res = preg_match($pattern,$str,$matches);

print_r($matches);

执行结果

PHP Warning:  preg_match(): Compilation failed: unrecognized character follows \ at offset 5 in
...

该修饰符目前就仅此一个功能,没有其他的用途。


J (PCRE_INFO_JCHANGED)

内部选项设置(?J)修改本地的PCRE_DUPNAMES选项。允许子组重名。在中文官网中有下面一段话

(译注:只能通过内部选项设置,外部的 /J 设置会产生错误。)

我用程序验证过,外部/J设置并不会报错,也就是说J也是一个模式修饰符。

<?php

$str = "<p>This is not an example</p></p>";

$pattern = '/^<p>(?<k>This)(?<k>.*)<\/p>/';

$res = preg_match($pattern,$str,$matches);

print_r($matches);

在不加J修饰的情况下,由于子组都使用k命名,所以会报错

PHP Warning:  preg_match(): Compilation failed: two named subpatterns have the same name at offset 18 in  ......

加上J 修饰符,允许子组重名

<?php

$str = "<p>This is not an example</p></p>";

$pattern = '/^<p>(?J)(?<k>This)(?<k>.*)<\/p>/';
// 或者  $pattern = '/^<p>(?<k>This)(?<k>.*)<\/p>/J'; 两者都可以

$res = preg_match($pattern,$str,$matches);

print_r($matches);

正常执行,执行结果如下

Array
(
    [0] => <p>This is not an example</p></p>
    [k] =>  is not an example</p>
    [1] => This
    [2] =>  is not an example</p>
)

所以说 J的作用就是对子组命名的控制。


u (PCRE_UTF8)

此修正符打开一个与 perl 不兼容的附加功能。 在默认情况下正则表达式和目标字符串都被认为是 utf-8 编码的的。如果设置了该修饰符,那么它产生的效果: 无效的目标字符串会导致什么都匹配不到; 无效的模式字符串会导致 E_WARNING 级别的错误。 PHP 5.3.4 后,5字节和6字节的 UTF-8 字符序列被考虑为无效(resp. PCRE 7.3 2007-08-28)。 以前就被认为是无效的 UTF-8。
下面我们先看无效的目标字符串的情况

<?php

$str = "\xf8\xa1\xa1\xa1\xa1";

$pattern = "/.*/";
$pattern_u = '/.*/u';

$res = preg_match($pattern,$str,$matches);
$res_u = preg_match($pattern_u,$str,$matches_u);

print_r($matches);
print_r($matches_u);

$pattern 由于点号.的作用是可以匹配出内容来;但是$pattern_u由于设置了u修饰符,按照其功能,如果目标字符串是无效的,那不会匹配到任何内容。

// 未设置 `u`
Array
(
    [0] => �����  // 因为是无效的编码,所以显示的是乱码。
)

// 设置了 `u`  匹配不到内容
Array
(
)

如果正则表达式是无效的,设置u之后,就不是匹配不到内容了,而是会产生Warning警告。

<?php
$str = "Hello example!";

$pattern = "/\xf8\xa1\xa1\xa1\xa1/";
$pattern_u = "/\xf8\xa1\xa1\xa1\xa1/u";

$res_u = preg_match($pattern_u,$str,$matches_u);
$res = preg_match($pattern,$str,$matches);

print_r($matches_u);
print_r($matches);

执行结果

// 指定了 u
Warning: preg_match(): Compilation failed: invalid UTF-8 string at offset 0 in ...

// 未指定 u 则匹配不到任何内容
Array
(
)
举报

相关推荐

0 条评论