摘要: 本文旨在总结一些编写表达式的技巧和原理。鉴于介绍python中re模块的使用方法的文章太多。所以本文在基础方面都是略过,而在回溯原理和一些技巧方面记录一点点学习总结。
        最近的时间内对正则表达式进行了一点点学习。所选教材是《mastering regular
expressions》,也就是所谓的《精通正则表达式》。读过一遍后,顿感正则表达式的强大和精湛之处。其中前三章是对正则表达式的基本规则的介绍和
铺垫。七章以后是对在具体语言下的应用。而核心的部分则是四五六这三章节。
        其中第四章是讲了整个正则表达式的精华,即传统引擎NFA的回溯思想。第五章是一些例子下对回溯思想的理解。第六章则是对效率上的研究。根源也是在回溯思想上的引申和研究。
        这篇文章是我结合python官方re模块的文档以及这本书做一个相应的总结。
        其中官方的文档:
http://docs.python.org/3.3/library/re.html


        由于我都是在python上联系和使用的,所以后面的问题基本都是在python上提出来的,所以这本书中的其它正则流派我均不涉及。依
书中,python和perl风格差不多,属于传统NFA引擎,也就是以“表达式主导“,采用回溯机制,匹配到即停止(
顺序敏感,不同于POSIX NFA等采用匹配最左最长的结果)。

        对于回溯部分,以及谈及匹配的时候,将引擎的位置总是放在字符和字符之间,而不是字符本身。比如^对应的是第一个字符之前的那个”空白“位置。

基础规则的介绍

python中的转义符号干扰

python中,命令行和脚本等,里面都会对转义符号做处理,此时的字符串会和正则表达式的引擎产生冲突。即在python中字符串
'\n'会被认为是换行符号,这样的话传入到re模块中时便不再是‘\n’这字面上的两个符号,而是一个换行符。所以,我们在传入到正则引擎时,必须让引
擎单纯的认为是一个'\'和一个'n',所以需要加上转义符成为'\\n',针对这个情况,python中使用raw_input方式,在字符串前加上
r,使字符串中的转义符不再特殊处理(即python中不处理,统统丢给正则引擎来处理),那么换行符就是r'\n'

基本字符

1 .        #普通模式下,匹配除换行符外的任意字符。(指定DOTALL标记以匹配所有字符)

量词限定符

1 *        #匹配前面的对象0个或多个。千万不要忽略这里的0的情况。
2 +        #匹配前面的对象1个或多个。这里面的重点是至少有一个。
3 ?        #匹配前面的对象0个或1个。
4 {m}      #匹配前面的对象m次
5 {m,n}    #匹配前面的对象最少m次,最多n次。

锚点符

1 ^        #匹配字符串开头位置,MULTILINE标记下,可以匹配任何\n之后的位置
2 $        #匹配字符串结束位置,MULTILINE标记下,可以匹配任何\n之前的位置

正则引擎内部的转义符号

01 \m    m是数字,所谓的反向引用,即引用前面捕获型括号内的匹配的对象。数字是对应的括号顺序。
02 \A    只匹配字符串开头
03 \b    可以理解一个锚点的符号,此符号匹配的是单词的边界。这其中的word定义为连续的字母,数字和下划线。
04       准确的来说,\b的位置是在\w和\W的交界处,当然还有字符串开始结束和\w之间。
05 \B    和\b对应,本身匹配空字符,但是其位置是在非"边界"情况下.
06       比如r'py\B'可以匹配'python',但不能匹配'py,','py.' 等等
07 \d    匹配数字
08 \D    匹配非数字
09 \s    未指定UNICODE和LOCALE标记时,等同于[ \t\n\r\f\v](注意\t之前是一个空格,表示也匹配空格)
10 \S    与\s相反
11 \w    未指定UNICODE和LOCALE标记时,等同于[a-zA-Z0-9_]
12 \W    和\w相反
13 \Z    只匹配字符串结尾
14 其他的一些python支持的转移符号也都有支持,如前面的'\t'

字符集

[]
尤其注意,这个字符集最终 只匹配一个字符(既不是空,也不是一个以上!),所以前面的一些量词限定符,在这里失去了原有的意义。
另外,'-'符号放在两个字符之间的时候,表示ASCII字符之间的所有字符,如[0-9],表示0到9.
而放在字符集开头或者结尾,或者被'\'转义时候,则只是表示特指'-'这个符号
最后,当在开头的地方使用'^',表示排除型字符组.

括号的相关内容

普通型括号

1 (...)    普通捕获型括号,可以被\number引用。

扩展型括号

01 (?aiLmsx)
02 a        re.A
03 i        re.I    #忽略大小写
04 L        re.L
05 m        re.M
06 s        re.S    #点号匹配包括换行符
07 x        re.X    #可以多行写表达式
08 如:
09 re_lx = re.compile(r'(?iS)\d+$')
10 re_lx = re.compile(r'\d+',re.I|re.S)    #这两个编译表达式等价
01 (?:......)      #非捕获型括号,此括号不记录捕获内容,可节省空间   
02 (?P<name>...)   #此捕获型括号可以使用name来调用,而不必依赖数字。使用(?P=name)调用。
03 (?#...)         #注释型括号,此括号完全被忽略
04 (?=...)   #positive lookahead assertion    如果后面是括号中的,则匹配成功
05 (?!...)   #negative lookahead assertion    如果后面不是括号中的,则匹配成功
06 (?<=...)  #positive lookbehind assertion   如果前面是括号中的,则匹配成功
07 (?<!...)  #negative lookbehind assertion   如果前面不是括号中的,则匹配成功
08  #以上<span>四种类型的断<b>言</b></span>,本身均不匹配内容,只是告知正则引擎是否开始匹配或者停止。
09  #另外在后两种后项断言中,必须为<b>定长断言</b>。
10 (?(id/name)yes-pattern|no-pattern)
11 #如有由id或者name指定的组存在的话,将会匹配yes-pattern,否则将会匹配no-pattern,通常情况下no-pattern可以省略。

匹配优先/忽略优先符号

在量词限定符中,默认的情况都是匹配优先,也就是说,在符合条件的情况下,正则引擎会尽量匹配多的字符(
贪婪规则
当在这些符号后面加上'?',则正则引擎会成为忽略优先,此时的正则引擎会匹配
尽可能少的字符。

如'??'会先匹配没有的情况,然后才是1个对象的情况。而{m,n}?则是优先匹配m个对象,而不是占多的n个对象。

相关进阶知识

python属于perl风格,属于传统型NFA引擎,与此相对的是POSIX NFA和DFA等引擎。所以大部分讨论都针对传统型NFA

传统型NFA中的顺序问题

NFA是基于表达式主导的引擎,同时,传统型NFA引擎会在找到第一个符合匹配的情况下立即停止:即得到匹配之后就停止引擎。
而POSIX NFA 中不会立刻停止,会在所有可能匹配的结果中寻求最长结果。这也是有些bug在传统型NFA中不会出现,但是放到后者中,会暴露出来。
引申一点,NFA学名为”非确定型有穷自动机“,DFA学名为”确定型有穷自动机“
这里的非确定和确定均是对被匹配的目标文本中的字符来说的,在NFA中,每个字符在一次匹配中即使被检测通过,也不能确定他是否真正通过,因为NFA中会出现回溯!甚至不止一两次。图例见后面例子。而在DFA中,由于是目标文本主导,所有对象字符只检测一遍,到文本结束后,过就是过,不过就不过。这也就是”确定“这个说法的原因。

回溯/备用状态

备用状态

当出现可选分支时,会将其他的选项存储起来,作为备用状态。当前的匹配失败时,引擎进行回溯,则会回到最近的备用状态。
匹配的情况中,匹配优先与忽略优先某种意义上是一致的,只是顺序上有所区别。当存在多个匹配时,两种方式进行的情况很可能是不同的,但是当不存在匹配时,他们俩的情况是一致的,即必然尝试了所有的可能。

回溯机制两个要点

  1. 在正则引擎选择进行尝试还是跳过尝试时,匹配优先量词和忽略优先量词会控制其行为。
  2. 匹配失败时,回溯需要返回到上一个备用状态,原则是后进先出(后生成的状态首先被回溯到)

回溯典型举例:



上图可以看到,传统型NFA到D点即匹配结束。而在阴影中POSIX NFA的匹配流程,需要找到所有结果,
并在这些结果中取最长的结果返回

作为对比说明,下面是目标文本不能匹配时,引擎走过的路径:
如下图,我们看到此时POSIX NFA和传统型NFA的匹配路径是一致的



以上的例子引发了一个匹配时的思考,很多时候我们应该尽量避免使用'.*' ,因为其总是可以匹配到最末或者行尾,浪费资源。
既然我们只寻求引号之间的数据,往往可以借助排除型数组来完成工作。
此例中,使用'[^'']*'这个来代替'.*'的作用显而易见,我们只匹配非引号的内容,那么遇到第一个引号即可退出*号控制权。

固化分组思想

 固化分组的思想很重要,
但是python中并不支持。使用(?>...)括号中的匹配时如果产生了备选状态,那么一旦离开括号便会被立即
引擎抛弃掉(从而无法回溯!)。举个典型的例子如:

1 '\w+:'

这个表达式在进行匹配时的流程是这样的,会优先去匹配所有的符合\w的字符,假如字符串的末尾没有':',即匹配没有找到冒号,此时触发回溯机制,他会迫使前面的\w+释放字符,并且在交还的字符中重新尝试与':'作比对
但是问题出现在这里:    \w是不包含冒号的,显然无论如何都不会匹配成功,可是依照回溯机制,引擎还是得硬着头皮往前找,这就是对资源的浪费。
所以我们就需要避免这种回溯,对此的方法就是将前面匹配到的内容固化不令其存储备用状态!,那么引擎就会因为没有备用状态可用而只得结束匹配过程。大大减少回溯的次数!

Python模拟固化过程

虽然python中不支持,但书中提供了利用前向断言来模拟固化过程。

1 (?=(...))\1

本身,
断言表达式中的结果是不会保存备用状态的,而且他也不匹配具体字符,但是通过巧妙的
添加一个捕获型括号来反向引用这个结果,就达到了固化分组的效果!对应上面的例子则是:

1 '(?=(\w+))\1:'

多选结构

多选结构在传统型NFA中, 既不是匹配优先也不是忽略优先。而是按照顺序进行的。所以有如下的利用方式

  1. 在结果保证正确的情况下,应该优先的去匹配更可能出现的结果。将可能性大的分支尽可能放在靠前
  2. 不能滥用多选结构,因为当匹配到多选结构时,缓存会记录下相应数目的备用状态。举例子:[abcdef]和‘a|b|c|d|e|f’这两个表达式,虽然都能完成你的某个目的,但是尽量选择字符型数组,因为后者会在每次比较时建立6个备用状态,浪费资源。

一些优化的理念和技巧

平衡法则

好的正则表达式需寻求如下平衡:
  1. 只匹配期望的文本,排除不期望的文本。(善于使用非捕获型括号,节省资源)
  2. 必须易于控制和理解。避免写成天书。。
  3. 使用NFA引擎,必须要保证效率(如果能够匹配,必须很快地返回匹配结果,如果不能匹配,应该在尽可能短的时间内报告匹配失败。

处理不期望的匹配

在处理过程中,我们总是习惯于使用星号等非硬性规定的量词(其实是个不好的习惯),
这样的结果可能导致我们使用的匹配表达式中没有必须匹配的字符,例子如下:

1 '[0-9]?[^*]*\d*'    #只是举个例子,没有实际意义。

上面的式子就是这种情况,在目标文本是“理想”时,可能出现不了什么问题,但是如果本身数据有问题。那么这个式子的匹配结果就完全不可预知。

原因就在于他没有一部分是必须的!它匹配任何内容都是成功的。。。

对数据的了解和假设

其实在处理很多数据的时候,我们的操作数据情况都是不一样的,
有时会很规整,那么我们可以省掉考虑复杂表达式的情况,
但是反过来,当来源很杂乱的时候,就需要思考多一些,对各种可能的情形做相应的处理。

引擎中一般存在的优化项

编译缓存

反复使用编译对象时,应该在使用前,使用re.compile()方法来进行编译,这样在后面调用时不必每次重新编译。节省时间。尤其是在循环体中反复调用正则匹配时。
锚点优化
配合一些引擎的优化,应尽量将锚点单独凸显出来。对比^a|^b,其效率便不如^(a|b)
同样的道理,系统也会处理行尾锚点优化。所以在写相关正则时,如果有可能的话,将锚点使用出来。
量词优化
引擎中的优化,会对如.* 这样的量词进行统一对待,而不是按照传统的回溯规则,所以,从理论上说'(?:.)*' 和'.*'是等价的,不过具体到引擎实现的时候,则会对'.*'进行优化。速度就产生了差异。
消除不必要括号以及字符组
这个在python中是否有
未知。只是在支持的引擎中,会对如[.]中转化成\.,因为显然后者的效率更高(字符组处理引起额外开销)

以上是一些引擎带的优化,自然实际上是我们无法控制的的,不过了解一些后,对我们后面的一些处理和使用有很大帮助。

其他技巧和补充内容

过度回溯问题

消除指数级匹配

形如下面:
1 (\w+)*

这种情况的表达式,在匹配长文本的时候会遇到什么问题呢,如果在文本匹配失败时(别忘了,如果失败,则说明已经回溯了
所有的可能),想象一下,*号退一个状态,里面的+号就包括其余的
所有状态,验证都失败后,回到外面,*号
退到倒数第二个备用状态,再进到括号内,+号又要回溯一边比上一轮差1的
备用状态数,当字符串很长时,
就会出现指数级的回溯总数。系统就会'卡死'。甚至当有匹配时,这个匹配藏在回溯总数的中间时,也是会造成卡死的情况。所以,使用NFA的引擎时,必须要注意这个问题!

我们采用如下思路去避免这个问题:
占有优先量词(python中使用前向断言加反向引用模拟)
道理很简单,既然庞大的回溯数量都是被储存的备用状态导致的,那么我们直接使引擎放弃这些状态。说到底是摆脱(regex*)* 这种形式。

1 import re
2 re_lx = re.compile(r'(?=(\w+))\1*\d')

效率测试代码

在测试表达式的效率时,可借助以下代码比较所需时间。在两个可能的结果中择期优者。

01 import re
02 import time
03 re_lx1 = re.compile(r'your_re_1')
04 re_lx2 = re.compile(r'your_re_2')
05  
06 starttime = time.time()
07 repeat_time = 100
08 for i in range(repeat_time):
09     s='test text'*10000
10     result = re_lx1.search(s)
11 time1 = time.time()-starttime
12 print(time1)
13  
14 starttime = time.time()
15 for i in range(repeat_time):
16     s='test text'*10000
17     result = re_lx2.search(s)
18 time2 = time.time()-starttime
19 print(time2)

量词等价转换

现在来看看大括号量词的效率问题
1,当大括号修饰的对象是类似于字符数组或者\d这种
非确定性字符时,使用大括号效率高于重复叠加对象。即:
\d{5}优于\d\d\d\d\d
经测试在python中后者优于前者。会快很多.
2,但是当重复的字符时确定的某一个字符时,则简单的重复叠加对象的效率会高一些。这是因为引擎会对单纯的字符串内部优化(虽然我们不知道具体优化是如何做到的)
aaaaa 优于a{5}
总体上说'\d' 肯定是慢于'1'
我使用的python3中的re模块,经测试,不使用量词会快。
综上,python中总体上使用量词不如简单的列出来!(与书中不同!)

锚点优化的利用

下面这个例子假设出现匹配的内容在字符串对象的结尾,那么下面的第一个表达式是快于第二个表达式的,原因在于前者有锚点的优势。

1 re_lx1 = re.compile(r'\d{5}$')   
2 re_lx2 = re.compile(r'\d{5}')    #前者快,有锚点优化

排除型数组的利用

继续,假设我们要匹配一段字符串中的5位数字,会有如下两个表达式供选择:
经过分析,我们发现\w是包含\d的,当使用匹配优先时,前面的\w会包含数字,之所以能匹配成功,或者确定失败,是后面的\d迫使前面的量词交还一些字符。
知道这一点,我们应该尽量避免回溯,一个顺其自然的想法就是不让前面的匹配优先量词涉及到\d

1 re_lx1 = re.compile(r'^\w+(\d{5})')
2 re_lx2 = re.compile(r'^[^\d]+\d{5}')    #优于上面的表达式

总体来说,在我们没有时间去深入研究模块代码的时候,只能通过尝试和反复修改来得到最终的复合预期的表达式。

常识优化措施

然而我们利用可能的提升效果去尝试修改的时候很有可能
适得其反

因为某些我们看来缓慢的回溯在正则引擎内部会进行一定的优化

“取巧”的修改又可能会关闭或者避开了这些优化,所以结果也许会令我们很失望。
以下是书中提到的一些
常识性优化措施:
1 避免重新编译(循环外创建对象)
2 使用非捕获型括号(节省捕获时间和回溯时状态的数量)
3 善用锚点符号
4 不滥用字符组
5 提取文本和锚点。将他们从可能的多选分支结构中提取出来,会提取速度。
6 最可能的匹配表达式放在多选分支前面

一个很好用的核心公式

’opening normal*(special normal*)* closing‘

这个公式
特别用来对于匹配在两个特殊分界部分(可能不是一个字符)内的normal文本,special则是处理当分界部分也许和normal部分混乱的情况。
有如下的三点避免这个公式无休止匹配的发生。
  1. special部分和normal部分匹配的开头不能重合。一定保证这两部分在任何情况下不能匹配相同的内容,不然在无法出现匹配时遍历所有情况,此时引擎的路径就不能确定。
  2. normal部分必须匹配至少一个字符
  3. special部分必须是固定长度的

举个例子:

1 [^\\"]+(\\.[^\\"]+)* #匹配两个引号内的文本,但是不包括被转义的引号

Python下的正则表达式原理和优化笔记的更多相关文章

  1. Python实战之正则表达式RE/re学习笔记及简单练习

    # .,\w,\s,\d,,^,$# *,+,?,{n},{n,},{n,m}# re模块用于对python的正则表达式的操作.## 字符:## . 匹配除换行符以外的任意字符# \w 匹配字母或数字 ...

  2. 【python下使用OpenCV实现计算机视觉读书笔记1】输入输出

    说明: 该部分内容为<OpenCV Computer Vision with Python>读书笔记. 1.读入文件与保存. import cv2 image=cv2.imread('a. ...

  3. 【python下使用OpenCV实现计算机视觉读书笔记4】保存摄像头视频

    读取摄像头内容,然后保存一段十秒钟的视频. import cv2 cameraCapture = cv2.VideoCapture(0) fps = 30 # an assumption size = ...

  4. 【python下使用OpenCV实现计算机视觉读书笔记3】读写视频文件

    代码例如以下: import cv2 videoCapture = cv2.VideoCapture('car.avi') fps = videoCapture.get(cv2.cv.CV_CAP_P ...

  5. 【python下使用OpenCV实现计算机视觉读书笔记2】图像与字节的变换

    import cv2 import numpy import os # Make an array of 120,000 random bytes. randomByteArray = bytearr ...

  6. Python下opencv使用笔记(一)(图像简单读取、显示与储存)

    写在之前 从去年開始关注python这个软件,途中间间断断看与学过一些关于python的东西.感觉python确实是一个简单优美.easy上手的脚本编程语言,众多的第三方库使得python异常的强大. ...

  7. Python下opencv使用笔记(图像频域滤波与傅里叶变换)

    Python下opencv使用笔记(图像频域滤波与傅里叶变换) 转载一只程序喵 最后发布于2018-04-06 19:07:26 阅读数 1654  收藏 展开 本文转载自  https://blog ...

  8. Python下探究随机数的产生原理和算法

    资源下载 #本文PDF版下载 Python下探究随机数的产生原理和算法(或者单击我博客园右上角的github小标,找到lab102的W7目录下即可) #本文代码下载 几种随机数算法集合(和下文出现过的 ...

  9. Python下图片的高斯模糊化的优化

    资源下载 #本文PDF版下载 Python下图片的高斯模糊化的优化(或者单击我博客园右上角的github小标,找到lab102的W6目录下即可) #本文代码下载 高斯模糊(一维)优化代码(和本文方法集 ...

随机推荐

  1. 分布式模式之Broker模式(转)

    问题来源: 创建一个游戏系统,其将运行在互联网的环境中.客户端通过WWW服务或特定的客户端软件连接到游戏服务器,随着流量的增加,系统不断的膨胀,最终后台数据.业务逻辑被分布式的部署.然而相比中心化的系 ...

  2. cs108 03 ( 调试, java通用性)

    Debuger Great questions These questions will solve most bugs: what method shows the symptom ? what l ...

  3. js学习笔记25----Event对象

    Event : 事件对象,当一个事件发生的时候,和当前这个对象发生的这个事件有关的一些详细的信息都会被临时保存到一个指定的地方-event 对象,供我们在需要时调用. 事件对象必须在一个事件调用的函数 ...

  4. 一种基于匹配回朔的 css3 选择器引擎实现

    介绍 CSS 选择器是一种应用于 DOM 节点查找场景的特定微型语法, 本质上和正则表达式一样都是一种模式匹配语言,灵活使用可以方便得获取指定位置的节点集合. 目前 W3C 推荐标准为 Selecto ...

  5. Cloudera公司主要提供Apache Hadoop开发工程师认证

    Cloudera Cloudera公司主要提供Apache Hadoop开发工程师认证(Cloudera CertifiedDeveloper for Apache Hadoop ,CCDH)和Apa ...

  6. JSP 页面中用绝对路径显示图片

    首先,图片和工程不在一个盘符下.图片也不能放到工程下.  在JSP 文件中 <img src="E:/图片/1.jpg"/>  这样是引不到图片的.因为,JSP页面在引 ...

  7. jboss6.4 域模式自动部署

    1.通过jenkins传递部署包到服务器,调用自动部署shell完成jboss6.4的域模式部署: 2.自动部署shell如下: 完成部署包重命名(从jenkins过来的包可能没有改名:). 调用jb ...

  8. 传递任意数量的实参*parameter&使用任意数量的关键字实参**parameter

    1.*形参名(*parameter) 有时候我们不知道知道函数需要接受多少个实参,所以我们可以在形参名前加一个*,是让python创建一个名为parameter的空元组,并将收到的所有值都封装到这个元 ...

  9. oracle decode处理被除数为0 的情况

    ,,a) per from aa; 例如 我的b为(N30+N31+N32+N33+N34+N35+N36+N37+N38) ,,(N33)||'%' WHERE ssrq=''||sssq||'';

  10. Spring_day01--Spring的bean管理(xml方式)_属性注入介绍

    Spring的bean管理(xml方式) Bean实例化的方式 1 在spring里面通过配置文件 创建对象 2 bean实例化(创建对象)三种方式实现 第一种 使用类的无参数构造创建(重点) Use ...