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

首先列出当前可用的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
)

鉴于 sm 修饰符都涉及到了换行符\n , 这里值得一提的是在 PHP 中的字符串如果是在单引号(' ') 中,其中的特殊符号的作用都失效,相当于普通的字符。只有在双引号中(" ")的才有效。 也就是说,目标字符串如果是单引号指定的,关于换行符的模式修饰符都不会产生影响。


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>"

注意,对于 php5 高版本的会出现提示说该修饰符已经被降级

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代码。

注意,因为我们在preg_replace中给的第二个参数是\\1,所以在eval看来This is an example 并不是一个字符串。如果我们给的第二个参数是 '\\1',那在eval看来是这样的'This is an example'。所以就不会报错。 看例子

<?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>"

注意:在function($matches){}中最后一定要有return

通过上面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
(
)

注意: 这个修饰符的作用和 ^ 很像。不同的是 ^ 是指定行首的位置,当然包括目标字符串的开始位置,它是受修饰符m和字符串中的换行符\n 的影响的。 而 A 是强制整个目标字符串的开始位置。


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
(
)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,252评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,886评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,814评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,869评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,888评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,475评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,010评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,924评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,469评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,552评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,680评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,362评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,037评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,519评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,621评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,099评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,691评论 2 361

推荐阅读更多精彩内容