引言
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次。 |
并不是所有扩展正则表达式的实现都支持大括号。此外,根据具体的实现,您可能需要先使用反斜杠对其进行转义。
您可以使用这个正则表达式得到字典中以单词长度为顺序的报告。所获得结果的数目取决于本地系统的字典文件中单词的数目,然而,它应该与清单 1所示类似。在这个示例中,最常见的单词长度是 9 个字母,该字典中有 32,380 个匹配单词。该字典中不包括 25 个字母或更长的单词,并且最长的单词并不是您认为的 21 个字母长的disestablishmentarian(有 81 个同样长度的单词,包括superincomprehensible和phoneticohieroglyphic),这个 UNIX 字典中最长的单词有 5 个,包括pathologicopsychological。
清单 1. 计算字典中 X 个字母的单词的个数
匹配单词
围绕字符\<
和\>
是非常有用的模式构造器:它们将要匹配的整个单词括起来,这表示,它们不会匹配带括号的模式,除非该模式本身就是一个单词。单词定义为两侧由非单词字符描述的、任意数目组成单词的字符(数字、字母和下划线字符)。非单词字符包括下面的所有字符:
- 行首
- 空白字符
- 标点符号
- 行尾
- 任何除字母、数字或下划线以外的字符
这些围绕字符可以节省大量的时间,但是它们常常没有被充分地利用,可能是因为并非所有的正则表达式实现都支持它们。如果您的正则表达式实现支持它们,那么您应该逐步养成使用它们的习惯。
将需要单独匹配的单词括起来,如下所示:
这个示例中的正则表达式不会匹配单词ecosystem、systemic或system/70,也不会匹配模式system
出现在行中任意位置的那些行,它将仅仅匹配system作为独立的单词出现的那些行。
围绕字符与圆括号中的分组结合在一起,可以用来匹配部分单词。
要匹配包含以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]
这样的正则表达式来匹配包含月份名称的行。
匹配日期
要匹配“month,day,years”,可以使用下面的正则表达式(因为撇号字符是该正则表达式的一部分,所以必须使用双引号将它括起来,如下所示):
这个正则表达式匹配 9 种不同的日期格式:
-
MONTH [D]D,YY
-
MON. [D]D,0)!important">MON [D]D,YYYY
这个正则表达式的误报包括“Order 99,99”,要消除这些误报,可以将这个正则表达式与用于月份的正则表达式结合起来,如上所述,以便能够仅匹配实际的月份名称。另外,更改数值范围以避免错误的匹配,并且通过使逗号成为可选项,重复了 18 种可能的格式。
这将得到一个很长的正则表达式。尝试下面的表达式:
同样,根据您的需要仔细设计正则表达式。匹配模式通常比较容易,这是因为它存在于特定输入的上下文中,而不是因为它可能独立于数据集而存在。后代人将会发现,前面那个很长的正则表达式中仍然存在 Y10K 错误,因为它能匹配的最大可能的年份为 9999。
匹配整数
正如您在前几个示例中看到的,使用方括号中的范围可以很好地匹配数值。
要匹配任意长度的整数,可以在数值范围后面加上+
;要包括负值,可以在它的前面加上可选的负号(连字号)匹配:
前面的例子可以匹配 0,因为 0 是指定范围中可选的字符。
对于数值匹配,使用圆括号将某些部分括起来也非常有效。要匹配任意的十进制数值,可以使用包含小数点加上一个或多个数值的可选围绕字符,以此对前面的正则表达式进行扩展:
可以使用方括号指定十进制数值的小数位数。例如,要匹配小数位数为 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:]
,那就好了)。