粗通正则表达式

前言

本文并非原创,内容分别摘自维基百科、《精通正则表达式》第三版、正则表达式30分钟入门教程。

什么是正则表达式

在理论计算机科学和形式语言理论中,正则表达式是定义了一个检索模式的字符串。由字符串搜索算法在文本中检索、替换匹配正则表达式定义的字符串。[1]

一个简单的例子

假设需在一段文本中查找带有src属性的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]ygr(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.时,它会匹配singdanc

零宽正回顾后发断言

零宽正回顾后发断言,(?<=expression),断言自身出现位置的前面能匹配表达式expression。如(?<=\bre)\w+\b可以匹配以re开头的后半部分(除了re以外的部分),在运用这条正则检索reading a book.时,它会匹配ading

零宽负预测先行断言

前文提到可以通过排除性字符组列出在某处不希望匹配的字符。若只想确保某个字符没有出现,而不想匹配它,则可以使用负向零宽断言解决这样的问题。比如,在需要查找这样的单词:它里面出现了q,但是q后面不能是u。用表达式\b\w*q[^u]\w*\b检索Iraq fighting时,由于Iraqq结尾的单词,而[^u]总要匹配一个字符,所以当q作为单子的最后一个字符时,[^u]将会匹配q后面的单词分隔符,后面的\w*\b会匹配下一单词,所以这条正则会匹配整个字符串。正确的表达式为:\b\w*q(?!u)\w*\b
零宽负预测先行断言,(?!expression),断言自身出现位置的后面不能匹配表达式expression。如\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

零宽负回顾后发断言

零宽负回顾后发断言,(?,断言自身出现位置的前面不能匹配表达式expression。如(?匹配前面不是小写字母的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,如匹配嵌套的

标签。但是大多数系统(除Perl.NET\PCRE/PHP)中,正则表达式无法匹配任意深度的嵌套结构。

补充

正则表达式在线测试工具

你可能感兴趣的:(粗通正则表达式)