粗通正则表达式

前言

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

什么是正则表达式

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

一个简单的例子

假设需在一段文本中查找带有src属性的img标签,可以使用正则表达式<img\s+?src=(["'])[^\1]*?\1\s*?/?>
这条正则(<small>为了表述的流畅性,下文将“正则表达式”简称为“正则”</small>)描述了这样一个检索模式:以<img开头,以>结尾,<imgsrc=间有若干(大于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),断言自身出现位置的前面不能匹配表达式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)中,正则表达式无法匹配任意深度的嵌套结构。

补充

正则表达式在线测试工具

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

推荐阅读更多精彩内容