深入浅出之正则表达式
第一节 理解正则表达式
孟岩
在程序员日常工作中,数据处理占据了相当的比重。而在所有的数据之中,文本又占据了相当的比重。文本能够被人理解,具有良好的透明性,利于系统的开发、测试和维护。然而,易于被人理解的文本数据,机器处理起来就不一定都那么容易。文本数据复杂多变,特定性强,甚至是千奇百怪。因此,文本处理程序可谓生存环境恶劣。
一般来说,文本处理程序都是特定于应用的,一个项目有一个项目的要求,彼此之间很难抽出共同点,代码很难复用,往往是“一次编码,一次运行,到处补丁”。其程序结构散乱丑陋,谈不上有什么“艺术性”,基本上与“模式”、“架构”什么的无缘。在这里,从容雅致、温文尔雅派不上用场,要想生存就必须以暴制暴。
事实上,几十年的实践证明,除了正则表达式和更高级的parser技术,在这样一场街头斗殴中别无利器。而其中,尤以正则表达式最为常用。所以,对于今天的程序员来说,熟练使用正则
表达式着实应该是一种必不可少的基本功。
然而现实情况却是,知道的人很多,善于应用的人却很少,而能够洞悉其原理,理智而高效地应用它的人则少之又少。大多数开发者被它的外表吓倒,不敢也不耐烦深入了解其原理。事实上,正则表达式背后的原理并不复杂,只要耐心学习,积极实践,理解正则表达式并不困难。下面列举的一些条款,来自我本人学习和时间经验的不完全总结。由于水平和篇幅所限,只能浮光掠影,不足和谬误之处,希望得到有识之士的指教。
1. 了解正则表达式的历史
正则表达式萌芽于1940年代的神经生理学研究,由著名数学家StephenKleene第一个正式描述。具体地说,Kleene归纳了前述的神经生理学研究,在一篇题为《正则集代数》的论文中定义了“正则集”,并在其上定义了一个代数系统,并且引入了一种记号系统来描述正则集,这种记号系统被他称为“正则表达式”。
在理论数学的圈子里被研究了几十年之后,1968年,后来发明了UNIX系统的KenThompson第一个把正则表达式用于计算机领域,开发了qed和 grep两个实用文本处理工具,取得了巨
大成功。在此后十几年里,一大批一流计算机科学家和黑客对正则表达式进行了密集的研究和实践。在1980年代早期,UNIX运动的两个中心贝尔实验室和加州大学伯克利分校分别围绕grep工具对正则表达式引擎进行了研究和实现。与之同时,编译器“龙书”的作者 AlfredAho开发了Egrep工具,大大扩展和增强了正则表达式的功能。此后,他又与《C程序设计语言》的作者BrianKernighan等三人一起发明了流行的awk文本编辑语言。到了1986年,正则表达式迎来了一次飞跃。先是C语言顶级黑客HenrySpencer以源代码形式发布了一个用 C语言写成的正则表达式程序库(当时还不叫opensource),从而把正则表达式的奥妙带入寻常百姓家,然后是技术怪杰LarryWall横空出世,发布了Perl语言的第一个版本。自那以后,Perl一直是正则表达式的旗手,可以说,今天正则表达式的标准和地位是由Perl塑造的。Perl5.x发布以后,正则表达式进入了稳定成熟期,其强大能力已经征服了几乎所有主流语言平台,成为每个专业开发者都必须掌握的基本工具。
2. 掌握一门正则表达式语言
使用正则表达式有两种方法,一种是通过程序库,另一种是通过内置了正则表达式引擎的语言本身。前者的代表是Java、.NET、C/C++、Python,后者的代表则是Perl、Ruby、JavaS
cript和一些新兴语言,如Groovy等。如果学习正则表达式的目标仅仅是应付日常应用,则通过程序库使用就可以。但只有掌握一门正则表达式语言,才能够将正则表达式变成编程的直觉本能,达到较高的水准。不但如此,正则表达式语言也能够在实践中提供更高的开发和执行效率。因此,有心者应当掌握一门正则表达式语言。
3. 理解DFA和NFA
正则表达式引擎分成两类,一类称为DFA(确定性有穷自动机),另一类称为NFA(非确定性有穷自动机)。两类引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。
DFA与NFA机制上的不同带来5个影响:
(1)DFA 对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,所以反而应用广泛,当今主要的正则表达式引擎,如Perl、Ruby、Python的re模块、Java和.NET的regex库,都是NFA的。
(2)只有NFA才支持lazy和backreference等特性;
(3)NFA急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA则是“最长的左子正则式优先匹配成功”。
(4)NFA缺省采用greedy量词(见item4);
(5)NFA可能会陷入递归调用的陷阱而表现得性能极差。
我这里举一个例子来说明第3个影响。
例如,用正则式/perl ¦ perlman/来匹配文本‘perlmanbook’。如果是NFA,则以正则式为导向,手里捏着正则式,眼睛看着文本,一个字符一个字符的吃,吃完 ‘perl’以后,跟第一个子正则式/perl/已经匹配上了,于是记录在案,往下再看,吃进一个‘m’,这下糟了,跟子式/per
l/不匹配了,于是把 m吐出来,向上汇报说成功匹配‘perl’,不再关心其他,也不尝试后面那个子正则式/perlman/,自然也就看不到那个更好的答案了。
如果是DFA,它是以文本为导向,手里捏着文本,眼睛看着正则式,一口一口的吃。吃到/p/,就在手里的‘p’上打一个钩,记上一笔,说这个字符已经匹配上了,然后往下吃。当看到/perl/之后,DFA不会停,会尝试再吃一口。这时候,第一个子正则式已经山穷水尽了,没得吃了,于是就甩掉它,去吃第二个子正则式的/m/。这一吃好了,因为又匹配上了,于是接着往下吃。直到把正则式吃完,心满意足往上报告说成功匹配了‘perlman’。
由此可知,要让NFA正确工作,应该使用/perlman ¦ perl/模式。
通过以上例子,可以理解为什么NFA是最左子式匹配,而DFA是最长左子式匹配。实际上,如果仔细分析,关于NFA和DFA的不同之处,都可以出道理。而明白这些道理,对于有效应用正则表达式是非常有意义的。
4. 理解greedy(贪婪)和lazy(惰性)量词
由于日常遇到的正则表达式引擎全都是NFA,所以缺省都采用greedy量词。Greedy量词的意
思不难理解,就是对于/.*/、/\w+/这样的“重复n”次的模式,以贪婪方式进行,尽可能匹配更多字符,直到不得以罢手为止。
举一个例子,以/ <.*> /模式匹配‘ <book> <title> PerlHacks </title> </book> \t’文本,匹配结果不是‘ <book> ’,而是‘ <book> <title> PerlHacks </title> </book> ’。原因就在于NFA引擎以贪婪方式执行“重复n次”的命令。让我们来仔细分析一下这个过程。
条款3指出,NFA的模型是以正则式为导向,拿着正则式吃文本。在上面的例子里,当它拿着/.*/这个正则式去吃文本的时候,缺省情况下它就这么一路吃下去,即使碰到‘> ’字符也不罢手——既然/./是匹配任意字符,‘> ’当然也可以匹配!所以就尽管吃下去,直到吃完遇到结尾(包括\t字符)也不觉得有什么不对。这个时候它突然发现,在正则表达式最后还有一个/> /,于是慌了神,知道吃多了,于是就开始一个字符一个字符的往回吐,直到吐出倒数第二个字符‘> ’,完成了与正则式的匹配,才长舒一口气,向上汇报,匹配字符串从第一个字符‘ <’开始,到倒数第二个字符‘> ’结束,即‘ <book> <title> PerlHacks </title> </book> ’。
Greedy 量词的行为有时确实是用户所需要的,有时则不是。比如在这个例子里,用户可能实际上想得到的是‘book’串。怎么办呢?这时候lazy量词就派上用场了。把模式改为/ <.*?> /
就可以得到‘book’。这个加在‘*’号后面的‘?正则化判别分析’把greedy量词行为变成lazy量词行为,从而由尽量多吃变为尽量少吃,只要吃到一个 ‘> ’立刻停止。
问号在正则表达式里用途最广泛,这里是很重要的一个用途。
5. 理解backtracking
在条款4的基础上解释backtracking就很容易了。当NFA发现自己吃多了,一个一个往回吐,边吐边匹配,这个过程叫做 backtracking(回溯)。由于存在这个过程,在NFA匹配过程中,特别是在编写不合理的正则式匹配过程中,文本被反复扫描,效率损失是不小的。明白这个道理,对于写出高效的正则表达式很有帮助。
第二节 深入浅出之正则表达式
注:JanGoyvaerts为RegexBuddy写的教程的译文
前言:半年前我对正则表达式产生了兴趣,在网上查过不少资料,看过不少的教程,最后在使用一个正则表达式工具RegexBuddy时发现他的教程写的非常好,可以说是我目前见过
最好的正则表达式教程。于是一直想把他翻译过来。这个愿望直到这个五一长假才得以实现,结果就有了这篇文章。关于本文的名字,使用“深入浅出”似乎已经太俗。但是通读原文以后,觉得只有用“深入浅出”才能准确的表达出该教程给我的感受,所以也就不能免俗了。
本文是JanGoyvaerts为RegexBuddy写的教程的译文,版权归原作者所有,欢迎转载。但是为了尊重原作者和译者的劳动,请注明出处!谢谢!
1. 什么是正则表达式
基本说来,正则表达式是一种用来描述一定数量文本的模式。Regex代表RegularExpress。本文将用<<regex>>来表示一段具体的正则表达式。一段文本就是最基本的模式,简单的匹配相同的文本。
2. 不同的正则表达式引擎
正则表达式引擎是一种可以处理正则表达式的软件。通常,引擎是更大的应用程序的一部分。在软件世界,不同的正则表达式并不互相兼容。本教程会集中讨论 Perl5类型的引擎,因为这种引擎是应用最广泛的引擎。同时我们也会提到一些和其他引擎的区别。许多近代的
引擎都很类似,但不完全一样。例如.NET正则库,JDK正则包。
3. 文字符号
最基本的正则表达式由单个文字符号组成。如<<a>>,它将匹配字符串中第一次出现的字符“a”。如对字符串“Jackisaboy”。“J”后的“a”将被匹配。而第二个“a”将不会被匹配。正则表达式也可以匹配第二个“a”,这必须是你告诉正则表达式引擎从第一次匹配的地方开始搜索。在文本编辑器中,你可以使用“查下一个”。在编程语言中,会有一个函数可以使你从前一次匹配的位置开始继续向后搜索。类似的,<<cat>>会匹配“Aboutcatsanddogs”中的“cat”。这等于是告诉正则表达式引擎,到一个<<c>>,紧跟一个<<a>>,再跟一个<<t>>。要注意,正则表达式引擎缺省是大小写敏感的。除非你告诉引擎忽略大小写,否则<<cat>>不会匹配“Cat”。
⏹ 特殊字符
对于文字字符,有11个字符被保留作特殊用途。他们是:[]\^$. ¦?*+()
这些特殊字符也被称作元字符。如果你想在正则表达式中将这些字符用作文本字符,你需要
用反斜杠“\”对其进行换码(escape)。例如你想匹配“1+1=2”,正确的表达式为<<1\+1=2>>
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论