最近做项目需要用到正则表达式,即 Regular Expression。这个以前比较熟练,但好长时间没接触了。对这种需要注意细节的技术,温习且做些笔记,对于日后再用也有很大好处。这些笔记的对象是:对正则表达式比较了解,但或有遗忘或生疏。
这里提到的Regular Expression 是基于PHP所含的PCRE实现。绝大部分通用于其他流派的正则表达式(other flavors)。有差别的地方,大家可以自己查询手册。
一、back slash(BS) '\'的问题:
问题:需要在表达式里敲几个'\'才能用来匹配对象字符串中的一个'\'?
答案:是4个。
在PHP的 "(double quotes,DQ)和 ’(single quotes,SQ)中"\" BS 都是meta sequence,一旦遇见2个都直接转换成1个。而正则表达式中对"\"也是特殊处理的,一旦看见两个BS就表示匹配对象中的一个,所以 "\\\\" 通过 PHP 就变成了 "\\",在PCRE看来就是要匹配一个"\"。
$str = "test \\test";
$pat = '\\\\';
if (preg_match("!$pat!", $str)) {
var_dump($str);
var_dump($pat);
}
二、字符组(character class)的问题:
在正则表达式中'[ ]‘可以用来把需要匹配的字符都列到一个组里。大家对此都不陌生,但是需要注意一下要点,
- 特殊字符在'[ ]' 中失去了原有特殊含义,回归最初的字符,例如:[.*+$],实际就是来匹配 '.','*','+','$'
- 在字符组中有特殊含义的 '^',‘-’,如果放置在其他位置,例如最后,即可以回归字符本身的含义。例如 '[abc^]‘ 和 '[abc-]'
- 有简化的方式来替代一些字符组,例如
- \d == [0-9],\D == [^0-9]
- \s == [ \t\r\n\f],\S == [^ \t\r\n\f]
- \w == [0-9a-zA-Z_],\W == [^0-9a-zA-Z_]
- "\" 在字符组中需要2个才能表示匹配1个,即类似 [+.\\]
三、一些特殊的字符:
这里收集和列举一些相对不常用用法或不常见的字符
- \b :出现在 '[\b]' 中表示 backspace 字符,出现在一般情况下是 word boundary 词组边界的意思
- \a :bell 字符,以前用来在 term 中发出铃声的字符,哇
- \e :escape 字符,也是一个不可打印字符
- \0 :是个零,表示 ’nul‘ 字符
- \uFFFF 或 \x{FFFF} :表示 UTF-8 字符
四、重复符号(Quantifier):
一般的重复或量化符号(repetition operators OR quantifiers)大家都比较熟悉。有“?“,"*","+","{n}","{n,}","{n,m}”。使用这些符号时,正则表达式中有所谓的懒惰方式(lazy)、贪婪方式(greedy)、拥有方式(possessive)。
- Greedy 贪婪方式:正则表达式总是缺省采用贪婪greedy的方式,尽量最大量的匹配。
$str = "aaaa";
$pat = "/[ac]+/";
if (preg_match($pat, $str, $matches)) {
var_dump($matches);
} // result is "aaaa";
- Lazy 偷懒方式:如果量化符号后面再跟个 '?'符号,则表示用偷懒的方式,即一旦匹配就算成功,不最大化匹配。常见的有 '*?','+?','{2,5}?' 等
$str = "aaaa";
$pat = "/[ac]+?/";
if (preg_match($pat, $str, $matches)) {
var_dump($matches);
} // result is "a";
- Possessive 拥有方式:这种方式牵涉到正则匹配的内部实现方式,主要用来优化匹配,减少匹配引擎尝试次数。或者说一旦有合适的最大匹配,马上进行下一步,不再回头尝试其他可能路径。在量化符号后面再跟个 '+'符号,则表示用拥有的方式,常见的有'*+','++','{2,5}+ 等。
- 贪婪方式的工作步骤如下,
- 总是最大量匹配,然后进行下一步匹配
- 一旦下一步匹配不成功,贪婪方式会回头尝试修改上一步的匹配,例如减少匹配量然后再进行下一步
- 如此往复,争取下一步匹配成功
- 因此贪婪方式会进行很多路径的匹配尝试
- 拥有方式的工作步骤如下,
- 总是最大量匹配,然后进行下一步匹配(这步与贪婪方式相同)
- 一旦下一步匹配不成功,拥有方式就不回头了,直接表示匹配失败
- 因此拥有方式不会回头尝试多个路径,即加速了匹配进程
- 贪婪方式的工作步骤如下,
$str = '"abc"';
$pat = '/"[^"]*+"/';
if (preg_match($pat, $str, $matches)) {
var_dump($matches);
} // result is '"abc"'
注意上面这个例子和greedy方式的结果相同,符合预期。下面的例子就表现出拥有方式的特殊性。
$str = '"abc"';
$pat = '/".*+"/';
if (preg_match($pat, $str, $matches)) {
var_dump($matches);
} // failed match
这个匹配失败的原因是由于'.*+’直接就匹配了剩余的 'abc"' 4个字符(最大匹配)造成匹配模式中的最后一个双引号在对象中就没有字符相匹配而失败。前面的例子里 '[^"]*+'匹配的是’abc',所以最后一个双引号就成功了。从这两个例子可以看出,采用拥有模式,需要对匹配模式(pattern)仔细研究,防止出现错误。
有人会问,既然容易出错为何要用?主要原因是优化匹配,特别是在多个套group且带有‘*’这种重复符号时,可以减少匹配路径从而实现优化。
- 贪婪和偷懒方式转换:在匹配字符串用模式符号‘U' 可以翻转贪婪或偷懒方式
// add 'U' to change default greedy to lazy
$pat = '/[ac]+/U';
// add 'U' to change lazy to greedy
$pat = '/[ac]+?/U';
- 方式转换也可以在匹配字符串中用(?U)实现。
// add (?U) to change default greedy(+) to lazy
$pat = '/(?U)[ac]+/';
// add (?U) to change lazy(+?)to greedy
$pat = '/(?U)[ac]+?/';