(021) Linux之正则表达式

十年运维系列之基础篇 - Linux

作者:曾林 

联系:1494445739@qq.com

网站:www.jplatformx.com

版权:文章未经同意请勿转载


一、引言

      简单地说,正则表达式是一种符号表示法,用来识别文本模式。在某种程度上,他们类似于匹配文件和路径名时使用的shell通配符,但其用途更广泛。许多命令行工具和大多数编程语言都支持正则表达式,以此来解决文本操作方面的问题。然而,在不同的工具,以及不同的编程语言之间,正则表达式都会略有不同,这让事情进一步麻烦起来。方便起见,我们将正则表达式的讨论限定在POSIX标准中(它涵盖了大多数命令行工具)。

 

二、grep——文本搜索

      我们用来处理正则表达式的主要程序是grep。grep的名字源于“global regular expression print”,由此可以看到,grep与正则表达式有关。实际上,grep搜索文本文件中与指定正则表达式匹配的行,并将结果送至标准输出。

      grep按照如下方式接受选项和参数。 

      shell> grep [options] regex [file...]

      其中regex代表的就是某个正则表达式。下表列出了grep常用的选项。

选项 功能描述
-i 忽略大小写。不区分大写和小写字符,也可以用--ignore-case指定
-v 不匹配。正常情况下,grep会输出匹配行,而该选项可使grep输出不包含匹配项的所有行。也可以用--invert-match指定
-c 输出匹配项数目(如果有-v选项,那就输出不匹配项目的数目)而不是直接输出匹配行本身。也可以用--count指定。
-n 在每个匹配行前面加上该行在文件内的行号。也可以用--line-number指定
-l 输出匹配项文件名而不是直接输出匹配行自身。也可以用--files-with-matches指定
-L 与-l选项类似,但输出的是不包含匹配项的文件名。也可以用--files-without-match指定
-h 进行多文件搜索时,抑制文件名输出。也可以用--no-filename指定

 

三、元字符和文字

      虽然看起来不是很明显,但grep搜索一直都在使用正则表达式,尽管那些例子都十分简单。正则表达式bzip用于匹配文本中至少包含4个字符、存在连续的按b、z、i、p顺序组成的字符串的行。字符串bzip中的字符都是文字字符,即它们只能与自身进行匹配。除了文字字符,正则表达式还可以包含用于指定更为复杂的匹配的元字符。正则表达式的元字符包括以下字符:

      ^ $ . [ ] { } - ? * + ( ) | \

      其他所有字符则被当做文字字符,但是在极少数的情况下,反斜杠字符用来创建元序列,以及用来对元字符进行转义,使其成文文字字符。

      注: 可以看到,当shell在执行扩展时,许多正则表达式的元字符在shell中具有特殊的意义。所以,在命令行中输入包含元字符的正则表达式时,应把这些元字符用引号括起来以避免不必要的shell扩展。

 

四、任意字符

      接下来讨论的第一个元字符是“点”字符或者句点字符,该字符用于匹配任意字符。如果将其加入到某个正则表达式中,它将会在对应位置匹配任意字符。举例如下图:

      上述命令行,搜索到了所有匹配正则表达式.zip的命令行。但其输出结果有一些有趣的地方,比如说输出中并没有包含zip程序,这是因为正则表达式中的“.”元字符将匹配长度增加到了4个字符。而“zip”只包含了三个字符,所以不匹配。同样,如果列表中某个文件包含了文件扩展名“.zip”,那么该文件也会被认为是匹配文件,因为文件扩展名中的“.”符号也被当做任意字符处理了。

 

五、锚

      插入符(^)和美元符号($)在正则表达式里被当做锚,也就是说正则表达式只与行的开头(^)或是末尾($)的内容进行匹配比较。比如下图中的例子:

      上例中搜索的是行开头、行末尾都有字符串“zip”(例如:zip自成一行)的文件。请注意:正则表达式“^$”(行开头和行末尾之间没有字符)将会匹配空行。

 

六、中括号表达式和字符类

      中括号除了可以用于匹配正则表达式中给定位置的任意字符外,还可以用于匹配指定字符集中的单个字符。借助于中括号,我们可以指定要匹配的字符集(也包括那些可能会被解释为元字符的字符)。如下命令行则利用了一个两个字母组成的字符集,用于匹配包含bzip和gzip字符串的文本行。如下图所示:

      一个字符集可以包含任意数目的字符,并且当元字符放置到中括号中时,会失去它们的特殊含义。然而,在两种情况下,则会在中括号中使用元字符,并且会有不同的含义。第一个就是插入符(^),它在中括号内使用表示否定;另外一个是连字符(-),它表示字符范围。

  1. 否定

      如果中括号内的第一个字符是插入符(^),那么剩下的字符则被当作不应该在指定位置出现的字符集。作为演示,下图展示了相关的例子:

      通过使用否定操作,我们可以得到那些包含zip字符串但zip前面既不是b也不是g的所有程序。请注意,此时zip命令仍然没有出现在结果列表中,由此可见否定,字符集仍然需要在指定位置有对应字符,只不过这个字符不是否定字符集中的成员而已。

      插入字符“^”只有是中括号表达式中的第一个字符时才会被当作否定符,如果不是第一个,“^”将会丧失其特殊含义而成为普通字符。

 

      2. 传统字符范围

      如果我们建立一个正则表达式,用于查找文件名以大写字母开头的文件,可以用下面的命令行:

      shell> grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt

      这仅仅是将26个大写英文字母写入中括号的小事,但是要输入26个字母实在有点麻烦,我们可以使用范围符号来简化,如下的命令行语句:

      shell> grep -h '^[A-Z]' dirlist*.txt

 

      3. POSIX字符类

      首先需要声明地是,这里所指的POSIX字符类并不是针对grep正则表达式的,而是针对shell路径名的扩展。

      传统的字符范围表示方法是很容易理解的,而且能够有效、快速地指定字符集。但不足之处在于,它并不是所有的情况都适合。虽然到目前为止,在使用grep程序时还没有遇到任何问题,但是在其他程序中则可能会遇到问题。如下图所示:

      Linux发行版本不同,上述命令行得到的结果可能会有不同,甚至有可能是空列表。本例中的列表来自于CentOS系统。该命令行得到了预期的效果——只有以大写字母开头的文件列表。但是,如果我们使用下面的命令行,便会得到完全不同的结果。如下图所示:

      为什么会出现这样的差异呢?下面简单介绍一下:

      在UNIX开发初期,它只识别ASCII字符,而正是这一特性导致了上面的差异。在ASCII码中,前32个字符(第0~31个字符)都是控制字符(像Tab键、空格键以及Enter键),后32个字符(第32~63)包含打印字符,包括大多数的标点符号以及数字0~9,接下来的32个(64~95)包含大写字母和一些标点符号,最后的32个字母(第96~127)则包含了小写字母以及更多的标点符号。基于这样的安排,使用ASCII的系统使用了下面的这种排序:

      ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

      这与通常的字典排序不一样,字典中字母的排序表通常如下:

     aBbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

      随着Unix在美国以外的国家普及,人们越来越希望计算机能支持美式英语中找不到的字符。于是ASCII码字符表也得以扩展,开始使用8位二进制来表示,这就是增加了第128~255个字符,兼容了更多的语言。为了支持这样的功能,POSIX标准引入了域(locale)的概念,它通过不停调整以选择特定的位置所需要的字符集。我们可以使用下面的命令行查看语言设置。      

      有了这个设置,POSIX兼容的应用程序使用的便是字典中的字母排列顺序,而不是用ASCII吗中的字符排列顺序。这样,便解释了上面命令行的诡异行为。A~Z的字符范围,用字典中的顺序诠释时,包括了字母表中除了小写字母a的所有字母,因此使用命令行ls /usr/sbin/[A-Z]*才会出现全然不同的结果。

      为了解决这一问题,POSIX标准包含了许多标准字符类,这些字符类提供了一些有用的字符范围。

字符类 描述
[:alnum:] 字母字符和数字字符;在ASCII码中,与[A-Za-z0-9]等效
[:word:] 基本与[:alnum:]一样,只是多了一个下划线字符(_)
[:alpha:] 字母字符,在ASCII码中,等效于[a-zA-Z]
[:blank:] 包括空格和制表符
[:cntrl:] ASCII控制码;包括ASCII字符0~31和127
[:digit:] 数字0~9
[:graph:] 可见字符;在ASCII码中,包括数字33~126
[:lower:]

小写字母

[:punct:] 标点符合字符;在ASCII中,与[-!"#%&'()*+,./:;<=>?@[\\\]_`{|}~]等效
[:print:] 可打印字符;包括[:graph:]中的所有字符再加上空格字符
[:space:] 空白字符如空格符、制表符、回车符、换行符、垂直制表符以及换页符。在ASCII中,等效为[\t\r\n\v\f]
[:upper:] 大写字母
[:xdigit:] 用于表示十六进制的字符;在ASCII中,与[0-9A-Fa-f]等效

      当然,即便是有了这么多字符类,仍然没有比较方便的方法表示部分范围,如[A-M]。

      使用字符类,上面的例子可以改写成下面的形式:

      shell> ls /usr/sbin/[[:upper:]]*

      然而,请记住,上述并不是一个正则表达式的实例,它其实只是shell路径名扩展的一个例子。在此处提及这些,主要是因为这两种用法都支持POSIX字符集。

      当然,你可以设置自己的系统采用传统的ASCII字符顺序,方法就是改变LANG环境变量的值。LANG变量包含语言的名称以及该语言环境中使用的字符集,该参数值在Linux系统选择安装语言时就已经设定。将环境设置为使用传统的UNIX行为,可将LANG变量值设为POSIX:export LANG=POSIX。请注意,这样的改变将会导致系统使用美式英语(更精确地说,是ASCII码格式)的字符集,所以在进行此改变之前请三思。

 

七、POSIX基本正则表达式和扩展正则表达式的比较

      在读者正觉得正则表达式已经复杂到不能在复杂时,又会发现POSIX规范将正则表达式的实现方法分为了两种:基本正则表达式(BRE)和扩展正则表达式(ERE)。到目前为止,我们所讨论的正则表达式的所有特性,都得到了兼容POSIX的应用程序的支持,并且都是以BRE的方式实现。grep命令就是这样的例子。

      BRE和ERE到底有什么区别?其实仅仅是元字符的不同!在BRE方式中,只承认^、$、.、[、]、*这些是元字符,所有其他的字符都被识别为文字字符。而ERE中,则添加了(、)、{、}、?、+、|等元字符(及其相关功能)。

      然后,只有在用反斜杠进行转义的情况下,字符(、)、{、}才会在BRE被当作元字符来处理,而ERE中,任何元符号前面加上反斜杠反而会使其被当做文字字符来处理。

      由于下面要讨论的特性是ERE的一部分,所以需要使用不一样的grep。传统上,这是由egrep程序来执行的,但是GNU版本的grep可以运用-E选项以支持ERE方式。

      什么是POSIX?POSIX也就是Portable Operating System Interface(末尾增加X只是为了更流畅)的缩写。它是20世纪80年代中期,IEEE开始开发了一套规范UNIX和类UNIX系统工作方式的标准。这些标准,官方名称为IEEE 1003,定义了应用程序接口(API)、shell以及一些实用程序,它们可以在标准类UNIX系统中找到。该标准由Richard Stallman提议命名为POSIX,后来被IEEE采纳。

 

八、或选项

      我们将要讨论的第一个扩展正则表达式的特性是或选项,它是用于匹配表达式集的工具。中括号表达式可以从指定字符集中匹配单一字符,而或选项则用于从字符串集或正则表达式集中寻找匹配项。

      以下便利用grep结合echo作为演示实例。首先,我们进行一个简单的字符串匹配。

      shell> echo "AAA" | grep -i "aaa"  => 结果输出AAA

      shell> echo "BBB" | grep -i "aaa" =>  没有输出结果

      这是一个非常直白的例子,将echo的输出结果送至grep进行匹配搜索。如果匹配成功,结果便输出打印出来;如无匹配项,则无结果输出。

      现在添加或选项,它用元字符"|"表示。

      shell> echo "AAA" | grep -iE "aaa|bbb"  => 结果输出AAA

      shell> echo "BBB" | grep -iE "aaa|bbb"  => 结果输出BBB

      shell> echo "CCC" | grep -iE "aaa|bbb"  => 没有输出结果

      这里出现了'AAA|BBB'正则表达式,此表达式的含义是“匹配字符串AAA或者匹配字符串BBB”。请注意,由于此处使用的是扩展特性,所以grep命令增加了-E选项(虽然可以使用egrep命令来代替),并且将正则表达式用引号引起来以防止shell将元字符“|”当作管道操作符来处理。另外,或选项并不局限于两种选择,还可以有更多的选择项。例如:

      shell> echo "AAA" | grep -iE "aaa|bbb|ccc|ddd"  => 结果输出为AAA

      为了将或选项可与其他正则表达式符号结合使用,我们可以用“()”将或选项的所有元素与其他符号隔开。

      shell> grep -Eh '^(bz|gz|zip)' dirlist*.txt      

      以上表达式的含义是匹配文件名以bz、gz或是zip开头的文件。如果不使用括号,该正则表达式的含义就完全不同,其匹配的便是文件名以bz开头或者是包含gz和zip的文件。

 

九、限定符

      扩展正则表达式(ERE)提供多种方法指定某元素匹配的次数。

  1. ?——匹配某元素0次或1次

      该限定符实际上意味着“前面的元素可选”。比如,我们想检查某电话号码的有效性。所谓电话号码有效,指的是电话号码必须是下面两种形式(nnn)nnn-nnnn和nnn nnn-nnnn中的一种,其中n是数值。于是,我们可以构造如下所示的正则表达式。

      ^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$

      此表达式中,括号字符的后面增加了“?”符号以表示括号字符只能匹配一次或零次。同样,由于括号字符在ERE中通常是元字符,所以其前面加上了反斜杠告诉shell此括号为文字字符。实例如下图:

 

      2. *——匹配某元素0次或多次

      与“?”元字符类似,“*”用于表示一个可选择的条目。然而,与“?”不同,该条目可以出现多次,而不仅仅是一次。例如,如果我们想知道一串字符是否是一句话,也就是说,这串字符是否以大写字母开头而以句话结束,并且中间内容是任意数目的大小写字母和空格,那么要匹配这种非常粗糙的句子定义,可以用如下正则表达式:

      [[:upper:]][[:upper:][:lower:] ]*\.

      该表达式包含了三个条目:包含[:upper:]字符类的中括号表达式、包含[:upper:]和[:lower:]两个字符类以及一个空格的中括号表达式、用反斜杠转义过的原点符号。第二个条目后面紧跟着“*”元字符,所以只要句子的第一个字母是大写字母,后面不管会出现多少数目的大小写字母都无关紧要。样例如下图:

      该表达式匹配第一个测试语句,但是不匹配第二个。原因是它的首字母不是大写。

 

      3. +——匹配某元素一次或多次

      “+”元字符与“*”非常类似,只是“+”要求置于其前面的元素至少出现一次。示例如下:该正则表达式用于匹配由单个空格分隔的一个或者多个字母字符组成的行。

      ^([[:alpha:]]+ ?)+$

      示例如下图所示:

 

      4. {}——以指定次数匹配某元素

      “{”和“}”元字符用于描述最小和最大次数的需求匹配。可以通过4种方法来指定。见下表所示:

指定项 含义
{n} 前面的元素恰好出现n次则匹配
{n,m} 前面的元素出现的次数在n~m次之间时则匹配
{n,} 前面的元素出现次数超过n次则匹配
{,m} 前面的元素出现的次数不超过m次则匹配

      回到前面电话号码的例子,我们可以运用此处讲到的指定重复次数的办法将原来的正则表达式"^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$"简化为“^\(?[0-9]{3} [0-9]{3}-[0-9]{4}\)?$”。具体如下图所示:

 

posted @ 2015-03-08 22:40  jplatformx  阅读(414)  评论(0编辑  收藏  举报