前言
本文并非原创,内容分别摘自维基百科、《精通正则表达式》第三版、正则表达式30分钟入门教程。
什么是正则表达式
在理论计算机科学和形式语言理论中,正则表达式是定义了一个检索模式的字符串。由字符串搜索算法在文本中检索、替换匹配正则表达式定义的字符串。[1]
一个简单的例子
假设需在一段文本中查找带有src属性的img标签,可以使用正则表达式<img\s+?src=(["'])[^\1]*?\1\s*?/?>
。
这条正则(<small>为了表述的流畅性,下文将“正则表达式”简称为“正则”</small>)描述了这样一个检索模式:以<img
开头,以>
结尾,<img
与src=
间有若干(大于0)空白符,src=
与>
间同样有若干(大于等于0)空白符,且>
前可能存在一斜杠/
,同时src=
后紧跟一对引号(可能是单引号,也可能是双引号),且这对引号之间有若干(大于等于0)个外括引号以外的任意字符。
元字符
一条正则通常由两种字符构成:元字符及普通字符。通常可以将元字符看做普通语言中的语法,它为正则表达式提供了强大的描述能力。
行锚点
^
代表行的开始,它将匹配文本锚定在行首位置,即^cat
只匹配行首的cat。
$
代表行的结束,它将匹配文本锚定在行尾位置,即cat$
只匹配位于行尾的cat,如以scat结尾的行。
^
和$
都只匹配位置,而不是具体的文本。在某些不支持处理多行的场景里,^
和$
的意义则变为匹配字符串的开始和结束。
单词分界符
\b
代表单词的开头或者结尾,即单词的分界处,可以将它看作单词版本的行锚点,如\bcat\b
表示“匹配cat这个单词”。同理,它只匹配位置。
字符组
字符组,即结构体[···]
,允许使用者列出在某处期望匹配的字符。如gr[ea]y
的意思是:先找到g,然后是一个r,接着是e或者a,最后是一个y。字符组的内容是在同一个位置能够匹配若干字符,它的意思是“或”(前例中的gr[ea]y
在效果层面等同于gr(e|a)y
);而在字符组之外的普通字符必须顺序匹配,有“接下来是”的意思。
在字符组内部,-
可以表示为一个范围,当且仅当此时,-
才是元字符,若它出现在字符组的首尾位置,则仅表示为一个普通字符。同理,?
和.
在字符组外常被当做元字符,但是在字符组内则不是如此。
排除型字符组
排除型字符组,即[^···]
,允许使用者列出在某处不希望匹配的字符。换言之,这个字符组会匹配任何未列出的字符。这个字符组开头的^
表示“排除”,与在字符组外^
表示行锚点的意义截然不同。
多选结构
|
,相当于逻辑运算中的或运算,把不同的子表达式组合成一个总表达式,这个总表达式能够匹配任意的子表达式。在这样的组合中,子表达式称为“多选分支”。
gr[ea]y
和gr(e|a)y
虽然在效果上表现相同,但是字符组和多选结构在本质上是两个概念。字符组只能匹配目标文本中的单个字符,而每个多选结构自身都可能是一条完整的正则表达式。
分组与反向引用
圆括号将限定的若干字符组合成一个子表达式,作为一个分组。默认情况下,分组会“记住”子表达式匹配的文本供表达式或其他过程使用,又称为捕获。每个分组都有一个组号,组号由左往右从1开始分配。
反向引用则用于重复检索之前某个分组匹配的文本,如\1
代表分组1匹配的文本。
若仅希望子表达式进行匹配,而不捕获匹配的文本,也不给此分组分配组号,则可以在圆括号内的子表达式前加上?:
,如:(?:expression)
。
转义
若想匹配元字符本身,如.
或*
,则需要通过\\
对字符进行转义,取消这些字符在正则中的特殊意义。
常用元字符
代码 | 说明 | |
---|---|---|
^ | 匹配行或字符串的开始 | |
$ | 匹配行或字符串的结束 | |
. | 匹配除换行符以外的任意字符 | |
[···] | 匹配列出的任意字符 | |
[^···] | 匹配未列出的任意字符 | |
匹配分隔两边的任意表达式 | ||
(···) | 限定多选结构范围,标注量词作用的元素,为反向引用“捕获”文本 | |
\1,\2,... | 匹配之前的第一、第二等分组内匹配的文本 | |
\b | 匹配单词的开始或结束 | |
\w | 匹配字母、数字、下划线、汉字 | |
\s | 匹配任意的空白符 | |
\d | 匹配数字 | |
\W | 匹配任意非字母、数字、下划线、汉字的字符 | |
\S | 匹配任意非空白符的字符 | |
\D | 匹配非数字的字符 | |
\B | 匹配非单词开始或结束的位置 |
重复限定符
代码 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{min,} | 重复min次或更多次 |
{min,max} | 重复min到max次 |
?
代表可选项,将它加在某个字符后面,表示此处容许出现该字符,但它的出现并非匹配成功的必要条件。u?
是必然能匹配成功的,有时它会匹配一个u,其他时候则不匹配任何字符,例如u?
在semicolon中匹配成功10处,但什么字符都没有匹配。
贪婪与懒惰
当正则中包含重复限定符时,默认匹配模式是贪婪模式,即尽可能多的字符。若需要匹配尽可能少的字符时,只需在重复限定符后加上?
,即可将贪婪模式转换成懒惰模式。如用表达式a.*b
(贪婪)检索aabab时,会匹配整个字符串,而用表达式a.*?b
(懒惰)检索时,会匹配aab(前三个字符)和ab(后两个字符)。
代码 | 说明 |
---|---|
*? | 重复零次或更多次,但尽可能少重复 |
+? | 重复一次或更多次,但尽可能少重复 |
?? | 重复零次或一次,但尽可能少重复 |
{min,}? | 重复min次或更多次,但尽可能少重复 |
{min,max}? | 重复min到max次,但尽可能少重复 |
零宽断言
零宽断言是指匹配宽度为零,满足一定的断言。断言用来声明一个应该为真的事实,正则表达式中只有断言为真时才会继续匹配。而零宽断言则用于检索在某些内容(但并不包含这些内容)之前或之后的东西。像\b
、^
、$
一样,零宽断言用于指定一个位置,这个位置应该满足一定的条件(即断言)。
零宽正预测先行断言
零宽正预测先行断言,(?=expression)
,断言自身出现位置的后面能匹配表达式expression。如\b\w+(?=ing\b)
可以匹配以ing结尾的单词的前面部分(除了ing以外的部分),在运用这条正则检索I'm singing while you're dancing.时,它会匹配sing和danc。
零宽正回顾后发断言
零宽正回顾后发断言,(?<=expression)
,断言自身出现位置的前面能匹配表达式expression。如(?<=\bre)\w+\b
可以匹配以re开头的后半部分(除了re以外的部分),在运用这条正则检索reading a book.时,它会匹配ading。
零宽负预测先行断言
前文提到可以通过排除性字符组列出在某处不希望匹配的字符。若只想确保某个字符没有出现,而不想匹配它,则可以使用负向零宽断言解决这样的问题。比如,在需要查找这样的单词:它里面出现了q,但是q后面不能是u。用表达式\b\w*q[^u]\w*\b
检索Iraq fighting时,由于Iraq以q结尾的单词,而[^u]
总要匹配一个字符,所以当q作为单子的最后一个字符时,[^u]
将会匹配q后面的单词分隔符,后面的\w*\b
会匹配下一单词,所以这条正则会匹配整个字符串。正确的表达式为:\b\w*q(?!u)\w*\b
。
零宽负预测先行断言,(?!expression)
,断言自身出现位置的后面不能匹配表达式expression。如\b((?!abc)\w)+\b
匹配不包含连续字符串abc的单词。
零宽负回顾后发断言
零宽负回顾后发断言,(?<!expression)
,断言自身出现位置的前面不能匹配表达式expression。如(?<![a-z])\d{7}
匹配前面不是小写字母的7位数字。
字符串搜索算法通常从左往右开始检索文本,当前断言位置的左边,即前文所述的“断言自身出现位置的前面”,而当前断言位置的右边,即断言的前行方向。
例1:(?<=\d)(?=(?:\d{3})+(?!\d))
匹配左边有数字且右边有3x个数字的位置;
例2:(?<=<(\w+)>)[\s\S]*?(?=<\/\1>)
匹配不包含属性的简单HTML标签里的内容。
注释
圆括号的另一种用途是通过语法(?#comment)
来包含注释,如2[0-4]\d(?#200-249)
。但并不是所有流派支持这种功能。
平衡组
当需要匹配像<1 / <1 + 100> >(为方便描述,将算式中的圆括号用尖括号代替)这样的可嵌套的层次性结构时,无论是使用<.+>
,还是<.+?>
都不能保证匹配到的内容中的尖括号是逐层配对的。
这里需要用到平衡组,语法构造如下:
-
(?'group'expression)
把捕获的内容命名为group,并压入堆栈。 -
(?'-group'expression)
从堆栈弹出名为group的捕获内容,若堆栈为空,则本分组匹配失败。 -
(?(group)yes|no)
若堆栈上存在名为group的捕获内容的话,则继续匹配yes部分的表达式,否则继续匹配no部分的表达式。 -
(?!)
由于该零宽负向先行断言没有后缀表达式,试图匹配总是失败。
每碰到一个左括号,就往堆栈内压入一个“Open”,每碰到一个右括号,就弹出一个“Open”,最后检查堆栈是否为空,不为空则证明左括号多于右括号,匹配失败。正则表达式引擎进行回溯(放弃最前面或最后面的一些字符),使整个表达式得到匹配。完整表达式如下:
< #最外层的左括号
[^<>]* #最外层的左括号后面的不是括号的内容
(
(
(?'Open'<) #碰到了左括号,在黑板上写一个"Open"
[^<>]* #匹配左括号后面的不是括号的内容
)+
(
(?'-Open'>) #碰到了右括号,擦掉一个"Open"
[^<>]* #匹配右括号后面不是括号的内容
)+
)*
(?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败
>
平衡组的另一个常见应用就是匹配HTML,如匹配嵌套的<div>标签。但是大多数系统(除Perl.NET\PCRE/PHP)中,正则表达式无法匹配任意深度的嵌套结构。
网友评论