正则表达式
假设您在电话簿中查找姓名,但不记得确切的拼写。您可以花费大量时间搜索所有可能的组合,除非您有一个工具可以提取与您的搜索匹配的相对较少的选项,无论您的搜索多么不完整。正则表达式就是这样一种工具。
粗略地说,正则表达式(或 regexp)是一个描述另一个字符串或一组字符串的字符串。许多应用程序可以从这种能力中获益:Perl、sed、awk、egrep,甚至 Emacs(阅读本文后尝试 Ctrl-Alt-%),仅举几例。
事实上,你们中的许多人已经使用过某种正则表达式。在 shell 命令中
ls *.pl
字符 *.pl 充当正则表达式。也就是说,它是一个字符串,描述由任意数量的任何类型字符 (*) 组成的字符串,后跟一个句点 (.),后跟两个给定的字符 (pl)。
用于组成正则表达式的标准规则集能够描述所有字符串,无论它们多么复杂。不幸的是,生活总是更加复杂。事实证明,至少存在两个不同版本的正则表达式:扩展的和基本的。此外,并非所有应用程序都支持所有可能的规则。
如果正则表达式正确地描述了给定的字符串,则称该正则表达式匹配该字符串。给定的正则表达式可以与零个到多个字符串匹配。按照惯例,正则表达式写在斜杠之间 (/.../)。在下文中,我使用扩展正则表达式。
最简单的正则表达式是普通的字母数字字符串。这样的 regexp 与包含其内容作为子字符串的所有字符串匹配。例如,考虑以下来自罗西尼我最喜欢的歌剧《灰姑娘》中的诗句:“Zitto, zitto, piano, piano, senza strepito e rumore。” regexp /piano/ 被认为与这句诗匹配,因为后者包含与 regexp 相同的字符,相同的序列。
为了更好地理解我讨论的示例,您可以尝试以下 Perl 脚本,尝试对其包含的 regexp 进行修改
#!/usr/bin/perl $verse = "Zitto, zitto, piano, piano, senza " . "strepito e rumore"; if ($verse =~ /piano/) { print "Match!\n"; } else { print "Do not match!\n"; }
在 Perl 中,运算符 =~ 比较两个正则表达式,如果它们匹配则返回“true”。
一些字符(称为元字符)不被识别为普通字符,而是用于特殊目的。例如,* 用于匹配零次或多次字符组,该字符组又由一对定义原子(或必须视为单个实体的字符组)的括号标识。regexp /( piano,)*/ 与示例诗句匹配,因为字符“ piano,”(形成一个原子)重复了两次。如果原子由单个字符组成,则可以省略括号。
* 在正则表达式中的含义与它在 shell 中的含义不同。在正则表达式中,* 是一个修饰符;它描述了左侧原子的重复次数。因此,字符串“piano”在 shell 中与 p* 匹配,但在正则表达式中不匹配:/p*/ 与 p、pp、ppp 等匹配,甚至与空字符串匹配。
要指定原子的重复次数在 N 和 M 之间,可以使用符号 {N,M}。{N} 匹配与前一个原子完全重复 N 次的字符串;{N,} 将匹配至少 N 个。因此,以下正则表达式将匹配
/( piano,){0,10}/ /( piano,){1,2}/ /( piano,){2}/
当然,第一个 regexp 也将与“ piano, piano, piano” 匹配。
元字符 + 和 ? 分别是 {1,} 和 {0,1} 的简写。
匹配的带括号原子会自动存储到特殊变量(称为反向引用)中,这些变量由符号 \ 后跟一个数字标识。正则表达式中第一次出现的带括号原子将存储在 \1 中,第二次出现的存储在 \2 中,依此类推。例如
/Z(itto), z\1, ( piano,)\2/
将与上述诗句匹配(假设 \1 = “itto”,\2 = “piano,”)。
. 元字符可以描述任何字符,因此正则表达式 /.(itto), .\1/ 既匹配“Zitto, zitto”又匹配“zitto, zitto”。但是,它甚至与“Ritto, ritto”匹配,但这没有相同的含义。为了避免过于通用,您可以指定一组可能的替代方案,在方括号中列出可能的字符
/[Zz](itto), [Zz]\1/
方括号中的短划线用于指定字符范围。例如,/[a-z]/ 匹配所有小写字符,/[A-Z]/ 匹配所有大写字符。/[a-zA-Z0-9_]/ 匹配任何字母数字字符或下划线。
元字符 | 可用于表达不同的替代方案。它的作用类似于逻辑 OR 语句。因此
/Zitto|zitto/
将同时匹配“Zitto”和“zitto”。
元字符 ^ 和 $ 分别匹配字符串的开头和结尾。如果在方括号内使用,则插入符号被解释为否定运算符。所以
/[^a-z]itto/
将匹配 Zitto,但不匹配 zitto([^a-z] 可以理解为“任何不是小写字母的字母”)。
要匹配元字符,只需在其前面放一个反斜杠 (\) 即可告诉 regexp 将其解释为普通字符。\ 字符通常称为转义字符。
为了体会正则表达式的强大功能,让我们看一个简单的 Perl 脚本,它可以帮助系统管理员查找身份验证失败。在以下示例中,我使用了相当有表现力的正则表达式来展示不同的功能。您可以编写更简单的正则表达式来描述相同的字符串。
每次有人登录失败时,syslogd 都会将消息写入 /var/log/messages,如下所示
Jul 26 16:35:25 myhost su(pam_unix)[2549]: authentication failure; logname=verdi uid=500 euid=0 tty= ruser=organtin rhost= user=root Jul 27 14:54:36 myhost login(pam_unix)[688]: authentication failure; logname=LOGIN uid=0 euid=0 tty=tty1 ruser= rhost= user=mozart
这些行列表明了登录尝试的时间、尝试以另一个用户身份登录的用户(如果可用)以及目标用户。例如,用户 verdi 尝试以 root 身份登录两次,而有人从控制台登录 mozart 失败。
考虑清单 1 中所示的 Perl 脚本。它读取 /var/log/messages 文件,然后识别看起来有趣的行,并仅提取相关信息。
首先,我们仅选择相关行,并使用第 7 行显示的正则表达式 /authentication failure/ 匹配它们。其他所有内容都将被丢弃。然后,每一行都与一个正则表达式(第 8 行)匹配,该正则表达式应理解如下:获取所有以 (^) 开头、正好包含三个 ({3}) 字母 ([a-zA-Z]) 字符的字符串,后跟一个空格,后跟最多两个 (+) 字符,这些字符可以是数字(0-9,在 Perl 中等效于元字符 \d)或空格。在一个空格之后,必须跟随任意数量 (*) 的数字或分号。到目前为止描述的字符串部分包含在括号中,因此它存储在名为 \1 的反向引用中(它是第一个)。之后,在字符串“logname=”之前可以找到任意数量的字符 (.*)。该字符串后必须跟任意数量的字母数字字符。同样,因为有一对括号,我们将它们存储在 \2 中。最后,在字符串“user=”之前可以存在任意数量的字符,后跟任意数量的字母数字字符。所有这些都存储在 \3 中。
从这个例子中,您可以看到如何从字符串中提取子字符串。您不需要知道它们的相对位置,只要您可以描述它们的外观即可。
Perl 提供了一个有用的功能来处理 regexp。在正则表达式匹配后,可以使用以反向引用命名的 Perl 变量的自动定义,如 $1、$2 等。Perl 还允许用户定义有用的符号,例如 \d 或 \w(等效于 [A-Za-z0-9_]),以及表示相同内容的符合 POSIX 的符号(有关更多信息,请参阅 man perlre)。
基本正则表达式被其他几个程序使用,例如 sed 或 egrep。
在基本正则表达式中,元字符 |、+ 和 ? 不存在,括号和花括号需要转义才能被解释为元字符。元字符 ^、$ 和 * 遵循更复杂的规则(有关更多详细信息,请参阅 man 7 regex)。但是在大多数情况下,它们的行为类似于扩展的对应项。通常方便的做法是以扩展格式表示正则表达式,然后在需要时添加转义字符。
例如,清单 2 中所示的脚本生成一个 HTML 格式的页面,以使用互联网浏览器读取系统日志文件的内容。除了回显页面标题和表格的 HTML 标签外,它还简单地列出给定目录中的文件,并将结果通过管道传递给 sed,sed 使用 regexp 对其进行转换。sed 用于文本替换的语法非常常见,类似于
s/regexp/replacement/
其中 regexp 是必须替换的正则表达式。
清单 2. 用于生成 HTML 格式页面以读取日志文件的示例脚本
本质上,该语法表示一个由九个元素组成的字符串,这些元素由适当的正则表达式正确描述。例如,[rwxds-] 要求查找第一个元素中可能存在的字符。
字符串的后半部分由字母数字字符组成,其中穿插着斜杠。您可能会注意到,在这种情况下使用的正则表达式是 (.*\/)(.*)。第一组匹配斜杠(转义的)之前的所有字符,即路径名。第二组列出所有后续字符(文件名)。路径中斜杠的数量无关紧要。事实上,正则表达式(基本正则表达式和扩展正则表达式)被认为是贪婪的——它们试图匹配尽可能多的字符。
脚本的结果被写入标准输出,并且可以重定向到给定的文件(例如,通过 cron 以固定的时间间隔),以便在 Web 上显示。

Giovanni Organtini (g.organtini@roma1.infn.it) 是罗马大学物理学家计算和编程导论教授。他使用 Linux 多年,无论是为了娱乐还是工作,在工作中,它用于大型服务器场上 CMS 实验 (cmsdoc.cern.ch) 的模拟,以及作为复杂数据采集系统和机器控制的一部分。在他的儿子 Lorenzo 出生之前,他经常旅行,寻找美食餐厅并参加音乐会和歌剧。