2 可选项元素
2.1 ?
现在来看color和colour的匹配。它们的区别在于,后面的单词比前面的多一个u,我们可以用colou?r来解决这个问题。元字符?
(也就是问号)代表可选项。把它加在一个字符的后面,就表示此处容许出现这个字符,也可以不出现这个字符,问号前面的字符并非匹配成功的必要条件。
?
这个元字符与我们之前看到的元字符都不相同,它只作用于之前紧邻的元素。因此,colou?r
的意思是:c,然后是o,然后是l,然后是o,然后是u,最后是r。u?
是必然能够匹配成功的,有时它会匹配一个u,其他时候则不匹配任何字符。关键在于,无论u是否出现,匹配都是成功的。但这并不等于,任何包含?的正则表达式都永远能四配成功。例如,colo和u?都能在“semicolon”中匹配成功(前者匹配单词中的colo,后者什么字符都没有匹配)。可是最后的r无法匹配,因此,最终colou?r
无法匹配semicolon。
另一个例子,我们需要匹配表示7月4日(July fourth)的文本,其中月份可能写作July或是Jul,而日期可能写作fourth、4th或者是4.显然,我们可以使用“(July|Jul)(fourth|4th|4)
,但也可以用其他的办法来解决这个问题。
首先,我们把(July|Jul
)缩短为“(July?
)。删除|
之后,就没必要保留括号了。于是我们得到July? (fourth|4th|4)
。然后,可以把4th|4
简化为4(th)?
。我们看到,现在?
作用的元素是整个括号了。括号内的表达式可以任意复杂,但是“从括号外来看”它们是个整体。界定作用对象是括号的主要用途之一。我们的表达式现在成了July? (fourth|4(th)?)
。
通过上面两个例子,可以看到正则表达式不是死板的教条,它更像是门艺术。
2.2 其他量词:重复出现
+
(加号)和*
(星号)的作用与问号类似。元字符+
表示“之前紧邻的元素出现一次或多次”,而*
表示“之前紧邻的元素出现任意多次,或者不出现”。换种说法就是,*表示“匹配尽可能多的次数,如果实在无法匹配,也不要紧”。+的意思是“匹配尽可能多的次数,但如果连一次匹配都无法完成,就报告失败“。问号、加号和星号这3个元字符,统称为量词,因为它们限定了所作用元素的匹配次数。
与...?
一样,正则表达式中的...*
也是永远不会匹配失败的,区别只在于它们的匹配结果。而...+
在无法进行任何一次匹配时,会报告匹配失败。
2.3 规定重现次数的范围:区间
使用元字符序列来自定义重现次数的区间:(min,max)。这称为“区间量词(intervalquantifier)”。例如,{3,12}能够容许的重现次数在3到12之间。有人可能会用[a-zA-Z](1,5)
来匹配美国的股票代码(1到5个字母)。问号对应的区间量词是{0.1}。
2.4 括号及反向引用
前面已经见过括号的两种用途:限制多选项的范围;将若干字符组合为一个单元,受问号或星号之类量词的作用。现在介绍括号的另一种用途:括号能够移“记住”它们包含的子表达式匹配的文本。在解决单词重复问题时会用到这个功能。
查找重复单词,比如在一行中有连续的“the the”两个重复的单词,我们先匹配第一个单词,接下来检查“后面的单词是否与它一样”。如果你的egrep支持“反向引用(backreference)”,就可以这么做。反向引用是正则表达式的特性之一,它容许我们匹配与表达式先前部分匹配的同样的文本。
我们先把’the the“中的第一个the替换为能够匹配任意单词的正则表达式[A-Za-z]+
;然后在两端加上括号,最后把后一个‘the’替换为特殊的元字符序列1,就得到了\<([A-Za-z]+) +\1\>
。% egrep -i '\<([a-z]+) +\1\>' files...
这样就可以查找连续重复的单词。
在支持反向引用的工具软件中,括号能够“记忆”其中的子表达式匹配的文本,不论这些文本是什么,元字符序列\1
都能记住它们。而且,在一个表达式中我们可以使用多个括号。再用\1、\2、\3,等来表示第一、第二、第三组括号匹配的文本。所以([A-Z])([0-9])\1\2
中的\1代表[a-z]匹配的内容,而\2代表[0-9]匹配的内容。
注意:它的局限性。因为egrep把每行文字都当作一个独立部分来看待,所以如果重复单词的第一个单词在某行末尾,第二个单词在下一行的开头,这个表达式就无法找到。
2.5 转义
如果需要匹配的某个字符本身就是元字符,正则表达式会如何处理呢?例如,如果想要检索互联网的主机名ega.att.com,使用ega.att.com
可能得不到正确的结果。因为.
本身就是元字符,它可以匹配任何字符,包括空格。
所以,真正匹配文本中点号的元序列应该是反斜线加上点号的组合:ega\.att\.com
。称为“转义的点号”,这样的办法适用于所有的元字符,不过在字符组内部无效。
还可以用\([a-zA-Z] +\)
来匹配一个括号内的单词,例如(very)在开闭括号之前的反斜线消除了开闭括号的特殊意义,于是他们能够匹配文本中的开闭括号。
2.6 正则表达式的目标
一个正则表达式要么能够匹配给定文本中的某些字符,要么不能匹配。在编写正则表达式的时候,必须进行权衡:匹配符合要求的文本,同时忽略不符合要求的文本。在任何语言中,经验都是非常重要的。
2.7 变量名
程序语言标识符(变量名)只包含字母、数字以及下画线,但不能以数字开头。我们可以用[a-zA-Z_][a-zA-Z_0-9]*
,来匹配。第一个字符组匹配可能出现的第一个字符,第二个(包括对应的星号)匹配余下的字符。如果标识符的长度有限制,例如最长只能是32个字符,就加入区间量词{min, max},我们可以用{0,31}
来替代最后的*
。
2.8 引号内的字符串
匹配引号内的字符串最简单的办法是使用这个表达式"[^"]*"
。
两端的引号用来匹配字符串开头和结尾的引号。在这两个引号之间的文本可以包括双引号之外的任何字符。所以我们用[^"]
来匹配除双引号之外的任何字符,用*
来表示两个引号之间可以存在任意数目的非双引号字符。
关于引号字符串,更有用(也更复杂)的定义是,两端的双引号之间可以出现由反斜线转义的双引号,例如"nail the 2\"x4\" plank"
。
2.9 表示时刻的文字,例如!“9:17am”或者“12:30pm
匹配表示时刻的文字可能有不同的严格程度。
[O-9]?[0-9]:[0-9][0-9] (am|pm)
上面语句能够匹配9:17 am或者12:30 pm,但也能匹配无意义的时刻,如99:99 pm。
首先看小时数,如果小时数是一个两位数,第一位只能是1,但是1?[0-9]
,仍然能够匹配19(也能够匹配0),所以更好的办法应该是把小时部分分为两种情况来处理,1[012]
,匹配两位数,[1-9]
匹配一位数,结果就是(1[012]|[1-9])
分钟数第一位数字应该是[0-5]
,此时第二位数字应该是[0-9]
。综合起来就是(1[012]|[1-9]):[0-5][0-9] (am|pm)
2.10 总结
第一,第二节笔记学习了以下内容:
几点注意事项:
1、各个egrep程序是有差别的。它们支持的元字符,以及这些元字符的确切含义,通常都有差别—要参考相应的文档。
2、使用括号的3个理由是:限制多选结构、分组和捕获文本。
3、字符组的特殊性在于,关于元字符的规定是完全独立于正则表达式语言“主体”的。
4、多选结构和字符组是截然不同的,它们的功能完全不同,只是在有限的情况下,它们的表现相同。
5、排除型字符组同样是一种“肯定断言”一即使它的名字里包含了“排除”两个字,它仍然需要匹配一个字符。只是因为列出的字符都会被排除,所以最终匹配的字符肯定不在列出的字符之内。
6、-i
参数很有用,它能进行忽略大小写的匹配。
7、转义有3种情况:
\
加上元字符,表示匹配元字符所使用的普通字符(例如*匹配普通的星号)。
\
加上非元字符,组成一种新的有其规定意义的元字符序列(例如,\<
表示“单词的起始边界”)。
\
加上任意其他字符,默认情况就是匹配此字符(也就是说,反斜线被忽略了)。
对大多数版本的egrep来说,字符组内部的反斜线没有任何特殊意义,所以此时它并不是一个转义字符。
8、由星号和问号限定的对象在“匹配成功”时可能并没有匹配任何字符。即使什么字符都不能匹配到,它们仍然会报告“匹配成功”。