通过本文的学习,您可以增加一些有用的设计实际正则表达式 (regexp) 的技能。构建正则表达式是任何管理员日常工作中的一部分。为了构造返回所需条件的成功正则表达式,需要学习以模式匹配的角度进行思考,而这种技能需要花大量的时间进行练习。
引言
UNIX® 管理员每天都需要构建和使用正则表达式 (regexp) 进行文本模式匹配。大多数语言都支持正则表达式的某种实现。有的应用程序(如 EMACS)具有正则表达式搜索功能,并且您可以通过各种命令行工具使用正则表达式。无论什么应用程序,构建正确的正则表达式的关键之处在于,识别仅满足需要匹配的数据的模式,以便在输入中排除其他不必要的内容。
出于这个目的,本文将逐步介绍几种正则表达式模式构建技巧,并介绍它们如何帮助您完成各种常规任务。
使用正则表达式 (regexp)
除非特别说明,否则本文中使用的示例都是扩展可移植操作系统接口(扩展 POSIX)的正则表达式。如果通过命令行(如使用 egrep 实用工具)使用它们,您应该根据需要引用各种正则表达式。请记住,不同的正则表达式实现之间存在一些区别,您可能不得不适应所使用的特定的工具、应用程序或语言中的具体实现。
匹配整行内容
^ 元字符匹配行首,而 $ 匹配行尾,如果将它们组合在一起(如 ^$),它们将匹配空行。(这个表达式的镜像,即 $^,是不可能匹配成功的,它将永远 都无法匹配到有效行。)这个基本的正则表达式是许多复杂正则表达式的基础,如果您还不习惯使用这个基本的正则表达式,那么您应该逐步养成使用它的习惯。使用它来构建匹配整行内容 的模式。
在用户字典文件 (/usr/dict/words) 中搜索是一个很好的基本模式。(有些版本的 UNIX 将用户字典放在 /usr/share/dict/words 中。)
例如,假设您忘记了如何拼写单词 fuchsia。其中是否包含 sh 或 cs 呢?您所知道的只是,它以 fu 开头并以 ia 结尾。
尝试使用这个模式进行搜索:
$ egrep -i ‘^fu.*ia$’ /usr/dict/words
-i 标志表示在搜索过程中不区分大小写。在这个示例中,因为 fuchsia 拼写正确,所以在返回的单词中包括这个单词。
根据长度匹配行
使用大括号元字符 ({ }) 指定前面的正则表达式匹配多少次,如表 1 所示。当您将它们添加到刚才介绍的整行搜索中时,您可以指定行的长度。
表 1. 大括号元字符的含义
示例 描述
{X} 这个字符对前面的正则表达式匹配 X 次。
{X,} 这个字符对前面的正则表达式匹配 X 或更多 次。
{X,Y} 这个字符对前面的正则表达式匹配至少 X 而不超过 Y 次。
并不是所有扩展正则表达式的实现都支持大括号。此外,根据具体的实现,您可能需要先使用反斜杠对其进行转义。
您可以使用这个正则表达式得到字典中以单词长度为顺序的报告。所获得结果的数目取决于本地系统的字典文件中单词的数目,然而,它应该与清单 1 所示类似。在这个示例中,最常见的单词长度是 9 个字母,该字典中有 32,380 个匹配单词。该字典中不包括 25 个字母或更长的单词,并且最长的单词并不是您认为的 21 个字母长的 disestablishmentarian(有 81 个同样长度的单词,包括 superincomprehensible 和 phoneticohieroglyphic),这个 UNIX 字典中最长的单词有 5 个,包括 pathologicopsychological。
清单 1. 计算字典中 X 个字母的单词的个数
$ for i in `seq 1 32`
> {
> echo “There are” `egrep ‘^.{‘$i’}$’ /usr/dict/words
| wc -l` “$i-letter words in the dictionary.”
> }
There are 52 1-letter words in the dictionary.
There are 155 2-letter words in the dictionary.
There are 1351 3-letter words in the dictionary.
There are 5110 4-letter words in the dictionary.
There are 9987 5-letter words in the dictionary.
There are 17477 6-letter words in the dictionary.
There are 23734 7-letter words in the dictionary.
There are 29926 8-letter words in the dictionary.
There are 32380 9-letter words in the dictionary.
There are 30867 10-letter words in the dictionary.
There are 26011 11-letter words in the dictionary.
There are 20460 12-letter words in the dictionary.
There are 14938 13-letter words in the dictionary.
There are 9762 14-letter words in the dictionary.
There are 5924 15-letter words in the dictionary.
There are 3377 16-letter words in the dictionary.
There are 1813 17-letter words in the dictionary.
There are 842 18-letter words in the dictionary.
There are 428 19-letter words in the dictionary.
There are 198 20-letter words in the dictionary.
There are 82 21-letter words in the dictionary.
There are 41 22-letter words in the dictionary.
There are 17 23-letter words in the dictionary.
There are 5 24-letter words in the dictionary.
There are 0 25-letter words in the dictionary.
There are 0 26-letter words in the dictionary.
There are 0 27-letter words in the dictionary.
There are 0 28-letter words in the dictionary.
There are 0 29-letter words in the dictionary.
There are 0 30-letter words in the dictionary.
There are 0 31-letter words in the dictionary.
There are 0 32-letter words in the dictionary.
$
匹配单词
围绕字符 < 和 > 是非常有用的模式构造器:它们将要匹配的整个单词 括起来,这表示,它们不会匹配带括号的模式,除非该模式本身就是一个单词。单词 定义为两侧由非单词字符描述的、任意数目组成单词的字符(数字、字母和下划线字符)。非单词字符包括下面的所有字符:
行首
空白字符
标点符号
行尾
任何除字母、数字或下划线以外的字符
这些围绕字符可以节省大量的时间,但是它们常常没有被充分地利用,可能是因为并非所有的正则表达式实现都支持它们。如果您的正则表达式实现支持它们,那么您应该逐步养成使用它们的习惯。
将需要单独匹配的单词括起来,如下所示:
这个示例中的正则表达式不会匹配单词 ecosystem、systemic 或 system/70,也不会匹配模式 system 出现在行中任意位置的那些行,它将仅仅 匹配 system 作为独立的单词出现的那些行。
围绕字符与圆括号中的分组结合在一起,可以用来匹配部分 单词。
要匹配包含以 pre 开头 的单词的那些行,可以使用:
<(pre).*>
前面的示例将匹配包含单词 preface 和 preposterous 的行,但不会匹配 spread 或 Dupre。
匹配重复单词
这里介绍一种使用单词围绕字符匹配重复单词的快速方法,重复单词表示一个单词在空格之后再次出现。您还可以使用逆向引用,这是大多数流行的正则表达式实现中的一种递归特性,它可以匹配模式本身的某一部分。(将模式中需要引用的部分使用圆括号括起来,然后使用反斜杠加上需要进行引用的围绕字符编号来调用逆向引用:1 表示第一个圆括号分组,2 表示第二个圆括号分组,依此类推。)
要查找重复的单词,搜索在任意数目的空格之后再次出现该单词的情况,可以通过对第一个使用圆括号的部分进行逆向引用来实现:
(<.*>)( )+
这个示例匹配缩写形式和任何类型的单词,但是它不会匹配由标点符号分隔的重复单词,如 It’s been a long, long time。
要匹配所有的重复单词,包括由空格和 任意标点符号分隔的重复单词,可以使用下面的表达式:
(<.*>).?( )+
如果需要对这些正则表达式使用 grep,则务必使用 -i 标志,以便在搜索中不区分大小写。
匹配小时
让我们再来看另外一类常见的问题:时间和日期。这里介绍了一些设计匹配正确模式的正则表达式所需要考虑的事项。
您无法搜索任何两位的数字来匹配分钟和秒,因为它们仅仅是从 0 到 59,要匹配它们,您需要使用方括号将表示十位和个位的范围括起来:
要匹配标准的 12 或 24 小时格式的小时,可以使用下面的表达式:
(([0-1]?[0-9])|([2][0-3])):([0-5][0-9])(:[0-5][0-9])?
要匹配 12 小时 AM/PM 格式、带或不带秒数的时间,甚至匹配大写或小写、不带后缀 AM 或 PM 标识符的时间,可以使用下面的表达式:
([^0-9])([0-1]?[0-9]){1}(((:([0-5]){1}([0-9]){1}){1,2})|(( )?([AP]M)|([ap]m)))?
如果在上一个示例中没有开始的否定语句,它将匹配不带冒号的时间,这将取决于输入数据,可能会匹配中波广播电台(在美国称为调幅 AM 电台),如 1450 AM。
匹配月份
匹配 12 个月中的任何月份需要一个使用 | 操作符进行分隔的列表,但有时会使用不同的方式对日期进行缩写:
要查找完整拼写或三字母缩写的 12 个月份,可以使用下面的表达式(位于一行):
Jan(uary)?|Feb(uary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|
Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?
您可以加以想象并搜索完整拼写或三字母缩写的变形,即仅当后面紧跟着一个空格或点号的情况,可以使用下面的表达式(位于一行):
Jan(uary| |.)|Feb(uary| |.)|Mar(ch| |.)|Apr(il| |.)|May( |.)|Jun(e| |.)|Jul(y| |
.)|Aug(ust| |.)|Sep(tember| |.)|Oct(ober| |.)|Nov(ember| |.)|Dec(ember| |.)
请注意,在上面的这两个示例中,May 是一个特殊的例外。在所有的月份中,它是唯一的完整拼写与三字母缩写相同的月份,所以成功的匹配必须包含这两种变形中的任何一种 作为其缩写,因此像“Mayflower”这样的单词不会导致误报。
当匹配模式前面的字符不是空格或行首时,这些示例还是会失败(返回误报的结果)。这不太可能会出现在英语散文中,但是可能出现在程序源代码中,因为其中可能使用了像 NumOct 这样的变量名。
要修复这些问题,可以执行下面的操作:
使用圆括号将整个正则表达式括起来,并在它的前面加上另一个限定符,用于匹配行首或者空格字符,如下所示(位于一行):
(^| )(Jan(uary| |.)|Feb(uary| |.)|Mar(ch| |.)|Apr(il| |.)|
May( |.)|Jun(e| |.)|Jul(y| |.)|Aug(ust| |.)|Sep(tember| |
.)|Oct(ober| |.)|Nov(ember| |.)|Dec(ember| |.))
另一种完成这个任务的方法是,在该正则表达式的前面加上一个限定符,以匹配非文字数字的字符,如下所示(位于一行):
([^A-Za-z0-9])(Jan(uary| |.)|Feb(uary| |.)|Mar(ch| |.)|
Apr(il| |.)|May( |.)|Jun(e| |.)|Jul(y| |.)|Aug(ust| |.)|
Sep(tember| |.)|Oct(ober| |.)|Nov(ember| |.)|Dec(ember| |.))
但是仍然存在潜在的问题,对于搜索整篇英文散文,这些示例并不可靠,因为它们可能返回错误的匹配结果,如“Janelle”或“Augury”这样的单词。要修复这个问题,您必须使用单词围绕字符将每个月份括起来。
本文开头提到,正确的正则表达式应该仅返回需要匹配的数据,以便在输入中排除其他不必要的内容。这种措词是经过仔细考虑的,因为对于构建正则表达式来说,这与上下文有关。对于有些情况,前面的示例非常适合,无需添加额外的单词围绕字符。在其他的情况下,可以对其进行相当程度的简化,例如,如果您正在搜索仅包含大写的日期数值数据的日志文件,那么只需要使用像 [A-S] 这样的正则表达式来匹配包含月份名称的行。
匹配日期
您可以结合一些表 1 所示的数量匹配来匹配日期。
要匹配“month, day, years”,可以使用下面的正则表达式(因为撇号字符是该正则表达式的一部分,所以必须使用双引号将它括起来,如下所示):
“[A-Za-z]{3,10}.? [0-9]{1,2}, ([0-9]{4}|’?[0-9]{2})”
这个正则表达式匹配 9 种不同的日期格式:
MONTH [D]D, YY
MONTH [D]D, ‘YY
MONTH [D]D, YYYY
MON. [D]D, YY
MON. [D]D, ‘YY
MON. [D]D, YYYY
MON [D]D, YY
MON [D]D, ‘YY
MON [D]D, YYYY
这个正则表达式的误报包括“Order 99, 99”,要消除这些误报,可以将这个正则表达式与用于月份的正则表达式结合起来,如上所述,以便能够仅匹配实际的月份名称。另外,更改数值范围以避免错误的匹配,并且通过使逗号成为可选项,重复了 18 种可能的格式。
这将得到一个很长的正则表达式。尝试下面的表达式:
“([^A-Za-z0-9])(Jan(uary| |.)|Feb(uary| |.)|Mar(ch| |.)|
Apr(il| |.)|May( |.)|Jun(e| |.)|Jul(y| |.)|Aug(ust| |.)|
Sep(tember| |.)|Oct(ober| |.)|Nov(ember| |.)|
Dec(ember| |.)) [0-3]?[0-9]{1}(,)? ([0-9]{4}|’?[0-9]{2})”
同样,根据您的需要仔细设计正则表达式。匹配模式通常比较容易,这是因为它存在于特定输入的上下文中,而不是因为它可能独立于数据集而存在。后代人将会发现,前面那个很长的正则表达式中仍然存在 Y10K 错误,因为它能匹配的最大可能的年份为 9999。
匹配整数
正如您在前几个示例中看到的,使用方括号中的范围可以很好地匹配数值。
要匹配任意长度的整数,可以在数值范围后面加上 +;要包括负值,可以在它的前面加上可选的负号(连字匹配:
-?[0-9]+
前面的例子可以匹配 0,因为 0 是指定范围中可选的字符。
对于数值匹配,使用圆括号将某些部分括起来也非常有效。要匹配任意的十进制数值,可以使用包含小数点加上一个或多个数值的可选围绕字符,以此对前面的正则表达式进行扩展:
-?[0-9]+(.[0-9]+)?
可以使用方括号指定十进制数值的小数位数。例如,要匹配小数位数为 5 或更多小数位数的正数值,可以使用下面的表达式:
[^-][0-9]+.([0-9]){5,}
更多实际的匹配
范围加上使用括号括起来的元字符,在查找符合任何特定格式的数值时非常有用。将前面介绍的一些技术结合起来,可以构建匹配各种数据的正则表达式:
要匹配美国的电话号码,可以使用:
((([2-9][0-9]{2}))? ?|[2-9][0-9]{2}(?:-?| ?))[2-9][0-9]{2}[- ]?[0-9]{4}
这个正则表达式可以匹配美国 15 种格式的电话号码:
(NPA) PRE-SUFF
(NPA) PRE SUFF
(NPA) PRESUFF
(NPA)PRE-SUFF
(NPA)PRE SUFF
(NPA)PRESUFF
NPA PRE-SUFF
NPA PRE SUFF
NPA PRESUFF
NPAPRE-SUFF
NPAPRE SUFF
NPAPRESUFF
PRE-SUFF
PRE SUFF
PRESUFF
它还可以匹配美国免费 WATS 号码,尽管 1-800 的“1-”前缀或其他的免费号码不是匹配的一部分,但它本身可以匹配 10 位的数值。对于以 1 或 1+ 开头的美国号码和任意数目的空格,也完全一样,长途电话拨号前缀本身无法匹配,但是只要它后面跟着实际的号码,这个正则表达式就能够将其找出来。
要匹配两或三位域的电子邮件地址,可以尝试下面的表达式:
<[^@]+>@[a-zA-Z_.]+?.[a-zA-Z]{2,3}
要匹配如今所有流行的 URL,可以使用下面的正则表达式:
(((http(s)?|ftp|telnet|news)://|mailto:)[^()[:space:]]+)
这个表达式可以正常运行,但是匹配 URL 并不像您想象的那么简单。匹配任何可能的 URL 的正则表达式,如 RFC 1738 中的定义,发表在“Regexp for URLs”(请参见参考资料部分)一文中,它非常巨大并且看起来令人生畏。现在应该将它合并为一个 [:url:] 类(如果有用于处理类似数据种类的各种新的类,如 [:email:],那就好了)。
结束语
本文涉及到一些用于编写正则表达式的模式构建技术,以及如何使用它们来完成管理员时常碰到的特定类型数据匹配的工作。在此过程中,向您介绍了大量有价值的实际正则表达式,您可以将它们添加到自己的管理工具库中。