摘要: 本文旨在总结一些编写表达式的技巧和原理。鉴于介绍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个或多个。这里面的重点是至少有一个。 |
5 |
{m,n} #匹配前面的对象最少m次,最多n次。 |
锚点符
1 |
^ #匹配字符串开头位置,MULTILINE标记下,可以匹配任何\n之后的位置 |
2 |
$ #匹配字符串结束位置,MULTILINE标记下,可以匹配任何\n之前的位置 |
正则引擎内部的转义符号
01 |
\m m是数字,所谓的反向引用,即引用前面捕获型括号内的匹配的对象。数字是对应的括号顺序。 |
03 |
\b 可以理解一个锚点的符号,此符号匹配的是单词的边界。这其中的word定义为连续的字母,数字和下划线。 |
04 |
准确的来说,\b的位置是在\w和\W的交界处,当然还有字符串开始结束和\w之间。 |
05 |
\B 和\b对应,本身匹配空字符,但是其位置是在非 "边界" 情况下. |
06 |
比如r 'py\B' 可以匹配 'python' ,但不能匹配 'py,' , 'py.' 等等 |
09 |
\s 未指定 UNICODE 和LOCALE标记时,等同于[ \t\n\r\f\v](注意\t之前是一个空格,表示也匹配空格) |
11 |
\w 未指定 UNICODE 和LOCALE标记时,等同于[a - zA - Z0 - 9_ ] |
14 |
其他的一些python支持的转移符号也都有支持,如前面的 '\t' |
字符集
[]
尤其注意,这个字符集最终 只匹配一个字符(既不是空,也不是一个以上!),所以前面的一些量词限定符,在这里失去了原有的意义。
另外,'-'符号放在两个字符之间的时候,表示ASCII字符之间的所有字符,如[0-9],表示0到9.
而放在字符集开头或者结尾,或者被'\'转义时候,则只是表示特指'-'这个符号
最后,当在开头的地方使用'^',表示排除型字符组.
括号的相关内容
普通型括号
1 |
(...) 普通捕获型括号,可以被\number引用。 |
扩展型括号
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中,由于是目标文本主导,所有对象字符只检测一遍,到文本结束后,过就是过,不过就不过。这也就是”确定“这个说法的原因。
回溯/备用状态
备用状态
当出现可选分支时,会将其他的选项存储起来,作为备用状态。当前的匹配失败时,引擎进行回溯,则会回到最近的备用状态。
匹配的情况中,匹配优先与忽略优先某种意义上是一致的,只是顺序上有所区别。当存在多个匹配时,两种方式进行的情况很可能是不同的,但是当不存在匹配时,他们俩的情况是一致的,即必然尝试了所有的可能。
回溯机制两个要点
- 在正则引擎选择进行尝试还是跳过尝试时,匹配优先量词和忽略优先量词会控制其行为。
- 匹配失败时,回溯需要返回到上一个备用状态,原则是后进先出(后生成的状态首先被回溯到)
回溯典型举例:
上图可以看到,传统型NFA到D点即匹配结束。而在阴影中POSIX NFA的匹配流程,需要找到所有结果,
并在这些结果中取最长的结果返回。
作为对比说明,下面是目标文本不能匹配时,引擎走过的路径:
如下图,我们看到此时POSIX NFA和传统型NFA的匹配路径是一致的。
以上的例子引发了一个匹配时的思考,很多时候我们应该尽量避免使用'.*' ,因为其总是可以匹配到最末或者行尾,浪费资源。
既然我们只寻求引号之间的数据,往往可以借助排除型数组来完成工作。
此例中,使用'[^'']*'这个来代替'.*'的作用显而易见,我们只匹配非引号的内容,那么遇到第一个引号即可退出*号控制权。
固化分组思想
固化分组的思想很重要,
但是python中并不支持。使用(?>...)括号中的匹配时如果产生了备选状态,那么一旦离开括号便会被立即
引擎抛弃掉(从而无法回溯!)。举个典型的例子如:
这个表达式在进行匹配时的流程是这样的,会优先去匹配所有的符合\w的字符,假如字符串的末尾没有':',即匹配没有找到冒号,此时触发回溯机制,他会迫使前面的\w+释放字符,并且在交还的字符中重新尝试与':'作比对。
但是问题出现在这里: \w是不包含冒号的,显然无论如何都不会匹配成功,可是依照回溯机制,引擎还是得硬着头皮往前找,这就是对资源的浪费。
所以我们就需要避免这种回溯,对此的方法就是将前面匹配到的内容固化,不令其存储备用状态!,那么引擎就会因为没有备用状态可用而只得结束匹配过程。大大减少回溯的次数!
Python模拟固化过程
虽然python中不支持,但书中提供了利用前向断言来模拟固化过程。
本身,
断言表达式中的结果是不会保存备用状态的,而且他也不匹配具体字符,但是通过巧妙的
添加一个捕获型括号来反向引用这个结果,就达到了固化分组的效果!对应上面的例子则是:
多选结构
多选结构在传统型NFA中, 既不是匹配优先也不是忽略优先。而是按照顺序进行的。所以有如下的利用方式
- 在结果保证正确的情况下,应该优先的去匹配更可能出现的结果。将可能性大的分支尽可能放在靠前。
- 不能滥用多选结构,因为当匹配到多选结构时,缓存会记录下相应数目的备用状态。举例子:[abcdef]和‘a|b|c|d|e|f’这两个表达式,虽然都能完成你的某个目的,但是尽量选择字符型数组,因为后者会在每次比较时建立6个备用状态,浪费资源。
一些优化的理念和技巧
平衡法则
好的正则表达式需寻求如下平衡:
- 只匹配期望的文本,排除不期望的文本。(善于使用非捕获型括号,节省资源)
- 必须易于控制和理解。避免写成天书。。
- 使用NFA引擎,必须要保证效率(如果能够匹配,必须很快地返回匹配结果,如果不能匹配,应该在尽可能短的时间内报告匹配失败。)
处理不期望的匹配
在处理过程中,我们总是习惯于使用星号等非硬性规定的量词(其实是个不好的习惯),
这样的结果可能导致我们使用的匹配表达式中没有必须匹配的字符,例子如下:
1 |
'[0-9]?[^*]*\d*' #只是举个例子,没有实际意义。 |
上面的式子就是这种情况,在目标文本是“理想”时,可能出现不了什么问题,但是如果本身数据有问题。那么这个式子的匹配结果就完全不可预知。
原因就在于他没有一部分是必须的!它匹配任何内容都是成功的。。。
对数据的了解和假设
其实在处理很多数据的时候,我们的操作数据情况都是不一样的,
有时会很规整,那么我们可以省掉考虑复杂表达式的情况,
但是反过来,当来源很杂乱的时候,就需要思考多一些,对各种可能的情形做相应的处理。
反复使用编译对象时,应该在使用前,使用re.compile()方法来进行编译,这样在后面调用时不必每次重新编译。节省时间。尤其是在循环体中反复调用正则匹配时。
锚点优化
配合一些引擎的优化,应尽量将锚点单独凸显出来。对比^a|^b,其效率便不如^(a|b)
同样的道理,系统也会处理行尾锚点优化。所以在写相关正则时,如果有可能的话,将锚点使用出来。
量词优化
引擎中的优化,会对如.* 这样的量词进行统一对待,而不是按照传统的回溯规则,所以,从理论上说'(?:.)*' 和'.*'是等价的,不过具体到引擎实现的时候,则会对'.*'进行优化。速度就产生了差异。
消除不必要括号以及字符组
这个在python中是否有
未知。只是在支持的引擎中,会对如[.]中转化成\.,因为显然后者的效率更高(字符组处理引起额外开销)
以上是一些引擎带的优化,自然实际上是我们无法控制的的,不过了解一些后,对我们后面的一些处理和使用有很大帮助。
其他技巧和补充内容
过度回溯问题
消除指数级匹配
形如下面:
这种情况的表达式,在匹配长文本的时候会遇到什么问题呢,如果在文本匹配失败时(别忘了,如果失败,则说明已经回溯了
所有的可能),想象一下,*号退一个状态,里面的+号就包括其余的
所有状态,验证都失败后,回到外面,*号
退到倒数第二个备用状态,再进到括号内,+号又要回溯一边比上一轮差1的
备用状态数,当字符串很长时,
就会出现指数级的回溯总数。系统就会'卡死'。甚至当有匹配时,这个匹配藏在回溯总数的中间时,也是会造成卡死的情况。所以,使用NFA的引擎时,必须要注意这个问题!
我们采用如下思路去避免这个问题:
占有优先量词(python中使用前向断言加反向引用模拟)
道理很简单,既然庞大的回溯数量都是被储存的备用状态导致的,那么我们直接使引擎放弃这些状态。说到底是摆脱(regex*)* 这种形式。
2 |
re_lx = re. compile (r '(?=(\w+))\1*\d' ) |
效率测试代码
在测试表达式的效率时,可借助以下代码比较所需时间。在两个可能的结果中择期优者。
03 |
re_lx1 = re. compile (r 'your_re_1' ) |
04 |
re_lx2 = re. compile (r 'your_re_2' ) |
06 |
starttime = time.time() |
08 |
for i in range (repeat_time): |
10 |
result = re_lx1.search(s) |
11 |
time1 = time.time() - starttime |
14 |
starttime = time.time() |
15 |
for i in range (repeat_time): |
17 |
result = re_lx2.search(s) |
18 |
time2 = time.time() - starttime |
量词等价转换
现在来看看大括号量词的效率问题
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}' ) #优于上面的表达式 |
总体来说,在我们没有时间去深入研究模块代码的时候,只能通过尝试和反复修改来得到最终的复合预期的表达式。
常识优化措施
然而我们利用可能的提升效果去尝试修改的时候很有可能
适得其反
,
因为某些我们看来缓慢的回溯在正则引擎内部会进行一定的优化
,
“取巧”的修改又可能会关闭或者避开了这些优化,所以结果也许会令我们很失望。
以下是书中提到的一些
常识性优化措施:
2 |
使用非捕获型括号(节省捕获时间和回溯时状态的数量) |
5 |
提取文本和锚点。将他们从可能的多选分支结构中提取出来,会提取速度。 |
一个很好用的核心公式
’opening normal*(special normal*)* closing‘
这个公式
特别用来对于匹配在两个特殊分界部分(可能不是一个字符)内的normal文本,special则是处理当分界部分也许和normal部分混乱的情况。
有如下的三点避免这个公式无休止匹配的发生。
- special部分和normal部分匹配的开头不能重合。一定保证这两部分在任何情况下不能匹配相同的内容,不然在无法出现匹配时遍历所有情况,此时引擎的路径就不能确定。
- normal部分必须匹配至少一个字符
- special部分必须是固定长度的
举个例子:
1 |
[^\\ "]+(\\.[^\\" ] + ) * #匹配两个引号内的文本,但是不包括被转义的引号 |
- Python实战之正则表达式RE/re学习笔记及简单练习
# .,\w,\s,\d,,^,$# *,+,?,{n},{n,},{n,m}# re模块用于对python的正则表达式的操作.## 字符:## . 匹配除换行符以外的任意字符# \w 匹配字母或数字 ...
- 【python下使用OpenCV实现计算机视觉读书笔记1】输入输出
说明: 该部分内容为<OpenCV Computer Vision with Python>读书笔记. 1.读入文件与保存. import cv2 image=cv2.imread('a. ...
- 【python下使用OpenCV实现计算机视觉读书笔记4】保存摄像头视频
读取摄像头内容,然后保存一段十秒钟的视频. import cv2 cameraCapture = cv2.VideoCapture(0) fps = 30 # an assumption size = ...
- 【python下使用OpenCV实现计算机视觉读书笔记3】读写视频文件
代码例如以下: import cv2 videoCapture = cv2.VideoCapture('car.avi') fps = videoCapture.get(cv2.cv.CV_CAP_P ...
- 【python下使用OpenCV实现计算机视觉读书笔记2】图像与字节的变换
import cv2 import numpy import os # Make an array of 120,000 random bytes. randomByteArray = bytearr ...
- Python下opencv使用笔记(一)(图像简单读取、显示与储存)
写在之前 从去年開始关注python这个软件,途中间间断断看与学过一些关于python的东西.感觉python确实是一个简单优美.easy上手的脚本编程语言,众多的第三方库使得python异常的强大. ...
- Python下opencv使用笔记(图像频域滤波与傅里叶变换)
Python下opencv使用笔记(图像频域滤波与傅里叶变换) 转载一只程序喵 最后发布于2018-04-06 19:07:26 阅读数 1654 收藏 展开 本文转载自 https://blog ...
- Python下探究随机数的产生原理和算法
资源下载 #本文PDF版下载 Python下探究随机数的产生原理和算法(或者单击我博客园右上角的github小标,找到lab102的W7目录下即可) #本文代码下载 几种随机数算法集合(和下文出现过的 ...
- Python下图片的高斯模糊化的优化
资源下载 #本文PDF版下载 Python下图片的高斯模糊化的优化(或者单击我博客园右上角的github小标,找到lab102的W6目录下即可) #本文代码下载 高斯模糊(一维)优化代码(和本文方法集 ...
随机推荐
- 【sql绕过】Bypass waf notepad of def
文章是通过阅读<[独家连载]我的WafBypass之道 (SQL注入篇)>写的阅读笔记. Waf的类型 1.云waf云waf通常是CDN包含的waf,DNS在解析的时候要解析到cdn上面制 ...
- cocos2dx中的一些坑
1.CCTableView中的lua绑定LUA_TableViewDataSource 在TestLua里有例子,有个TableView的例子function TableViewTestLayer.c ...
- call_usermodehelper内核中运行用户应用程序
init是用户空间第一个程序,在调用init前程序都运行在内核态,之后运行init时程序运行到用户态. 操作系统上,一些内核线程在内核态运行,它们永远不会进入用户态.它们也根本没有用户态的内存空间.它 ...
- PCB封装技术
TQFP(thin quad flat package,即薄塑封四角扁平封装)薄四方扁平封装低成本,低高度引线框封装方案. MLF(MicroLeadFrame),MLF接近于芯片级封装(Chip S ...
- 30Mybatis_mybatis和spring整合-原始dao开发
这篇文章很重要, 第一步:我们讲一下整合的思路: 我们以前要用Mybatis是需要sqlMapConfig.xml(这个配置文件需要配置dataource,以及mapper.xml文件.)sqlMap ...
- 【UVa】And Then There Was One(dp)
http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&p ...
- 『Spring.NET+NHibernate+泛型』框架搭建之DAO(三)★
本节内容介绍Nhibernate所封装的数据库訪问层.只是我增加了泛型进行封装.大概思路:首先,我们有一个接口层,另一个相应的实现层.在接口层中我们先定义一个父接口,父接口中定义每个接口都可能会用到的 ...
- RabbitMQ之Exchange-4
RabbitMQ消息模型的核心思想是生产者不会将消息直接发送给队列.生产者通常不知道消息将会被哪些消费者接收,按照刚开始里介绍的rabbitMQ中所画的,生产者不是直接将消息发送给Queue么认识会交 ...
- C#正则表达式操作中使用LINQ
比如:[程序员][代码]博客园 - 程序员的网上家园,代码改变世界 提取出来的Tag应该是:[程序员].[代码] Regex _regexTag = new Regex(@"^(\[[^\] ...
- 通过代码注册COM、DLL组件
注册代码如下: C++ Code 1234567891011121314151617181920212223242526272829303132333435363738 // //====== ...