正则表达式
正则中的 ^
和 $
使用^
和 $ 表示匹配字符串的开头和结尾,常用来检查用户输入是否完全符合期望。如果想要从字符串中匹配子串,则不应该使用^
和 $ 。re.match
起到同样的作用。re.match
从字符串开始位置匹配正则表达式,如果开始位置就不符合,那么将返回None
。所以在python
的使用中
可以利用re.match
和re.search
方法来达到添加与不添加^
的效果,一条表达式就能在两种需求里使用。
正则中的表达修饰符
修饰符 | 描述 |
---|---|
re.I | 使匹配对大小写不敏感,表达式r'hello' 就能匹配'HELLO' |
re.L | 做本地化识别(locale-aware)匹配,re.L 其中L 就是LOCALE ,此处的LOCALE 和另外一个标志UNICODE 相对应,LOCALE 表示使用系统字符集,UNICODE 表示使用Unicode字符集。 |
re.M | 多行匹配,影响 ^ 和 $,指定了以后,’^’会匹配每行的开始;’$’会增加匹配每行的结束; |
re.S | 使 . 匹配包括换行在内的所有字符 |
re.U | 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B. |
re.X | 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。可以写注释,除了在方括号内的和被反斜杠转义的以外的所有空白字符,都将被忽略,而且每行中,一个正常的井号后的所有字符也被忽略 |
正则中的“回溯陷阱”
正则表达式引擎
说起回溯陷阱,要先从正则表达式的引擎说起。正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而 DFA 对应的是文本主导的匹配。DFA从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但NFA支持更多的特性,因而绝大多数编程场景下(包括java,js),我们面对的是NFA。以下面的表达式和文本为例,
1 | text = 'after tonight' |
在NFA匹配时候,是根据正则表达式来匹配文本的,从t开始匹配a,失败,继续,直到文本里面的第一个t,接着比较o和e,失败,正则回退到 t,继续,直到文本里面的第二个t,然后 o和文本里面的o也匹配,继续,正则表达式后面有三个可选条件,依次匹配,第一个失败,接着二、三,直到匹配。
而在DFA匹配时候,采用的是用文本来匹配正则表达式的方式,从a开始匹配t,直到第一个t跟正则的t匹配,但e跟o匹配失败,继续,直到文本里面的第二个 t 匹配正则的t,接着o与o匹配,n的时候发现正则里面有三个可选匹配,开始并行匹配,直到文本中的g使得第一个可选条件不匹配,继续,直到最后匹配。
可以看到,DFA匹配过程中文本中的字符每一个只比较了一次,没有吐出的操作,应该是快于NFA的。另外,不管正则表达式怎么写,对于DFA而言,文本的匹配过程是一致的,都是对文本的字符依次从左到右进行匹配,所以,DFA在匹配过程中是跟正则表达式无关的,而 NFA 对于不同但效果相同的正则表达式,匹配过程是完全不同的。
回溯
说完了引擎,我们再来看看到底什么是回溯。对于下面这个表达式,相信大家很清楚它的意图,
1 | ab{1,3}c |
也就是说中间的b需要匹配1~3次。那么对于文本“abbbc”,按照第1部分NFA引擎的匹配规则,其实是没有发生回溯的,在表达式中的a匹配完成之后,b恰好和文本中的3个b完整匹配,之后是c发生匹配,一气呵成。如果我们把文本换成“abc”呢?无非就是少了一个字母b,却发生了所谓的回溯。
这个就是我们下面将要提到的正则的贪婪特性,也就是说b{1,3}会竭尽所能的匹配最多的字符。在这个地方我们先知道它一直要匹配到撞上南墙为止。 在这种情况下,在发生不匹配之后,整个匹配流程并没有走完,而是像栈一样,将字符c吐出来,然后去用正则表达式中的c去和文本中的c进行匹配。这样就发生了一次回溯。
贪婪、懒惰与独占
下面的几个特殊字符相信大家都知道它们的用法:
- ?: 告诉引擎匹配前导字符0次或一次。事实上是表示前导字符是可选的。
- +: 告诉引擎匹配前导字符1次或多次。
- *: 告诉引擎匹配前导字符0次或多次。
- {min, max}: 告诉引擎匹配前导字符min次到max次。min和max都是非负整数。如果有逗号而max被省略了,则表示max没有限制;如果逗号和max都被省略了,则表示重复min次。
默认情况下,这个几个特殊字符都是贪婪的,也就是说,它会根据前导字符去匹配尽可能多的内容。这也就解释了为什么在第3部分的例子中,第3步以后的事情会发生了。
在以上字符后加上一个问号(?)则可以开启懒惰模式,在该模式下,正则引擎尽可能少的重复匹配字符,匹配成功之后它会继续匹配剩余的字符串。在上例中,如果将正则换为
1 | ab{1,3}?c |
与文本abc
在非贪婪模式下,正则中的b{1,3}?与文本b匹配之后,接着去用c与文本中的c进行匹配,而未发生回溯。
如果在以上四种表达式后加上一个加号(+),则会开启独占模式。同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。我们以下面的表达式为例,
1 | ab{1,3}+bc |
可以发现,在第2和第3步,b{1,3}+会将文本中的2个字母b都匹配上,结果文本中只剩下一个字母c。那么在第4步时,正则中的b和文本中的c进行匹配,当无法匹配时,并不进行回溯,这时候整个文本就无法和正则表达式发生匹配。如果将正则表达式中的加号(+)去掉,那么这个文本整体就是匹配的了。
贪婪 | 懒惰 | 独占 |
---|---|---|
X? | X?? | X?+ |
X* | X*? | X*+ |
X+ | X+? | X++ |
X{n,m} | X{n,m}? | X{n,m}+ |
向前正则匹配和向后正则匹配
向前方向是从左到右,向后的方向是从右到左
模式 | 描述 |
---|---|
(?!re) |
向前否定匹配 |
(?=re) |
向前肯定匹配 |
(?<!re) |
向后否定匹配 |
(?<=re) |
向后肯定匹配 |
1 | # 使用了向后否定和向前否定匹配 |
参考