正则表达式学习总结

Posted by Alex King on December 13, 2014

之前我对于正则表达式的了解大多数来自零零散散的碎片知识,经常是在要用到一些正则表达式的时候,忘记怎么写了,就到这个网页上查一下。上次帮dhl解决一个从HTML中抓取http://xxx.jpg格式字符串的问题时,在饱受挫折之后,我下定决心好好看一下正则表达式,乘双十一买了一本《精通正则表达式》,目前看了五章,所以有了这篇小结。

正则表达式模式

把这个部分放到第一个讲是因为很多情况下不同模式会影响匹配的结果,也是初学正则表达式容易忽略的地方。大多数引擎支持的模式和功能见下表。

模式名称 功能 模式修饰符
不区分大小写的匹配模式 不区分大小写,也包含Unicode中的字符 i
宽松排列(注释模式) 忽略字符组外部的所有空白字符 x
单行模式(点号通配模式) .匹配换行符 s
多行文本模式(增强的行锚点模式) ^ $匹配字符串内嵌的文本行开头和结束位置 m

更改正则表达式的模式通常可以通过参数的形式设置,在支持模式修饰符的引擎中,可以用(?modifier)打开某种模式,用(?-modifier)关闭模式,其中modifier对应的值为上表中的模式修饰符。模式作用的范围为一个括号内部,或者是(?modifier)(?-modifier)中间。

分组

一般用()括起来的内容,称为分组,普通分组都会被正则表达式引擎捕获,之后可以用\1 \2这样的元字符引用之前捕获的内容,还可以用于替换,称为反向引用,这个功能非常常见。捕获的顺序是按照左括号的顺序来安排的,所以有嵌套括号时,数左括号就行。另外,有许多分组结构只用于分组,不能被捕获(也就是不会产生反向引用的序号),见下表:

名称 语法 能否被捕获
命名捕获 (?<name>)
无名称分组 (?:...)
固化分组 (?>...)
环视 (?=...) (?!...) (?<=...) (?<!...)
模式修饰符 (?modifier) (?-modifier)
条件判断 (?if then \| else)

零宽断言

零宽断言(Zero-Width Assertions)并不会匹配实际的文本,而是寻找文本中的位置,因为它不会匹配文本,所以得名“零宽(或者叫零长度)” 。deerchao的《正则表达式30分钟入门教程》中的零宽断言实际上应该是指环视(lookahead),广义上说类似^ $这样的锚点也是零宽断言,因为它们也不匹配文本只匹配位置。下表是一些锚点和零宽断言的总结:

语法 功能描述
^ 匹配文本起始位置,如果启用了多行文本模式,那么也能匹配每个换行符之后的位置。
\A 无论什么模式都匹配待搜索文本的起始位置。
$ 匹配目标字符串的末尾,或者是整个字符串末尾的换行符之前的位置。如果启用多行文本模式,那么匹配字符串中任何换行符之前的位置。
\Z 相当于不启用多行文本模式下的$
\z 只匹配字符串的末尾,不考虑任何换行符。
\G 上一次匹配的结束位置(大多数引擎)、匹配的起始位置(少数引擎),这个概念有点复杂,还没有完全理解。
\b 单词分界符,匹配一个这样的位置:两边中有且仅有一边是字符、数字或下划线。
\B 非单词分界符,匹配一个这样的位置:两边要么都是字符、数字或下划线,要么都不是。
(?=...) 肯定顺序环视。断言该位置的右边匹配子表达式。
(?!...) 否定顺序环视。断言该位置的右边不能匹配子表达式。
(?<...) 肯定逆序环视。断言该位置的左边匹配子表达式。
(?<!...) 否定逆序环视。断言该位置的左边不能匹配子表达式。

量词、多选结构

最基本的量词见下表,这几乎是众所周知的。

语法 含义
* 可以重现无穷多次,也可以不出现
+ 可以重现无穷多次,但至少要出现一次
? 可以不出现,或者只出现一次
{m, n} 可以出现n至m次

这里的关键是量词也有不同的模式,一般分为以下三类,其中前两类被广泛支持,第三类则支持力度不大。

名称 语法 含义
匹配优先量词(贪婪模式) * + ? {m, n} 匹配尽可能多的内容
忽略优先量词(懒惰模式) *? +? ?? {m, n}? 匹配尽可能少的内容
占有优先量词 *+ ++ ?+ {m, n}+ 类似匹配优先量词,但一旦匹配就不会进行回溯

理解这些模式的关键在于理解正则表达式引擎匹配的过程,表达式驱动的NFA引擎会在遇到量词和多选结构(|)时,记录下当前所在的位置,然后根据不同的量词来选择接下来匹配的路径,一旦匹配失败,就会回溯到上一个记录的可选位置,选择另一条路径。对于多选结构来说,“另一条路径”就是多选结构中的下一个结构;对于匹配优先量词,一开始会选择尽可能匹配,遇到匹配失败后,“另一条路径”就是不匹配;对于忽略优先量词,一开始会选择不匹配,遇到匹配失败后,“另一条路径”就是尝试匹配;对于占有优先量词,它的行为与匹配优先量词相同,区别在于它把“另一条路径”丢掉了,所以一旦匹配失败也无法就行回溯。

对于量词的使用理论知识固然重要,但经验更重要,用的多了才会有感觉,看了书之后我觉得量词方面的坑真是多,尤其是.*这样的,要慎用。

字符表示

字符表示这块主要是Unicode的坑,相对来说汉语中的坑稍微少点,所以不从事国际编程方面的工作的话,也未必遇得到。主要问题在于有的语言中一个字符是使用一个基本字符和任意数目的组合字符拼成的,而.只会匹配一个Unicode字符。我了解的也不多,这方面就不多说了。一些常见的字符如下表:

语法 含义
\d 数字,等价于[0-9],如果支持Unicode,还会匹配Unicode数字(罗马数字这些)。
\D 非数字,等价于[^\d]
\w 一般情况下等价于[a-zA-Z0-9_]
\W 非单词字符,等价于[^\w]
\s 空白字符,等价于[ \f\n\r\t\v],支持Unicode还包含Unicode中的空白字符。
\S 非空白字符,等价于[^\s]
\p{Prop} 满足某个Unicode属性的字符。有许多属性,这里不列出了。
\P{Prop} 不满足某个Unicode属性的字符。
\xnum \x{num}
\unum \Unum
十六进制转义或Unicode转义,引擎不同则用法不同。

目前就写到这里,以后还有内容再补充。