浅析正则表达式

190 阅读10分钟

1.什么是正则表达式

在编写处理字符串的程序或者网页时,经常还会有查找某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
举个例子,这一编写一个正则表达式,用来找找所有以0开头,后面跟着2-3个数字,然后是一个连字号‘-’,最后是7或8位数组的字符串(如010-12345678或0519-13456789)

2.元字符

2.1\b(单词的开头或结尾)

假如要在一篇英文文章中查找hi,那么就可以使用正则表达式hi
这表示:由两个字符组成,前一个字符是h,后一个是i
但是,很多单词里包含了hi这两个连续的字符,比如him, history, high
只是用hi来查找的话,这些hi也会被找出来
要精确查找hi
就要使用\bhi\b
\b是正则表达式规定的一个特殊代码(可以成为元字符),代表着单词的开头或结尾,也就是单词的分界处。
虽然通常英文的单词是由空格,标点符号或者换行来分割的,但是\b并不匹配这些单词分隔符中的任何一个,它只匹配一个位置。
\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w

2.2 .(除了换行符意外的任意字符)

加入要找的是hi后面不远处跟着Lucky,就可以使用\bhi\b.*\bLucy\b
.是另一个元字符,匹配除了换行符意外的任意字符

2.3 \d(一位数字)

\d表示匹配一位数字(0或1或2或。。。)
0\d\d-\d\d\d\d\d\d\d\d
表示:以0 开头,然后使两个数组,然后是一个连字号‘-’,最后使8个数字
-不是元字符,只匹配它本身
简化后可以写成:0\d{2}-\d{8}
\d后面的{2}{8}的意思是前面\d必须连续重复匹配2次(8次)

2.4 \w(字母或数字或下划线或汉字等)

匹配字母或数字或下划线或汉字等
\ba\w*\b:匹配以a开头的字母,显示某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w*),最后是单词结束处(\b)
\b\w{6}\b:匹配刚好6个字符的单词

2.5 \s(任意的空白符)

匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等

2.6 ^ (配查找的字符串的开头,(^匹配查找的字符串的开头,匹配查找的字符串的结尾)

^匹配查找的字符串的开头,匹配查找的字符串的结尾<br/>\d5,12匹配查找的字符串的结尾<br />^\d{5,12}:匹配5-12个数字的字符串

3. (字符转义)

查找元字符本身,比如. * ,就要用\来取消这些字符的特殊意义
. :.
* :*
\ :\
deerchao.cn :deerchao.cn
C:\Windows :C:\Windows

4.重复

Windows\d+:匹配Windows后面跟着1个或更多数字
^\w+:匹配一行的第一个单词(或这个字符串的第一个单词)

4.1 *(重复0次或更多次)

是元字符,它代表的是数量,它指定前面的内容可以(0-n次)连续重复使用任意次以使整个表达式得到匹配
.表示任意数量的不包含换行的字符
\b\hi\b.
\bLucy\b表示:先是一个单词hi,然后使任意个字符(但不能使换行),最后使Lucy这个单词

4.2 +(重复一次或更多次)

4.3 ?(重复零次或一次)

4.4 {n}(重复n次)

4.5 {n,}(重复n次或更多次)

4.6 {n,m}(重复n到m次)

5.

匹配没有预定义元字符的字符合集(比如元音字母a,e,i,o,u)
[aeiou]匹配任何一个引文元音字母
[.?!]匹配标点符号
[0-9] = \d
[a-z0-9A-Z] = \w

6. |(分支条件)

分枝条件指的是有几种鬼册,如果满足其中任意一种规则都应该当成匹配,用|把不同的规则分隔开

7.测试

(?0\d{2}[) -]?\d{8}:
(?:表示转义字符(,它能出现0次或1次
0\d{2}:表示0后面有2个数字
[) -]?:表示1次或0次的 )或者-或者空格
\d{8}:表示8位数字
是像(010)88888888或022-88888888或02988888888
但是这样有bug,010)12345678这样的也是匹配的
0\d{2}-\d{8}|0\d{3}-\d{7}:匹配两种以连字号‘-’分割的数字:一组是前3位以0开头,后面8位;另一组是前4位以0开头,后面7位
(0\d{2})[- ]?\d{8}|0\d{2}[- ]?\d{8}:(以0开头3位数字),有0或1次的-或者空,8位数字 |
以0开头的3位数字,有0次或1次的-或空,8位数字
\d{5}-\d{4}|\d{5}:5位数字,-,4位数字 | 5位数字

8. ()(分组)

重复多个字符
用小括号来指定字表达式(也叫分组),然后就可以指定这个子表达式的重复次数了,也可以进行其他操作
(\d{1,3}.){3}\d{1,3}:1到3的数字加一个英文.,这个整体重复3次,1个1位到3位的数字
但是这个表达式的值也有可能是256.300.888.999这样的
如果要匹配IP地址
((2[0-4]\d|25[0-5][0-1]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

9.反义

有时需要查找不属于某个能简单定义的字符类的字符。
比如像查找除了数字以为,其他任意字符都行,就要用到反义
<a[^>]+>匹配用尖括号括起来的以a开头的字符串

9.1 \W(匹配任意不是字母,数字,下划线,汉字的字符)

9.2 \S(匹配任意不是空白符的字符)

9.3 \D(匹配任意非数字的字符)

9.4 \B(匹配不是单词开头或结束的位置)

9.5 [^x](匹配除了x意外的任意字符)

9.6 [^aeiou](匹配除了aeiou这几个字母以外的任意字符)

10.后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理
默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。
后向引用用于重复搜索前面某个分组匹配的文本。
例如,\1代表分组1匹配的文本。难以理解?请看示例:
\b(\w+)\b\s+\1\b可以用来匹配重复的单词,像go go, 或者kitty kitty。
这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)。
你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?\w+)(或者把尖括号换成'也行:(?'Word'\w+)),这样就把\w+的组名指定为Word了。
要反向引用这个分组捕获的内容,你可以使用\k,所以上一个例子也可以写成这样:\b(?\w+)\b\s+\k\b。
使用小括号的时候,还有很多特定用途的语法。下面列出了最常用的一些:

10.1捕获

10.1.1 (exp) 匹配exp,并捕获文本到自动命名的组里

10.1.2 (?exp) 匹配exp,并捕获文本到名称位name的组里,也可以写成(?'name'exp)

10.1.3 (?:exp) 匹配exp,不补货匹配的文本,也不给此分组分配组号

10.2零宽断言

10.2.1 (?=exp) 匹配exp前面的位置

10.2.2 (?<=exp) 匹配exp后面的位置

10.2.3 (?!exp) 匹配后面跟的不是exp的位置

10.2.4 (?<!exp) 匹配前面不是exp的位置

10.3注释 (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

11.零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:
断言用来声明一个应该为的事实。正则表达式中只有当断言为真时才会继续进行匹配。
(?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。
比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I'm singing while you're dancing.时,它会匹配sing和danc。
(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading。
假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=\d)\d{3})+\b,用它对1234567890进行查找时结果是234567890。
下面这个例子同时使用了这两种断言:(?<=\s)\d+(?=\s)匹配以空白符间隔的数字(再次强调,不包括这些空白符)。

12.负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词--它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样:
\b\wq[^u]\w\b匹配包含后面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b将会匹配下一个单词,于是\b\wq[^u]\w\b就能匹配整个Iraq fighting。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\wq(?!u)\w\b。
零宽度负预测先行断言(?!exp),断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。
同理,我们可以用(?一个更复杂的例子:(?<=<(\w+)>).(?=</\1>)匹配不包含属性的简单HTML标签内里的内容。(?<=<(\w+)>)指定了这样的前缀:被尖括号括起来的单词(比如可能是),然后是.(任意的字符串),最后是一个后缀(?=</\1>)。注意后缀里的/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是的话,后缀就是了。整个表达式匹配的是之间的内容(再次提醒,不包括前缀和后缀本身)。

13.注释

小括号的另一种用途是通过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249)|250-5|[01]?\d\d?(?#0-199)。
要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:
      (?<=     # 断言要匹配的文本的前缀
      <(\w+)> # 查找尖括号括起来的内容
              # (即HTML/XML标签)
      )       # 前缀结束
      .*       # 匹配任意文本
      (?=     # 断言要匹配的文本的后缀
      </\1>   # 查找尖括号括起来的内容
              # 查找尖括号括起来的内容
      )       # 后缀结束

14.贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。
有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.
?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:
a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。
为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

14.1 *? 重复任意次,但尽可能少重复

14.2 +? 重复1次或更多次,但尽可能少重复

14.3 ?? 重复0次或1次,但尽可能少重复

14.4 {n,m}? 重复n到m次,但尽可能少重复

14.5 {n,}? 重复n次以上,但尽可能少重复

15.处理选项

上面介绍了几个选项如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项:
表6.常用的处理选项

名称说明
IgnoreCase(忽略大小写)匹配时不区分大小写。
Multiline(多行模式)更改^和的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式)更改.的含义,使它与每一个字符匹配(包括换行符\n)。
IgnorePatternWhitespace(忽略空白)忽略表达式中的非转义空白并启用由#标记的注释。
ExplicitCapture(显式捕获)仅捕获已被显式命名的组。

在C#中,你可以使用Regex(String, RegexOptions)构造函数来设置正则表达式的处理选项。如:Regex regex = new Regex(@"\ba\w{6}\b", RegexOptions.IgnoreCase);
一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。事实上,为了避免混淆,在最新的 JavaScript 中,单行模式其实名叫 dotAll,意为点可以匹配所有字符,然而在指定该选项时,用的还是 Singleline 的首字母 s.

16.平衡组/递归匹配

有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构,这时简单地使用(.+)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如( 5 / ( 3 + 2 ) ) ),那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的内容呢?
这里介绍的平衡组语法是由.Net Framework支持的;其它语言/库不一定支持这种功能,或者支持此功能但需要使用不同的语法。
为了避免(和(把你的大脑彻底搞糊涂,我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把xx <aa aa> yy这样的字符串里,最长的配对的尖括号内的内容捕获出来?
这里需要用到以下的语法构造:

  • (?'group') 把捕获的内容命名为group,并压入堆栈(Stack)
  • (?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
  • (?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
  • (?!) 零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败

我们需要做的是每碰到了左括号,就在压入一个"Open",每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。
<                   #最外层的左括号
  [^<>]*             #它后面非括号的内容
  (
      (
        (?'Open'<)  #左括号,压入"Open"
        [^<>]*       #左括号后面的内容
      )+
      (
        (?'-Open'>) #右括号,弹出一个"Open"
        [^<>]*       #右括号后面的内容
      )+
  )
  (?(Open)(?!))     #最外层的右括号前检查
                    #若还有未弹出的"Open"
                    #则匹配失败
>                     #最外层的右括号
平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的

标签:<div[^>]>[^<>](((?'Open'<div[^>]>)[^<>])+((?'-Open'
)[^<>])+)*(?(Open)(?!)).

参考文章:deerchao.cn/tutorials/r…