「正则表达式入门」

  CREATED BY JENKINSBOT

什么是正则表达式?

正则表达式是一种描述字符串结构模式的形式化的表达方法,具体讲指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。正则表达式使用一些基本的字符序列来描述所有的字符串结构。

正则表达式的历史由来

1)、美国新泽西州的Warren McCulloch和出生在美国底特律的Walter Pitts这两位神经生理方面的科学家,研究出了一种用数学方式来描述神经网络的新方法,他们创造性地将神经系统中的神经元描述成了小而简单的自动控制元,从而作出了一项伟大的工作革新。

2)、在1956 年,出生在被马克·吐温(Mark Twain)称为“美国最美丽的城市之一”的哈特福德市的一位名叫Stephen Kleene的数学科学家,他在Warren McCulloch和Walter Pitts早期工作的基础之上,发表了一篇题目是《神经网事件的表示法》的论文,利用称之为“正则集合的数学符号”来描述此模型,引入了正则表达式的概念。正则表达式被作为用来描述其称之为“正则集的代数”的一种表达式,因而采用了“正则表达式”这个术语。

3)、之后一段时间,人们发现可以将这一工作成果应用于其他方面。Ken Thompson就把这一成果应用于计算搜索算法的一些早期研究,Ken Thompson是Unix的主要发明人,也就是大名鼎鼎的Unix之父。Unix之父将此符号系统引入编辑器QED,正则表达式的第一个实用应用程序即为Unix中的qed编辑器,然后是Unix上的编辑器ed,并最终引入grep。

4)、自此以后,正则表达式被广泛地应用到各种UNIX或类UNIX的工具中,如大家熟知的Perl。Perl的正则表达式源自于Henry Spencer编写的正则表达式,之后已演化成了PCRE(Perl Compatible Regular Expressions),PCRE是一个由Philip Hazel开发的、为很多现代工具所使用的库。

正则表达式的实际应用有那些?

正则表达式更侧重于文本处理。

正则表达式有三类主要应用:
(1)测试字符串是否匹配某个模式。例如,可以输入一个字符串进行测试看该字符串中是否存在一个电话号码模式或者一个信用卡模式,这成为数据的有效性检验。
(2)替换匹配模式的文本。可以再文档中使用一个正则表达式来表示特定文字,然后可以将其全部删除或者替换成别的文字。
(3)提取匹配模式的文本。可以用来在文本或者输入字段中查找特定的文字。

正则表达式在实践中的应用:
(1)偷懒。批量文本替换操作。
(2)爬虫。从抓取的网页中筛选出感兴趣的内容,不需要第三方提供固定的接口。
(3)日志分析。从NGINX的访问日志中筛选出百度蜘蛛的访问日志。
(4)网站验证。对用户输入的参数进行检查校验。
(5)等等…

正则表达式的语法结构

以grep中的egrep命令为例。在GNU/Linux中,grep命令用于从文本文件中匹配包含某个文本的行。而命令egrep只是对grep -E命令的简单包装,二者是等价的。所以,egrep命令也是用于从文本中检索出包含匹配正则字符串的行。egrep使用的是扩展正则表达式(关于扩展正则表达式后面会提到)。

如下命令:

# egrep ‘^(From|Subject):’ mail-file

上面的命令,该命令用于从邮件中匹配发件人以及邮件主题。其中,mail-file为要检查的文件名。^(From|Subject):为传递给egrep命令的正则表达式。而单引号是SHELL的要求的,与正则表达式无关,是为了防止正则表达式中的特殊字符被SHELL解析。

如上的^(From|Subject):,一个正则表达式是一个字符序列,这个字符序列中的字符分为两种(1)普通字符,(2)元字符。普通字符就对应了普通语言中的单词。比如,From就代表单词From,Subject就代表Subject。而元字符则对应着语法。比如,脱字(^)就是一个元字符,与其他字符结合起来,实现我们期望的功能。

正则表达式的”流派“

很多的编程语言都存在方言,比如LISP。正则表达式也存在不同的”流派“。

很多的工具使用”不同的正则表达式“来完成任务。这里”不同的正则表达式“指的是工具在支持的支持的元字符和其他特性方面的差异。比如,单词分界符号\<\>,有些工具中并不支持这两个元字符,还有些工具中的这两个元字符具有不同的含义。我们称这个两个工具使用了不同流派的正则表达式。

流派这个词描述的是所有这些细微的实现规定。

由Perl开创的流派因为表能力及其强大而被人们熟知,后来很多的语言从中汲取思想,提供了自己的增则表示,然后给自己贴上了PCRE的标签(比如PHP、Java的大量正则包、Python、Tcl等等)。但各种语言在重要的方面也各有不同。而且Perl也在不断的演化发展。

像其他东西一样,总的局面越来越混乱,变得乱七八糟的。

命令egrep中的正则元字符

行首(^)与行尾($)

脱字符号(^)代表了行的开始。在匹配时,脱字符号用于将文本”锚定“到行的开头。比如下面的命令:

# egrep ‘^The’ /tmp/file.txt

上面的命令匹配第一字符为T,紧接一个h,紧接一个e的行。注意不要用”匹配以The开始的行“来描述这个正则表达式的作用。虽然二者在含义上是一样的,但是前者更贴近于正则内部的处理逻辑。

同样的对于美元符号($),它代表了行的结束。对于正则表达式The$在匹配时,只寻找以e结尾,e前是一个h,h前使一个T的行。

总的说,脱字符号(^)和美元符号($)代表的是一个位置,而不时具体的字符。

字符组([…])

字符组用于匹配若干字符之一。对于美语中的gray和英语中的grey,这两个单词的含义是一样的,有时会出现在同一篇文章中,我们要一次性匹配出包行这两个单词的所有行,那我们就要用到字符组。字符组的结构就是使用一对方括号来包含若干要匹配的字符,并且只会匹配若干字符中的一个。如下示例:

# egrep ‘gr[ae]y’ /tmp/file.txt

上面的命令可以匹配文件中包含gray或grey的行。对于正则表达式gr[ae]y,e只能匹配e,a只能匹配a,而[ae]可以匹配到a或e。所以,gr[ae]y的含义就是先匹配一个g,然后一个r,然后一个a或e,然后一个y。如果文本中有首字母大小写的情况,比如Gray、Grey、gray,我们可以使用[Gg]r[ae]y表达式。

如果我们要配置数字则可以使用表达式[0123456789]。比如正则表达式<H[123456]>可以匹配<H1>、<H2>、<H3>等等。

在字符组内部,有一个字符组元字符,它是连字符(-)。连字符用于表示一个范围:<H[1-6]><H[123456]>是等价的。常用的还有[0-9]、[a-z]、[A-Z]。字符组内部也可以使用多个范围:[0-9a-zA-Z]。还可以与其他字符一起使用:[0-9a-zA-Z.!?_]。表示范围的连字符省去了在字符组内写一长串字符的麻烦。

注意事项
(1)、对于连字符,只有在字符组中才有意义,这也是为什么称为字符组元字符而不是元字符的原因。
(2)、即使连字符在字符组中也不一定有意义。如果连字符在字符组中的第一个字符,那它就是一个普通字符。比如:[-A-Z],第一个连字符就是一个普通的连字符,而第二个连字符才表示范围。

排除型字符组([^…])

排除字符组([^…])会匹配任何未列出的字符。例如,[^a-z]会匹配除了a到z以外的任何字符,等价于在字符组里手动列出了所有a到z以外的字符。

这里的脱字符号(^)并不表示”行首“,而表示的是”排除“。因为它在字符组里,所以有了不同的含义,表示排除。而在字符组外面,它的作用是锚定,表示行首。

比如,我们要在文件里搜索一些特殊的单词,这些单词中,字母q后面更的不是字母u。用正则表达式表示就是q[^u],如下命令:

# egrep ‘q[^u]’ /tmp/foo.txt

上面的命令会搜索出所有包含字母q后面的字母不为u的行。注意,以字母q结尾的行依旧是匹配不出来的,比如Iraq。 所以,请记住排除型字符组的表示”匹配未列出的字符”,而不是“不要匹配里出的字符”,Iraq就是一个很好的例子。

匹配任意字符(.)

点符号(.)可以用来匹配任意字符。如果我们要在表达式中使用一个“匹配任意字符”的占位符,点好就很好用。

比如,我们要匹配2015/03/12、2015-03-12、2015.03.12类型的日期,不怕麻烦的话,我们可以用2015[-/.]03[-/.]12,或者使用点:2015.03.12。在2015[-/.]03[-/.]12中的点并不是元字符,它只是一个普通的点,只代表点符号。同样的,2015[-/.]03[-/.]12中的连字符也只是一个普通的连字符,没有表示范围的含义,因为它是字符组中的第一个字符。如果写成[/-.],那连字符就是表范围了,显然在这个示例中是错误的,因为我们只需要连字符本身,不需要它表示范围。

注意一点,对于正则表达式2015.03.12,因为点符号可以表示任意字符,所以可能会匹配到2015303412。这是没有办法避免的,所以日常使用正则表达式时依赖于你对文本的了解以及你想达到的精准度

多选结构(|)

|是一个非常简单的元字符,表示“或”。能够把不同的子表达式合并为一个表达式,同时这个合并后的表达式能够匹配任意的子表达式。比如,对于AbcDef这两个表达式,Abc|Def可以同时匹配其中的任意一个表达式。在这样的组合中,子表达式称为多选分支

对于之前的gr[ae]y可以改写为gr(a|e)ygray|grey。对于gr(a|e)y,它使用括号来来划出多选结构的范围。对于没有括号的gra|ey,意思就成为了gra或者ey,很显然不符合我们的要求。对于gr[a|e]y中的|,它只是一个普通的字符,没有多选的含义,也是不符合我们要求的。

以下的几个正则都是对多选结构的使用,他们的含义是相同的:

(First|1st) [Ss]treet

(Fir|1)st [Ss]treet

再比如,下面的几个,他们的含义也是相同的:

Jeffrey|Jeffery

Jeff(rey|ery)

Jeff(re|er)y

注意一点,在多选结构中使用脱字符号要小心。比如:^From|Subject^(From|Subject)这两个看着相似,但是匹配结果是完全不相同的。^From|Subject由两个子表达式组成:^From与Subject,实用性并不大。我们希望在每个多选分支前都有脱字符来表示行首,所以应该使用括号:

^(From|Subject)

行的起始为F,r,o,m这个序列,或者行的其实为S,u,b,j,e,c,t这个序列。如下命令使用该表达式来筛选数据,

# egrep ‘^(From|Subject)’ /tmp/foo.txt

但是,你也可以写作:

# egrep ‘^From|^Subject’ /tmp/foo.txt

这种写法也是可以的,但出现多个多选分支的时候,不如第一种。

忽略大小写

这个功能不时正则表达式的一部分,这个属于egrep命令的一部分,该命令的-i选项表示进行忽略大小写的匹配。可以把-i选项写在之前的正则表达式前面:

# egrep -i ‘^(From|Subject)’ /tmp/foo.txt

如上命令就可以匹配到FROM,FrOM,SUBject等等,否则就只能匹配到包含From或者Subject的行。

单词分界符(\<、\>)

在实际的匹配中,我们可能希望匹配某个单词,但是这个单词可能包含在另外一个单词里。比如,min与varmint。这时候,我们就会使用到元字符序列——单词分界符号:\<\>。有点像匹配行首的脱字符号和匹配行为的美元符号,只不过单词分界符用于匹配单词的开始和结束。如下示例:

# egrep ‘\<cat\>’ /tmp/foo.txt

上面的命令匹配包含单词cat的行。而命令:

# egrep ‘cat’ /tmp/foo.txt

会匹配有c,a,t字符序列的行,这包括了含有cat单词的行。

注意事项
(1)<与>并不是元字符,只有与反斜线相结合时,整个序列具有特殊意义。因此才会被成为元字符序列
(2)并不是所有版本的egrep命令都支持单词分界符号。即使支持,也不见得能认得出单词。因为所谓的单词开始和单词结束,只不过是非字母字符的结束和开始。

可选元素(?)

现在我们看以下color和colour的匹配,这是英美语言的差异。后面的单词多了一个u字母。我们可以使用colou?r这个正则表达式来解决这个问题。问号(?)代表了可选项,表示问号前的对象是可选的。

另外一个例子,我们需要July fourth这个日期,这个日期还可能写成Jul,4th,4的形式,我们可以用July? (fourth|4th|4)来表示。进一步的我们可以简写为July? fourth|4(th)?,此时的问号就作用于前面的括号了。但是不能改写成July? (four|4)(th)?的形式,因为这会出项four的情况,很显然这是不正确的。

其他量词(*、+、{min, max})

加号(+)和星号(*)与问号的作用是类似的。星号表示星号前的对象可以出现任意多次,或者不出现。加号表示加号前的对象可以出现任意多此,但少要出现一次,否则匹配失败。对于星号是不会出项匹配失败的情况,因为它允许出现零次。

加号、星号、问号这三个元字符统称为量词,因为它们用于限定指定元素的出现次数。

有了量词以后,我们就可以提升<H[1-6]>匹配的范围,因为可能会出项<H1 >、<H1 >、<H1 >等类似的情况,而<H[1-6]>是匹配不到的,使用量词改写后的表达式为:

<H[1-6] *>

这样就可以匹配到包含空格的情况。

对于更加复杂的<HR SIZE=16>这样的复杂HTML标签,我们可以使用正则表达式:

<HR +SIZE *= *[0-9]+ *>

进行匹配。如果还要忽略大小写的匹配就可以使用egrep的-i选项:

# egrep -i ‘<HR +SIZE *= *[0-9]+ *>’ /tmp/foo.html

如果SIZE属性是可选项,那么可以写为如下形式:

# egrep -i ‘<HR( +SIZE *= *[0-9]+)? *>’ /tmp/foo.html

区间量词
我们还可以限定某个对象出现的次数。格式为{min, max},这称之为区间量词。比如,[0-9]{4, 7}可以用于匹配长度为4到7的数字。上面的可选元素问号(?)的区间量词的表示法为{0, 1}。所以,July?July{0, 1}是等价的。

扩展及反向引用(()、\1)

目前我们已经见过括号的两种作用:(1)限定多选项的范围。比如gr(a|e)y。(2)组成一个单元,受量词的作用。比如( +SIZE *= *[0-9]+)?

但是,括号还有另外一个作用,虽然不常见,但是也非常有用,经常在sed、awk中出现。这个功能称之为反向引用。反向引用中的括号能够“记住”他们所匹配的文本。这可以用来匹配联系相同的字符串。比如,匹配the the、that that等等重复的形式。

以the the为例,如果要匹配两个连续的the the,可以写作\<the the\>。如果要匹配that that,可以写作\<that that\>,如果还有其他的呢?我们不可能穷举所有的连续单词的情况。这时,我们就可以使用反向引用。首先将\<the the\>中的第一个the替换为能够匹配单词的正则表达式[a-zA-z]+;然后两端加上括号;然后将第二个the替换为特殊的元字符序列\1,最终的结果就是:\<([a-zA-z]+) +\1\>

对于\<([a-zA-z]+) +\1\>,括号会“记住”了它所代表的文本,然后将“记住”的文本带入\1的位置。比如,当([a-zA-z]+)匹配到了the,那\1此时就是the,整个表达式就是\<the +the\>。当([a-zA-z]+)匹配到了that,那\1此时就是that,整个表达式就是\<that +that\>

当然,我们可以在一个正则表达式中使用多个括号,也可以使用\1\2\3分别代表第一个、第二个、第三个括号匹配到的文本。括号是按照左括号开始计数的。所以,([a-z])([0-9])\1\2中的\1代表了([a-z]),而\2代表了([0-9])

转义字符(\)

还有一种特殊情况就是:如果要检索的字符如果是元字符怎么办?比如,我要检索3.3,如果直接使用3.3,那点符号是一个元字符,代表任意字符,那么就会匹配到343、353之类的数字,很显然这不是我们想要的。这时候就可以使用转义字符——反斜线(\)。通过在元字符前面加上反斜线,来将元字符转义为字面含义。比如,只匹配数字3.3,那么正则就可以写为:3\.3

我们还可以使用\([a-zA-Z]+\)来匹配那些在括号里的单词。因为,此时括号使用了反斜线进行了转义,就无特殊含义了,只是普通的括号。

注意事项
(1)如果反斜线后面跟得不时元字符,那么反斜线的含义就与工具的实现有关系了,有个工具会忽略反斜线,有个工具会将反斜线当作普通的字符,视情况而定。
(2)对于字符组内的反斜线,在有的工具中表示转义。比如,[\n]表示换行符号。而在有的工具则表示一个反斜线和一个n字母。

注意的问题

有些东西不是正则表达式的问题,正则表达式也无能为力。

对于文本的处理,依赖于对文本的了解程度。

总结

本章节主要介绍了正则表达式的中的一些元字符及一些案例。对egrep命令中的元字符进行了简单的介绍。

参考文献

http://www.regular-expressions.info/