正则表达式详解

  正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以从某个字符串中取出符合某些条件的子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。正则中比较难懂的我觉得是分组,说白了就是小括号的使用,常用的分组有捕获型分组和非捕获型分组。另外就是 JS 和 Python3 中正则的使用也略有不同。

正则表达式

  正则表达式是由普通字符和特殊字符(称为”元字符”)组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

普通字符

  普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

非打印字符

符号 说明
\cx 匹配由 x 指明的控制字符。例如 \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 “c” 字符。
\f 匹配一个换页符。等价于 \x0c\cL
\n 匹配一个换行符。等价于 \x0a\cJ
\r 匹配一个回车符。等价于 \x0d\cM
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
\t 匹配一个制表符。等价于 \x09\cI
\v 匹配一个垂直制表符。等价于 \x0b\cK

特殊字符

  所谓特殊字符,其实就是正则表达式中具有特殊意义的专用字符。特殊符号的匹配需要加反斜杠 “\” 转义,但是大部分特殊字符如果放到中括号中,就不需要转义了,这涉及到了括号的使用,稍后介绍。

符号 说明
$ 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。
( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。
* 匹配前面的子表达式零次或多次。
+ 匹配前面的子表达式一次或多次。
. 匹配除换行符 \n 之外的任何单字符。
[ 标记一个中括号表达式的开始。
? 匹配前面的子表达式零次或一次,如果紧跟在任何量词 * + ? {} 的后面,将会使量词变为非贪婪的(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。
^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。
{ 标记限定符表达式的开始。
  1. 匹配以上字符需要进行转义,如:匹配 “?” 需要使用 \? ,要注意右中括号 ] 和右花括号 } 不需要转义

  2. 短横线 - 只有在 中括号内不处于首末位置时 ,才是连字符,其他时候只代表一个普通字符 “-“,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # js中正则语法: /正则表达式主体/修饰符(可选)
    var reg1 = /1-5/ //严格匹配字符串"1-5",而不是数字1,2,3,4,5
    var reg2 = /(1-5)/ //严格匹配字符串"1-5",而不是数字1,2,3,4,5
    reg1.test('23') // -> false,需要严格匹配字符串"1-5"
    reg.test('-') // -> false,需要严格匹配字符串"1-5"

    var reg1 = /[-123]/ //等同于 /(-|1|2|3)/
    var reg2 = /[1-3]/ //等同于 /(|1|2|3)/,这里的 - 是连字符
    var reg3 = /[13-58]/ //1或者3,4,5或者8
    reg1.test('-') // -> true,中括号不需严格匹配,包含"-"即可
    reg2.test('-') // -> false,reg2想要匹配的是1或2或3
  3. 特殊符号 $.*+?{}()| 在中括号里是不需要转义的,比如匹配字符 “?” 或 “+” 可以用 [?+] ,效果等同 \?\+,但是如下几个特殊符号在中括号里必须转义:

    1. 反斜线 \
    2. 中括号 [],右中括号虽然不属于特殊符号,但在中括号内会引起闭合上的歧义,所以需要转义。
    3. 处于首位的 ^ 和 处于非首末位的 - ,因为 [^1-3] 是匹配以非 1,2,3开头的字符,而 [\^1\-3] 是匹配字符 ^、数字 1、字符 -、和数字 3 。

额外说一下比较难理解的 \b
  \b 匹配一个单词边界,不匹配任何字符,也就是指单词和空格间的位置。例如, er\b 可以匹配 “never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’,因为 “never” 中开头的 ‘n’ 之前以及末尾的 ‘r’ 之后都相当于有个分隔符,\b 就是匹配这个分隔符,但是这个单次中间是不存在任何分隔符的。

  举例1:对于字符串 “I am abc” \sam\s 匹配到的是 “ am “ ,也能匹配到空格,但 \bam\b 匹配到的是 “am”,\b 只是匹配字符串开头结尾及空格回车等的位置, 不会匹配空格符本身。

  举例2:表达式 .\b. 在匹配 “###abc” 时,匹配到的内容是 “#a”,不匹配任何字符,”#” 和 “a” 对应的是两个 .,这两个字符之间的间隔就是 \b,所以结果只有两个字符。\b^$ 类似,本身不匹配任何字符。

  举例3:表达式 \bend\b 在匹配 “weekend,endfor,end” 时,匹配结果是 “end”,只有最后一个 “end” 是完整的单次,前后存在分隔符。

匹配单个任意字符

  以下特殊字符可以实现从 一个集合 中选出 一个字符 的功能,可能会匹配多次,但结果只能是一个字符。

  1. . 匹配 任意一个 除换行符(\n)以外的字符。

  2. \w 匹配 任意一个 字母、数字或下划线,等价于 [^A-Za-z0-9_],能否匹配汉字取决于操作系统和应用环境,这个不绝对。

  3. \s 匹配 任意一个 任意的空白符,包括空格、制表符、换页符等。

  4. \d 匹配 任意一个 0-9 的数字

  例如表达式 a\d. 结果只能是三个字符,在匹配 “aa2a3aaa45” 时,匹配到的内容是 “a2a” 和 “a45”,其实 “a3a” 也符合,但是已匹配成功的字符不会被拿来继续匹配,如果未匹配成功的话可能出现回溯的情况,一个字符可能会被拿来进行多次匹配。

限定词

  上面提到的特殊符号都只能匹配一个字符,比如 \w 意味着匹配结果只有一个字符,\w\w结果是两个字符,用下面的表达式可以实现对前一个表达式的重复匹配。

  1. {n} 表达式重复 n 次,比如:\w{2} 相当于 \w\w

2.{m,n} 表达式至少重复 m 次,最多重复 n 次,比如:ba{1,3} 可以匹配 “ba” 或 “baa” 或 “baaa” 。

  1. {m,} 表达式至少重复 m 次,比如:\w\d{2,} 可以匹配 “a12”,”_456”,”M12344” 。

  2. ? 代表前面的字符必须出现 0 次或者 1 次,相当于 {0,1},比如:a[cd]? 可以匹配 “a”,”ac”,”ad”,因为中括号是匹配单个字符,是 “c” 或者 “d” 而不是 “cd”。

  3. + 代表前面的字符必须至少出现 1 次,相当于 {1,},a+b 可以匹配 “ab”,”aab”,”aaab” 。

  4. * 代表前面的字符不出现或出现任意次,相当于 {0,},\^*b 可以匹配 “b”,”^^^b” 。

  例如表达式 \d+\.?\d* 匹配结果的个数是不确定的,在匹配 It costs $12.5 or $125” 时,匹配到的内容是 “12.5” 和 “125”。

贪婪和非贪婪

  在使用限定符时,有几种表示方法可以使同一个表达式能够匹配不同的次数,比如:{m,n}{m,}?*+,具体匹配的次数随被匹配的字符串而定。这种重复匹配不定次数的表达式在匹配过程中,总是 尽可能多 的匹配,这种匹配原则就叫作 贪婪 模式。比如,针对文本 “dxxxdxxxd” :

  1. (d)(\w+)\w+ 将匹配第一个 “d” 之后的所有字符 “xxxdxxxd”
  2. (d)(\w+)(d)\w+ 将匹配第一个 “d” 和最后一个 “d” 之间的所有字符 “xxxdxxx”。

  在修饰匹配次数的特殊符号后再加上一个 ? 号,则可以使匹配次数不定的表达式 尽可能少 的匹配,使可匹配可不匹配的表达式,尽可能的 “不匹配”。这种匹配原则叫作 “非贪婪 模式,也叫作 “勉强”模式。如果少匹配就会导致整个表达式匹配失败的时候,与贪婪模式类似,非贪婪模式会最小限度的再匹配一些,以使整个表达式匹配成功。例如针对文本 “dxxxdxxxd” :

  1. (d)(\w+?)\w+? 将尽可能少的匹配第一个 “d” 之后的字符,结果是:”\w+?” 只匹配了一个 “x”
  2. (d)(\w+?)(d) ,为了让整个表达式匹配成功,\w+? 不得不匹配 “xxx” 才可以让后边的 “d” 匹配,从而使整个表达式匹配成功。因此匹配到的结果是 “xxx” 。

括号

  小括号 () 的作用是实现分组和分支结构(或者叫多选结构),分组的类型很多,单独小括号属于捕获型分组,当有捕获分组时候,在匹配成功的一串字符串中,捕获组的内容会被自动提取并保存。

1
2
3
4
5
6
string = "2017-06-12"
print(re.findall(r'\d{4}-\d{2}-\d{2}', string))
['2017-06-12']

print(re.findall(r'(\d{4})-(\d{2})-(\d{2})', string))
[('2017', '06', '12')]

  大括号 {} 的作用是表示匹配的长度,大括号 {}?+* 其实都属于限定符,如果使用表达式再加上限定符,那么不用重复书写表达式就可以重复匹配,比如 [bcd][bcd] 可以写成 [bcd]{2}

  中括号 [] 用于指定匹配的字符范围,使用方括号 [ ] 包含一系列字符,能够匹配其中 任意一个 字符。用 [^ ] 包含一系列字符,则能够匹配 其中字符之外任意一个 字符。虽然可以匹配其中任意一个,但是只能是一个,不是多个。

  1. [ab5@] 匹配 “a” 或 “b” 或 “5” 或 “@” 。
  2. [^abc] 匹配 “a”,”b”,”c” 之外的任意一个字符。
  3. [f-k] 匹配 “f”~”k” 之间的任意一个字母。
  4. [^A-F0-3] 匹配 “A”~”F”,”0”~”3” 之外的任意一个字符。

  表达式 [bcd][bcd] 匹配 “abc123” 时,匹配的结果是 “bc”,各匹配一个字符。
  表达式 [^abc] 匹配 “abc123” 时,匹配的结果是 “1”,只有一个字符。
  表达式 [^abc]+ 匹配 “abc123” 时,匹配的结果是 “123”,+ 意思是前面的字符出现至少一次,所以这个表达式也等同于 [^abc]{1,}
  表达式 ^abc 匹配 “abc123” 时,匹配的结果是 “abc”,这里的 ^ 意思是以 “abc” 开头的字符。
  表达式 ^abc(\d+) 匹配 “abc123” 时,匹配的结果是 “123”,意思是以 “abc” 开头,分组内是若干数字,小括号有分组的功能,是为了提取匹配的字符串,所以只会匹配到小括号里的内容。

注意:
中括号里的 $.*+?{}()| 都是字符本身,没有特殊含义,比如中括号内嵌套了小括号,仅仅是指匹配小括号这个字符,中括号内的特殊字符是 \[] 以及处于 首位^ 和 处于 非首末位-

分组

捕获型分组 - ()
  一对圆括号括起来的表达式是捕获组,格式 (Pattern) ,与小括号内的表达式匹配的内容都会被保存起来,后续可以执行各种操作。 我们知道 /a+/ 匹配连续出现的 “a”,而要匹配连续出现的 “ab” 时,需要使用 /(ab)+/,其中括号是提供分组功能,使量词 + 作用于 “ab” 这个整体,Python3 代码如下:

1
2
3
4
5
6
7
str1 = "123@qq.comaaa@163.combbb@126.comasdf111@asdfcom"
print(re.findall(r'\w+@(qq|163|126).com',str1))
['qq', '163', '126']

str2 = "10010.86$10000.86¥"
print(re.findall(r'(\d+)(\.?)(\d+)([$¥])', str2))
[('10010', '.', '86', '$'), ('10000', '.', '86', '¥')]

  四个分组内的数据会被提取出来。分组序号看左小括号就行了左括号考前的,序号就考前,JS 中可以用 \1$1 取对应分组:

1
2
3
4
5
6
var reg = /(A+)((B|C|D)+)(E+)/gi;//该正则表达式有4个分组
//对应关系
//RegExp.$1 <-> (A+)
//RegExp.$2 <-> ((B|C|D)+)
//RegExp.$3 <-> (B|C|D)
//RegExp.$4 <-> (E+)

  限定词不会增加分组,比如 (ab){2} 只有一个分组,而 (ab)(ab) 是两个分组,但是他们在匹配上意思是一样的。

非捕获性分组 - (?:)
  小括号内以 ?: 开头的是非捕获组,格式 (?:Pattern),非捕获组仅参与匹配,不提取内容,既起到了分组的作用,又不会破坏正则表达式的整体性:

1
2
3
4
5
6
7
str3 = "123@qq.comaaa@163.combbb@126.comasdf111@asdfcom"
print(re.findall(r'\w+@(?:qq|163|126).com',str3))
['123@qq.com', 'aaa@163.com', 'bbb@126.com']

str4 = "10010.86$10000.86¥"
print(re.findall(r'(\d+)(?:\.?)(?:\d+)([¥$])$', str4))
[('10000', '¥')]

  非捕获分组内的数据不会被提取,针对 str3 的匹配,只有一个分组还是非捕获分组,相当于一个捕获分组都没,输出的就是匹配到的全部字符串,和普通匹配没什么区别。

正向前瞻型 - (?=)
  正向前瞻,格式 (?=Pattern),表示 后面要有什么 。前瞻分组会作为匹配校验,但不出现在匹配结果字符里面,而且不作为子匹配返回。

1
2
3
str5 = "123aa4aa56aaf"
print(re.findall(r'[0-9a-z]{2}(?=aa)', str5))
['23', 'a4', '56']

  需要满足:两位字符(数字,或字母),且后面紧跟着两个 “a”,首先匹配到的是 “23aa” ,因为 (?=) 的部分不捕获,所以输出的不包括 “aa”,结果是 “23”。注意,继续查找的时候,并非是从下标 [5](即字符 “4”)开始,而是从下表 [3](即第一个字符 “a”)开始的,下一次匹配到了 “a4aa”,的、所以输出 “a4”,以此类推。

  在看一个例子,表达式 (\w)((?=\1\1\1)(\1))+ 在匹配字符串 “aaa ffffff 999999999” 时,将可以匹配6个 “f” 的前 4 个,因为最后一个 (\1) 可以重复使用 (?=\1\1\1) 匹配过的字符,匹配 9 个 “9” 的前 7 个。这个表达式可以读解成:重复 4 次以上的字母数字,则匹配其剩下最后 2 位之前的部分。当然,这个表达式可以不这样写,在此的目的是作为演示之用。

非捕获型分组和前瞻型分组区别:

1
2
3
4
5
6
str6 = "kid is a doubi"
print(re.findall(r'(kid is a (?=doubi))', str6))
['kid is a ']

print(re.findall(r'(kid is a (?:doubi))', str6))
['kid is a doubi']

  可见,非捕获型分组匹配到的串,仍会被外层的捕获型分组捕获到,但前瞻型却不会,当你需要参考后面的值,又不想连它一起捕获时,前瞻型分组就派上用场了。

反向前瞻型 - (?!)
反向前瞻,格式 (?!Pattern),与正向前瞻正好相反,表示 后面不能有什么
  

正向后顾 - (?<=)(JavaScript 不支持)
正向后顾,格式 (?<=Pattern),表示 前面要有什么
  用法和正向前瞻类似:

1
2
3
str7 = "123aa4aa56aaf"
print(re.findall(r'(?<=aa)[0-9a-z]{2}', str7))
['4a', '56']

反向后顾 - (?<!)(JavaScript 不支持)
反向后顾,格式 (?<!Pattern),与正向后顾正好相反,表示 前面不能有什么
  

分支结构
  其实就是实现 or 的功能,比如 (p1|p2) 就是匹配表达式 p1 或者 p2。要匹配如下的字符串:

I love JavaScript
I love Regular Expression

js中正则代码如下:

1
2
3
var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") ); // true
console.log( regex.test("I love Regular Expression") ); // true

  但是如果去掉正则中的括号,即 /^I love JavaScript|Regular Expression$/,匹配字符串则成了 “I love JavaScript” 和 “Regular Expression”。

  小括号能实现分组的功能,捕获组可以通过 反向引用 对分组内容进行操作,也可以在匹配操作完成后从匹配器检索,下一章价绍。

反向引用

  反向引用引用的仅仅是 文本内容 ,而不是正则表达式!

  表达式在匹配时,表达式引擎会将小括号 ( ) 包含的表达式所匹配到的字符串记录下来。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。当用某种边界来查找,而所要获取的内容又不包含边界时,必须使用小括号来指定所要的范围。

  \1 引用第 1 对括号内匹配到的字符串,\2 引用第 2 对括号内匹配到的字符串……以此类推,如果一对括号内包含另一对括号,则外层的括号先排序号。换句话说,哪一对的左括号 “(“ 在前,那这一对就先排序号。

1
2
3
4
5
6
str8 = "aaa bbbb ccccc abcdef ddddddd 11112111"
print(re.findall(r'(\w)\1{4,}', str3))
['c', 'd']

print(re.findall(r'(\w){5,}', str3))
['c', 'f', 'd', '1']

  这个表达式要求 \w 范围的字符至少 重复 5 次,注意与 \w{5,} 之间的区别,\w{5,} 只要是任意 5 位以上就符合,不需要重复。

下面在看一些例子:

  1. 匹配 “ABAB” 型字符串:(\w\w)\1
  2. 匹配 “AABB” 型字符串:(\w)\1(\w)\2
  3. 匹配 “AABA” 型字符串:(\w)\1(?:\w)\1
  4. 匹配 “ABBA” 型字符串:(\w)(\w)\2\1

正则匹配的一些细节

正则眼中的字符串

  正则眼中的字符串是 n 个字符,n+1 个位置,如图:
chandpos

  上图一共 8 个字符 9 个位置,为什么要有字符还要有位置呢?因为位置是可以被匹配的。那么进一步我们再来理解 “占有字符”和“零宽度”:

  • 如果一个子正则表达式匹配到的是字符,而不是位置,而且会被保存到最终的结果中,那个这个子表达式就是 占有字符 的,比如 /ha/(匹配 “ha”)就是占有字符的;
  • 如果一个子正则匹配的是位置,而不是字符,或者匹配到的内容不保存在结果中(其实也可以看做一个位置),那么这个子表达式是零宽度的,比如 read(?=ing)(匹配 “reading”,但是只将 “read” 放入结果中),其中的 (?=ing) 就是零宽度的,它本质代表一个位置,\b^$都是定位符。

  占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。举个栗子,比如 /aa/ 是匹配不了 “a” 的,这个字符串中的 “a” 只能由正则的第一个 “a” 字符匹配,而不能同时由第二个 “a” 匹配(废话);但是位置是可以多个匹配的,比如 /\b\ba/ 是可以匹配 “a” 的,虽然正则表达式里有 2 个表示单词开头位置的 \b 元字符,这两个 \b 是可以同时匹配位置 0(在这个例子中)的。

注意:我们说字符和位置是面向字符串说的,而说占有字符和零宽度是面向正则说的。

控制权和传动

  这两个词可能在搜一些博文或者资料的时候会遇到,这里做一个解释先:
  控制权 是指哪一个正则子表达式(可能为一个普通字符、元字符或元字符序列组成)在匹配字符串,那么控制权就在哪。

  传动 是指正则引擎的一种机制,传动装置将定位正则从字符串的哪里开始匹配。
正则表达式当开始匹配的时候,一般是由一个子表达式获取控制权,从字符串中的某一个位置开始尝试匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的。

  举一个栗子,/read(?=ing)ing\sbook/ 匹配 “reading book”,我们把这个正则看成 5 个子表达式 read(?=ing)ing\sbook,当然你也可以把 read 看做 4 个单独字符的子表达式,只是我们这里为了方便这么看待。read 从位置 0 开始匹配到位置 4,后面的 (?=ing) 继续从位置 4 开始匹配,发现位置 4 后面确实是 ing,于是断言匹配成功,也就是整一个 (?=ing) 就是匹配了位置 4 这一个位置而已(这里更能理解什么是零宽了吧),然后后面的 ing 再从位置 4 开始匹配到位置 7,然后 \s 再从位置 7 匹配到位置 8,最后的 book 从位置 8 匹配到位置 12,整一个匹配完成。

零宽匹配

1
2
正则:^(?=[aeiou])[a-z]+$
源字符串:apple

  首先这个正则表示:匹配这样一个从头到尾完整的字符串,这整一个字符串仅由小写字母组成,并且以 “a”、”e”、”i”、”o”、”u” 这 5 个字母任一字母开头。

  匹配过程:首先正则的 ^(表示字符串开始的位置)获取控制权,从位置 0 开始匹配,匹配成功,控制权交给 (?=[aeiou]),这个子表达式要求该位置右边必须是元音小写字母中的一个,零宽子表达式相互间不互斥,所以从位置 0 开始尝试匹配,右侧是字符串的 “a”,符合因此匹配成功,所以 (?=[aeiou]) 匹配此处的位置 0 匹配成功,控制权交给 [a-z]+,从位置 0 开始匹配,字符串 “apple” 中的每个字符都匹配成功,匹配到字符串末尾,控制权交回正则的 $,尝试匹配字符串结束位置,成功,至此,整个匹配完成。

NFA 和 DFA

  正则表达式引擎主要有 NFA(非确定性有穷自动机)和 DFA(确定性有穷自动机),Java、Perl、PHP、Python 等使用的正则都是基于 NFA 引擎,当然现在又出现个 POSIX NFA。正则的匹配不是从某个字符开始,确切的说是从两个相邻字符的中间位置开始。
  本章节我所参考的内容,有一些质疑的声音 如下

不幸的是,Friedl对匹配过程的分析,是完全错误的——引擎的不同,是指构建的自动机的不同,而不是匹配算法的不同!

  不过我也没有太多精力去深究,大家看到这里的话留个心眼,自己甄别。

NFA

  NFA(Non-deterministic finite automaton),非确定型有穷自动机引擎是以 正则表达式为主导 的。对于表达式的每一部分,都要检查当前文本中是否有与之匹配的字符,若匹配则继续表达式的下一部分,直至所有部分都能匹配,即表达式匹配成功。

1
2
3
4
正则表达式中的位置        字符串中的位置
t¦o(nite|knight|night) ... t¦onight ...
to(ni¦te|knight|night) ... toni¦ght ...
to(nite|knight|nigh¦t) ... tonigh¦t ...

  当碰到分支结构时,引擎会依次选择分支所列的多种匹配模式,直至匹配成功。由于传统 NFA 引擎使用的是顺序结构的多选分支,所以在安排分支的先后顺序时需格外小心,以免写出无意义的多选结构,比如用正则式 perl|perlman 来匹配文本 “perlman book” ,以正则式为主,匹配完 perl 发现成功就不再继续了,正则式中 perlman 相当于没有。

DFA

  DFA(Deterministic finite automaton),确定型有穷自动机引擎是以 文本为主导 的。对于文本的每一部分,都要检查正则式中是否与之匹配,若匹配则继续文本的下一部分,直至所有部分都能匹配,即表达式匹配成功。用正则式 perl|perlman 来匹配文本 “perlman book” 的时候,以文本为主,”perlman book” 的每个字符都要在正则式中匹配一次,所以会匹配到 “perlman”。

1
2
3
4
字符串中的位置	        正则表达式中的位置
... per¦lman book ... per¦l|per¦lman
... perlm¦an book ... perl|perlm¦an
... perlma¦n book ... perl|perlma¦n

  DFA 引擎不需要进行回溯,所以匹配效率一般情况下要高,但是它并不支持捕获组,于是也就不支持反向引用和 $ 这种形式的引用,也不支持环视(Lookaround)、非贪婪模式等一些 NFA 引擎特有的特性。

回溯

  正则匹配过程中会经常用到回溯,因为大部分的匹配并非能够一次性匹配完成。

没有回溯的匹配

  假设我们的正则是 /ab{1,3}c/,当目标字符串是 “abbbc” 时,就没有所谓的“回溯”。其匹配过程是:
backtrack1

其中子表达式 b{1,3}表示 “b” 字符连续出现 1 到 3 次。

有回溯的匹配

  1. 正则是 /ab{1,3}c/,目标字符串是 “abbc”,中间就有回溯。
    backtrack2

  图中第 5 步有红颜色,表示匹配不成功。此时 b{1,3} 已经匹配到了 2 个字符 “b”,准备尝试第三个时,结果发现接下来的字符是 “c”。那么就认为 b{1,3} 就已经匹配完毕。然后状态又回到之前的状态(即第 6 步,与第 4 步一样),最后再用子表达式c,去匹配字符 “c”。当然,此时整个表达式匹配成功了。图中的第6步,就是“回溯”。

  1. 正则是 /ab{1,3}bbc/,目标字符串是 “abbbc”,匹配过程是:
    backtrack3

  其中第 7 步和第 10 步是回溯。第 7 步与第 4 步一样,此时 b{1,3} 匹配了两个 “b”,而第 10 步与第 3 步一样,此时 b{1,3} 只匹配了一个 “b”,这也是 b{1,3} 的最终匹配结果。

  1. 正则是 /".*"/,目标字符串是 "acd"ef,匹配过程是:
    backtrack4

  图中省略了尝试匹配双引号失败的过程。可以看出 .* 是非常影响效率的。为了减少一些不必要的回溯,可以把正则修改为 /"[^"]*"/

Python3 正则函数

  Python3 的 re 模块使 Python 语言拥有全部的正则表达式功能,它提供 Perl 风格的正则表达式模式。

match 函数

  re.match 尝试从字符串的 起始位置 匹配一个模式,如果不是起始位置匹配成功的话,返回 None 。函数语法:

1
re.match(pattern, string, flags=0)

  1. pattern ,匹配的正则表达式。
  2. string ,要匹配的字符串。
  3. flags ,标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

我们可以使用 group(num)groups() 匹配对象函数来获取匹配表达式:

  1. group(num=0) ,匹配的整个表达式的字符串,group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
  2. groups() ,返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3
import re

line = "Cats are smarter than dogs"
# .* 表示任意匹配除换行符(\n、\r)之外的任何单个或多个字符
matchObj = re.match( r'(.*) are (.*?) .*', line, re.M|re.I)

if matchObj:
print (matchObj.group()) # Cats are smarter than dogs
print (matchObj.group(1)) # Cats
print (matchObj.group(2)) # smarter
else:
print ("No match!!")

search 函数

  re.search 扫描 整个字符串 并返回 第一个 成功的匹配。re.match 只匹配 字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回 None;而 re.search 匹配 整个字符串 ,直到找到一个匹配。所以他们的用法和格式是一样的,函数语法:

1
re.search(pattern, string, flags=0)

  1. pattern 匹配的正则表达式。
  2. string 要匹配的字符串。
  3. flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

findall 函数

  在字符串中找到正则表达式所匹配的 所有子串 ,并返回一个列表,如果模式中存在一个或多个捕获组,则返回一个组列表,如果模式有多个组,这将是一个元组列表,如果没有找到匹配的,则返回空列表。matchsearch 是匹配一次,而 findall 匹配所有。语法格式为:

1
findall(string[, pos[, endpos]])

  1. string ,待匹配的字符串。
  2. pos ,可选参数,指定字符串的起始位置,默认为 0。
  3. endpos ,可选参数,指定字符串的结束位置,默认为字符串的长度。

sub 函数

  re.sub 用于替换字符串中的匹配项。语法:

1
re.sub(pattern, repl, string, count=0)

  1. pattern ,正则中的模式字符串。
  2. repl ,替换的字符串,也可为一个函数。
  3. string ,要被查找替换的原始字符串。
  4. count ,模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。

Javascript 正则函数

  JS 中正则语法:

1
/正则表达式主体/修饰符(可选)

修饰符

  正则表达式有三个修饰符:

  1. i,ignoreCase,执行对大小写不敏感的匹配。
  2. g,global,执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
  3. m, multiline,执行多行匹配。

RegExp 对象

  在 JavaScript 中,RegExp 对象是一个预定义了属性和方法的正则表达式对象。

test()
  用于检测一个字符串 是否匹配某个模式,如果字符串中含有匹配的文本,则返回 true,否则返回 false。以下实例用于搜索字符串中的字符 “e”:

1
2
var patt = /e/;
patt.test("The best things in life are free!"); // true

exec()
  如果匹配成功, exec() 方法返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。如果匹配失败,exec() 方法返回 null :

1
2
3
var re = /quick\s(brown).+?(jumps)/ig;
var str = "The Quick Brown Fox Jumps Over The Lazy Dog"
var result = re.exec(str);

result 对象有如下属性:

  1. [0],匹配的全部字符串 “Quick Brown Fox Jumps”。
  2. [1], ... [n],括号中的分组捕获 [1] = “Brown” [2] = “Jumps”。
  3. index,匹配到的字符位于原始字符串的基于 0 的索引值 4。
  4. input,原始字符串 str。
  5. lastIndex,下一次匹配开始的位置 25。

再看一个例子:

1
2
3
4
5
var regex = /(ab.)+/;
var string = "abaabb abcxabd abeab";
regex.exec(string)[0]; //"abaabb"
regex.exec(string)[0]; //"abaabb"
regex.exec(string)[1]; //"abb"

  元字符 + 是贪婪模式,也就是会尽可能多的匹配符合的字符,第一次执行 regex.exec(string) 匹配到的是 “abaabb”,因为遇到空格时候不再符合 (ab.) 的格式所以暂停,贪婪模式取 “abb” ,如果非贪婪模式就是 “aba”。第一次执行 regex.exec(string) 和第一次结果一样,因为 lastIndex 没变,但是有了参数 g 就不一样了。若指定了 g,则下次调用 exec 时,会从上个匹配的 lastIndex 开始查找:

1
2
3
4
5
6
7
8
var regex2 = /(ab.)+/g;
var string2 = "abaabb abcxabd abeab";
regex2.exec(string2);
["abaabb", "abb", index: 0, input: "abaabb abcxabd abeab", groups: undefined]

regex2.exec(string2);
["abc", "abc", index: 7, input: "abaabb abcxabd abeab", groups: undefined]
regex2.exec(string2)[1]; //"abd"

  可以看到指定了参数 g 以后,每一次调用 exec 都会从上个匹配的 lastIndex 开始查找。

String 对象

  在 JavaScript 中,String 对象用于处理文本(字符串)。String 对象有很多属性和方法,我们看几个常用的

match() 方法
  查找一个或多个正则表达式的匹配。matchexec 一样,但 exec 是 RegExp 对象的方法;math 是 String 对象的方法。二者还有一个不同点,就是对参数 g 的解释,如果指定了参数 g,那么 match 一次返回所有的结果:

1
2
3
4
var regex3 = /(ab.)+/g;
var string3 = "abaabb abcxabd abeab";
string3.match(regex3);
["abaabb", "abc", "abd", "abe"]

   JS 和 Python3 的 match 不同,Python3 中 re.match 尝试从字符串的 起始位置 匹配一个模式,如果不是起始位置匹配成功的话,返回 None。JS 的 match 和 Python3 中 非捕获 模式的 re.findall 类似 :

1
2
3
4
5
6
string4 = "abaabb abcxabd abeab"
print(re.findall(r'(ab.)+', string4))
['abb', 'abc', 'abd', 'abe']

print(re.findall(r'(?:ab.)+', string4))
['abaabb', 'abc', 'abd', 'abe']

  小括号是捕获组,会提取小括号里匹配的内容,非捕获组仅参与匹配,不提取内容,既起到了分组的作用,又不会破坏正则表达式的整体性。

search() 方法
  用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置:

1
2
var str = "Visit Runoob!"; 
str.search(/Runoob/i); // 6

replace() 方法
用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串:

1
2
var str = "Visit Microsoft!"; 
str.replace(/microsoft/i,"Runoob"); // "Visit Runoob!"

参考

JS正则表达式一条龙讲解,从原理和语法到JS正则、ES6正则扩展,最后再到正则实践思路
正则表达式 - 语法
揭开正则表达式的神秘面纱
正则表达式
正则表达式中,在括号内的特殊字符哪些需要转义?

解惑正则表达式中的捕获
Python中正则匹配使用findall,捕获分组(xxx)和非捕获分组(?:xxx)的差异
正则表达式括号的作用
正则表达式匹配原理
python正则表达式系列(4)——分组和后向引用

Python3 正则表达式
JavaScript 正则表达式
RegExp.prototype​.exec()
正则表达式回溯法原理

文章目录
  1. 1. 正则表达式
    1. 1.1. 普通字符
    2. 1.2. 特殊字符
    3. 1.3. 匹配单个任意字符
    4. 1.4. 限定词
    5. 1.5. 贪婪和非贪婪
    6. 1.6. 括号
    7. 1.7. 分组
    8. 1.8. 反向引用
  2. 2. 正则匹配的一些细节
    1. 2.1. 正则眼中的字符串
    2. 2.2. 控制权和传动
    3. 2.3. 零宽匹配
  3. 3. NFA 和 DFA
    1. 3.1. NFA
    2. 3.2. DFA
  4. 4. 回溯
    1. 4.1. 没有回溯的匹配
    2. 4.2. 有回溯的匹配
  5. 5. Python3 正则函数
    1. 5.1. match 函数
    2. 5.2. search 函数
    3. 5.3. findall 函数
    4. 5.4. sub 函数
  6. 6. Javascript 正则函数
    1. 6.1. 修饰符
    2. 6.2. RegExp 对象
    3. 6.3. String 对象
  7. 7. 参考