内容

名称

perlretut - Perl 正则表达式教程

描述

本页面提供了一个关于理解、创建和使用 Perl 中正则表达式的基本教程。它作为正则表达式参考页面的补充 perlre。正则表达式是 m//s///qr//split 运算符的组成部分,因此本教程也与 "perlop 中的正则表达式引号类运算符""perlfunc 中的 split" 重叠。

Perl 因其在文本处理方面的卓越表现而广为人知,而正则表达式是其声名鹊起的关键因素之一。Perl 正则表达式展现出大多数其他计算机语言中所没有的效率和灵活性。即使掌握正则表达式的基础知识,也能让你以惊人的轻松程度操作文本。

什么是正则表达式?最基本地说,正则表达式是一个模板,用于确定字符串是否具有某些特征。该字符串通常是某些文本,例如一行、一句话、一个网页,甚至一整本书,但它不一定是文本。例如,它可以是二进制数据。生物学家经常使用 Perl 来寻找长 DNA 序列中的模式。

假设我们想要确定变量 $var 中的文本是否包含字符序列 m u s h r o o m(空格是为了可读性而添加的)。我们可以在 Perl 中编写

$var =~ m/mushroom/

如果 $var 在任何地方包含该字符序列,则该表达式的值为 TRUE,否则为 FALSE。用 '/' 字符括起来的这部分表示我们正在寻找的特征。我们用术语 模式 来表示它。查找模式是否出现在字符串中的过程称为 匹配"=~" 运算符以及 m// 告诉 Perl 尝试将模式与字符串匹配。请注意,模式也是一个字符串,但它是一种非常特殊的字符串,正如我们将看到的。模式在当今非常普遍;例如,在搜索引擎中输入的模式以查找网页,以及用于列出目录中文件的模式,例如,"ls *.txt" 或 "dir *.*"。在 Perl 中,正则表达式描述的模式不仅用于搜索字符串,还用于提取字符串的所需部分,以及执行搜索和替换操作。

正则表达式不应得地被认为是抽象且难以理解的。这实际上仅仅是因为用于表达它们的符号往往过于简洁和密集,而不是因为其固有的复杂性。我们建议使用 /x 正则表达式修饰符(下面描述)以及大量的空白来减少其密度,并使其更易于阅读。正则表达式是使用简单的概念(如条件语句和循环)构建的,与 Perl 语言本身中的相应 if 条件语句和 while 循环一样容易理解。

本教程通过逐一讨论正则表达式概念及其符号,并辅以大量示例,来降低学习曲线。教程的第一部分将从最简单的单词搜索开始,逐步介绍基本的正则表达式概念。如果你掌握了第一部分,你将拥有解决约 98% 需求所需的所有工具。教程的第二部分适用于那些熟悉基础知识并渴望更多强大工具的人。它将讨论更高级的正则表达式运算符,并介绍最新的尖端创新。

注意:为了节省时间,“正则表达式”通常缩写为 regexp 或 regex。Regexp 是比 regex 更自然的缩写,但更难发音。Perl pod 文档在 regexp 和 regex 上平分秋色;在 Perl 中,有多种缩写方式。在本教程中,我们将使用 regexp。

v5.22 中的新功能,use re 'strict' 在编译正则表达式模式时应用比其他情况更严格的规则。它可以找到一些虽然合法,但可能不是你想要的东西。

第一部分:基础知识

简单的单词匹配

最简单的 regexp 就是一个单词,或者更一般地说,是一个字符序列。仅包含一个单词的 regexp 匹配包含该单词的任何字符串

"Hello World" =~ /World/;  # matches

这个 Perl 语句到底是什么意思?"Hello World" 是一个简单的双引号字符串。World 是正则表达式,// 包含 /World/,告诉 Perl 在字符串中搜索匹配项。运算符 =~ 将字符串与 regexp 匹配相关联,如果 regexp 匹配则产生真值,如果 regexp 不匹配则产生假值。在我们的例子中,World 匹配 "Hello World" 中的第二个单词,所以表达式为真。这样的表达式在条件语句中很有用

if ("Hello World" =~ /World/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

这个主题有一些有用的变体。可以使用 !~ 运算符反转匹配的意义

if ("Hello World" !~ /World/) {
    print "It doesn't match\n";
}
else {
    print "It matches\n";
}

regexp 中的文字字符串可以被变量替换

my $greeting = "World";
if ("Hello World" =~ /$greeting/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

如果你要匹配默认的特殊变量 $_,可以省略 $_ =~ 部分

$_ = "Hello World";
if (/World/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

最后,匹配的 // 默认分隔符可以通过在前面加上 'm' 来更改为任意分隔符

"Hello World" =~ m!World!;   # matches, delimited by '!'
"Hello World" =~ m{World};   # matches, note the paired '{}'
"/usr/bin/perl" =~ m"/perl"; # matches after '/usr/bin',
                             # '/' becomes an ordinary char

/World/m!World!m{World} 都代表同一个东西。当例如使用引号 ('"') 作为分隔符时,正斜杠 '/' 成为普通字符,可以在这个 regexp 中毫无问题地使用。

让我们考虑一下不同的 regexp 如何匹配 "Hello World"

"Hello World" =~ /world/;  # doesn't match
"Hello World" =~ /o W/;    # matches
"Hello World" =~ /oW/;     # doesn't match
"Hello World" =~ /World /; # doesn't match

第一个正则表达式 world 无法匹配,因为正则表达式默认区分大小写。第二个正则表达式匹配成功,因为子字符串 'o W' 出现在字符串 "Hello World" 中。空格字符 ' ' 在正则表达式中与其他字符一样对待,在本例中需要匹配。第三个正则表达式 'oW' 无法匹配,原因是缺少空格字符。第四个正则表达式 "World " 无法匹配,因为正则表达式末尾有空格,而字符串末尾没有空格。这里需要注意的是,正则表达式必须完全匹配字符串的一部分,才能使语句为真。

如果正则表达式在字符串中匹配多个位置,Perl 将始终匹配字符串中最早可能出现的位置。

"Hello World" =~ /o/;       # matches 'o' in 'Hello'
"That hat is red" =~ /hat/; # matches 'hat' in 'That'

关于字符匹配,您还需要了解以下几点。首先,并非所有字符都可以在匹配中“按原样”使用。某些字符称为 *元字符*,通常保留用于正则表达式表示法。元字符是

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

此列表并不像看起来那样明确(或者在其他文档中声称的那样)。例如,"#" 仅在使用 /x 模式修饰符(下面描述)时才为元字符,而 "}""]" 仅在分别与 "{""[" 配对时才为元字符;其他情况也适用。

这些字符的意义将在本教程的其余部分中解释,但现在,您只需要知道可以通过在元字符前面加上反斜杠来匹配元字符本身。

"2+2=4" =~ /2+2/;    # doesn't match, + is a metacharacter
"2+2=4" =~ /2\+2/;   # matches, \+ is treated like an ordinary +
"The interval is [0,1)." =~ /[0,1)./     # is a syntax error!
"The interval is [0,1)." =~ /\[0,1\)\./  # matches
"#!/usr/bin/perl" =~ /#!\/usr\/bin\/perl/;  # matches

在最后一个正则表达式中,正斜杠 '/' 也被反斜杠转义,因为它用于分隔正则表达式。然而,这会导致 LTS(leaning toothpick syndrome,即“斜靠牙签综合征”),因此更改分隔符通常更易读。

"#!/usr/bin/perl" =~ m!#\!/usr/bin/perl!;  # easier to read

反斜杠字符 '\' 本身就是一个元字符,需要被反斜杠转义。

'C:\WIN32' =~ /C:\\WIN/;   # matches

在特定元字符的含义不合理的情况下,它会自动失去元字符特性,变成一个要按字面意义匹配的普通字符。例如,'}' 仅在它是 '{' 元字符的配对字符时才为元字符。否则,它将被视为一个字面意义上的右大括号。这可能会导致意外结果。 use re 'strict' 可以捕获其中一些情况。

除了元字符之外,还有一些 ASCII 字符没有可打印的字符等效项,而是由 *转义序列* 表示。常见的例子包括 \t 代表制表符,\n 代表换行符,\r 代表回车符,\a 代表响铃(或警报)。如果您的字符串更适合被视为任意字节的序列,则八进制转义序列,例如 \033,或十六进制转义序列,例如 \x1B,可能是您字节的更自然表示。以下是一些转义的示例

"1000\t2000" =~ m(0\t2)   # matches
"1000\n2000" =~ /0\n20/   # matches
"1000\t2000" =~ /\000\t2/ # doesn't match, "0" ne "\000"
"cat"   =~ /\o{143}\x61\x74/ # matches in ASCII, but a weird way
                             # to spell cat

如果您已经使用 Perl 一段时间,所有这些关于转义序列的讨论可能看起来很熟悉。双引号字符串中也使用了类似的转义序列,事实上,Perl 中的正则表达式大多被视为双引号字符串。这意味着变量也可以在正则表达式中使用。就像双引号字符串一样,正则表达式中变量的值将在正则表达式被评估以进行匹配之前被替换。因此,我们有

$foo = 'house';
'housecat' =~ /$foo/;      # matches
'cathouse' =~ /cat$foo/;   # matches
'housecat' =~ /${foo}cat/; # matches

到目前为止,一切顺利。有了上面的知识,你已经可以执行几乎任何你能想到的字面字符串正则表达式搜索了。这里有一个非常简单的 Unix grep 程序模拟

% cat > simple_grep
#!/usr/bin/perl
$regexp = shift;
while (<>) {
    print if /$regexp/;
}
^D

% chmod +x simple_grep

% simple_grep abba /usr/dict/words
Babbage
cabbage
cabbages
sabbath
Sabbathize
Sabbathizes
sabbatical
scabbard
scabbards

这个程序很容易理解。#!/usr/bin/perl 是从 shell 调用 perl 程序的标准方法。$regexp = shift; 将第一个命令行参数保存为要使用的正则表达式,并将其余的命令行参数视为文件。while (<>) 循环遍历所有文件中的所有行。对于每一行,print if /$regexp/; 如果正则表达式与该行匹配,则打印该行。在这行中,print/$regexp/ 都隐式地使用了默认变量 $_

对于上面所有的正则表达式,如果正则表达式在字符串中的任何位置匹配,则被认为是匹配。但是,有时我们希望指定正则表达式应该尝试匹配的位置。为此,我们将使用锚点元字符 '^''$'。锚点 '^' 表示匹配字符串的开头,锚点 '$' 表示匹配字符串的结尾,或者匹配字符串结尾处的换行符之前。以下是它们的用法

"housekeeper" =~ /keeper/;    # matches
"housekeeper" =~ /^keeper/;   # doesn't match
"housekeeper" =~ /keeper$/;   # matches
"housekeeper\n" =~ /keeper$/; # matches

第二个正则表达式不匹配,因为 '^'keeper 限制为仅匹配字符串的开头,但 "housekeeper" 中的 keeper 从中间开始。第三个正则表达式匹配,因为 '$'keeper 限制为仅匹配字符串的结尾。

'^''$' 同时使用时,正则表达式必须同时匹配字符串的开头和结尾,,正则表达式匹配整个字符串。考虑

"keeper" =~ /^keep$/;      # doesn't match
"keeper" =~ /^keeper$/;    # matches
""       =~ /^$/;          # ^$ matches an empty string

第一个正则表达式不匹配,因为字符串包含比keep更多的内容。由于第二个正则表达式与字符串完全一致,因此它匹配。在正则表达式中同时使用'^''$'会强制匹配整个字符串,因此您可以完全控制哪些字符串匹配,哪些字符串不匹配。假设您正在寻找一个名叫bert的家伙,他在字符串中独自存在

"dogbert" =~ /bert/;   # matches, but not what you want

"dilbert" =~ /^bert/;  # doesn't match, but ..
"bertram" =~ /^bert/;  # matches, so still not good enough

"bertram" =~ /^bert$/; # doesn't match, good
"dilbert" =~ /^bert$/; # doesn't match, good
"bert"    =~ /^bert$/; # matches, perfect

当然,对于字面字符串,您可以轻松地使用字符串比较$string eq 'bert',并且它会更有效率。^...$正则表达式在我们将下面更强大的正则表达式工具添加进来时才真正变得有用。

使用字符类

虽然人们已经可以使用上面的字面字符串正则表达式做很多事情,但我们只是触及了正则表达式技术的表面。在本节及后续章节中,我们将介绍正则表达式概念(以及相关的元字符符号),这些概念将使正则表达式不仅可以表示单个字符序列,还可以表示整个类

其中一个概念是字符类。字符类允许在正则表达式中的特定位置匹配一组可能的字符,而不仅仅是一个字符。您可以定义自己的自定义字符类。这些由方括号[...]表示,其中包含要匹配的字符集。以下是一些示例

/cat/;       # matches 'cat'
/[bcr]at/;   # matches 'bat, 'cat', or 'rat'
/item[0123456789]/;  # matches 'item0' or ... or 'item9'
"abc" =~ /[cab]/;    # matches 'a'

在最后一条语句中,即使'c'是类中的第一个字符,'a'也会匹配,因为字符串中的第一个字符位置是正则表达式可以匹配的最早点。

/[yY][eE][sS]/;      # match 'yes' in a case-insensitive way
                     # 'yes', 'Yes', 'YES', etc.

这个正则表达式展示了一个常见的任务:执行不区分大小写的匹配。Perl 提供了一种方法来避免所有这些方括号,只需在匹配的末尾附加一个'i'。然后/[yY][eE][sS]/;可以改写为/yes/i;'i'代表不区分大小写,是匹配操作的修饰符的一个例子。我们将在本教程的后面部分遇到其他修饰符。

我们在上一节中看到,有一些普通字符,它们代表自身,还有一些特殊字符,它们需要一个反斜杠'\'来代表自身。在字符类中也是如此,但字符类内部的普通字符和特殊字符集与字符类外部的不同。字符类的特殊字符是-]\^$(以及模式分隔符,无论它是什么)。']'是特殊的,因为它表示字符类的结束。'$'是特殊的,因为它表示一个标量变量。'\'是特殊的,因为它用于转义序列,就像上面一样。以下是特殊字符]$\的处理方式

/[\]c]def/; # matches ']def' or 'cdef'
$x = 'bcr';
/[$x]at/;   # matches 'bat', 'cat', or 'rat'
/[\$x]at/;  # matches '$at' or 'xat'
/[\\$x]at/; # matches '\at', 'bat, 'cat', or 'rat'

最后两个有点棘手。在[\$x]中,反斜杠保护了美元符号,因此字符类有两个成员'$''x'。在[\\$x]中,反斜杠受到保护,因此$x被视为一个变量,并在双引号方式下被替换。

特殊字符'-'在字符类中充当范围运算符,因此可以将一组连续的字符写成一个范围。使用范围,笨拙的[0123456789][abc...xyz]变成了简洁的[0-9][a-z]。以下是一些示例

/item[0-9]/;  # matches 'item0' or ... or 'item9'
/[0-9bx-z]aa/;  # matches '0aa', ..., '9aa',
                # 'baa', 'xaa', 'yaa', or 'zaa'
/[0-9a-fA-F]/;  # matches a hexadecimal digit
/[0-9a-zA-Z_]/; # matches a "word" character,
                # like those in a Perl variable name

如果 `'-'` 是字符类中的第一个或最后一个字符,则将其视为普通字符;`[-ab]`、`[ab-]` 和 `[a\-b]` 都是等效的。

字符类第一个位置的特殊字符 `'^'` 表示一个 *否定字符类*,它匹配除括号中字符之外的任何字符。`[...]` 和 `[^...]` 都必须匹配一个字符,否则匹配失败。然后

/[^a]at/;  # doesn't match 'aat' or 'at', but matches
           # all other 'bat', 'cat, '0at', '%at', etc.
/[^0-9]/;  # matches a non-numeric character
/[a^]at/;  # matches 'aat' or '^at'; here '^' is ordinary

现在,即使 `[0-9]` 也可能需要多次编写,为了节省按键次数并使正则表达式更易读,Perl 为常见的字符类提供了一些缩写,如下所示。自引入 Unicode 以来,除非使用 `/a` 修饰符,否则这些字符类匹配的字符不仅仅是 ASCII 范围内的几个字符。

/a 修饰符(从 Perl 5.14 开始可用)用于将 `\d`、`\s` 和 `\w` 的匹配限制在 ASCII 范围内。当您只想处理类似英语的文本时,它有助于防止您的程序不必要地暴露于完整的 Unicode(及其伴随的安全问题)。(“a” 可以加倍,`/aa`,以提供更多限制,防止 ASCII 与非 ASCII 字符进行不区分大小写的匹配;否则 Unicode “开尔文符号” 将不区分大小写地匹配 “k” 或 “K”。)

\d\s\w\D\S\W 缩写可以在方括号字符类内部和外部使用。以下是一些示例

/\d\d:\d\d:\d\d/; # matches a hh:mm:ss time format
/[\d\s]/;         # matches any digit or whitespace character
/\w\W\w/;         # matches a word char, followed by a
                  # non-word char, followed by a word char
/..rt/;           # matches any two chars, followed by 'rt'
/end\./;          # matches 'end.'
/end[.]/;         # same thing, matches 'end.'

由于句点是一个元字符,因此需要对其进行转义才能匹配为普通句点。例如,由于 `\d` 和 `\w` 是字符集,因此将 `[^\d\w]` 视为 `[\D\W]` 是不正确的;事实上,`[^\d\w]` 与 `[^\w]` 相同,与 `[\W]` 相同。想想德摩根定律。

实际上,句点和 `\d\s\w\D\S\W` 缩写本身就是字符类类型,因此用方括号括起来的字符类只是其中一种类型。当我们需要区分时,我们称它们为“方括号字符类”。

在基本的正则表达式中,一个有用的锚点是词语锚点 \b。它匹配一个词语字符和一个非词语字符之间的边界 \w\W\W\w

$x = "Housecat catenates house and cat";
$x =~ /cat/;    # matches cat in 'housecat'
$x =~ /\bcat/;  # matches cat in 'catenates'
$x =~ /cat\b/;  # matches cat in 'housecat'
$x =~ /\bcat\b/;  # matches 'cat' at end of string

请注意,在最后一个示例中,字符串的结尾被视为词语边界。

对于自然语言处理(例如,将撇号包含在词语中),请使用 \b{wb} 代替。

"don't" =~ / .+? \b{wb} /x;  # matches the whole string

你可能想知道为什么 '.' 匹配除 "\n" 之外的所有字符 - 为什么不匹配所有字符?原因是,通常情况下,我们会匹配行,并且希望忽略换行符。例如,虽然字符串 "\n" 代表一行,但我们希望将其视为空行。然后

""   =~ /^$/;    # matches
"\n" =~ /^$/;    # matches, $ anchors before "\n"

""   =~ /./;      # doesn't match; it needs a char
""   =~ /^.$/;    # doesn't match; it needs a char
"\n" =~ /^.$/;    # doesn't match; it needs a char other than "\n"
"a"  =~ /^.$/;    # matches
"a\n"  =~ /^.$/;  # matches, $ anchors before "\n"

这种行为很方便,因为我们通常希望在计算和匹配一行中的字符时忽略换行符。但是,有时我们希望跟踪换行符。我们甚至可能希望 '^''$' 锚定在字符串中的行首和行尾,而不仅仅是字符串的开头和结尾。Perl 允许我们通过使用 /s/m 修饰符来选择忽略或关注换行符。/s/m 代表单行和多行,它们决定字符串是作为一个连续的字符串处理,还是作为一组行处理。这两个修饰符影响正则表达式解释的两个方面:1) '.' 字符类如何定义,以及 2) 锚点 '^''$' 在哪里可以匹配。以下是四种可能的组合

以下是 /s/m 的示例

$x = "There once was a girl\nWho programmed in Perl\n";

$x =~ /^Who/;   # doesn't match, "Who" not at start of string
$x =~ /^Who/s;  # doesn't match, "Who" not at start of string
$x =~ /^Who/m;  # matches, "Who" at start of second line
$x =~ /^Who/sm; # matches, "Who" at start of second line

$x =~ /girl.Who/;   # doesn't match, "." doesn't match "\n"
$x =~ /girl.Who/s;  # matches, "." matches "\n"
$x =~ /girl.Who/m;  # doesn't match, "." doesn't match "\n"
$x =~ /girl.Who/sm; # matches, "." matches "\n"

大多数情况下,默认行为是我们想要的,但 /s/m 有时非常有用。如果使用 /m,字符串的开头仍然可以使用 \A 匹配,字符串的结尾仍然可以使用锚点 \Z(匹配结尾和之前的换行符,如 '$')和 \z(仅匹配结尾)匹配。

$x =~ /^Who/m;   # matches, "Who" at start of second line
$x =~ /\AWho/m;  # doesn't match, "Who" is not at start of string

$x =~ /girl$/m;  # matches, "girl" at end of first line
$x =~ /girl\Z/m; # doesn't match, "girl" is not at end of string

$x =~ /Perl\Z/m; # matches, "Perl" is at newline before end
$x =~ /Perl\z/m; # doesn't match, "Perl" is not at end of string

我们现在知道如何在正则表达式中创建字符类之间的选择。那么词语或字符字符串之间的选择呢?下一节将介绍这种选择。

匹配这个或那个

有时我们希望正则表达式能够匹配不同的可能单词或字符字符串。这可以通过使用交替元字符'|'来实现。要匹配dogcat,我们形成正则表达式dog|cat。和以前一样,Perl 将尝试在字符串中尽早匹配正则表达式。在每个字符位置,Perl 将首先尝试匹配第一个备选方案dog。如果dog不匹配,Perl 将尝试下一个备选方案cat。如果cat也不匹配,则匹配失败,Perl 将移动到字符串中的下一个位置。一些例子

"cats and dogs" =~ /cat|dog|bird/;  # matches "cat"
"cats and dogs" =~ /dog|cat|bird/;  # matches "cat"

即使dog是第二个正则表达式中的第一个备选方案,cat也能够在字符串中更早地匹配。

"cats"          =~ /c|ca|cat|cats/; # matches "c"
"cats"          =~ /cats|cat|ca|c/; # matches "cats"

在这里,所有备选方案都在第一个字符串位置匹配,因此第一个备选方案是匹配的方案。如果一些备选方案是其他备选方案的截断,请将最长的备选方案放在前面,以便它们有机会匹配。

"cab" =~ /a|b|c/ # matches "c"
                 # /a|b|c/ == /[abc]/

最后一个例子指出,字符类就像字符的交替。在给定的字符位置,允许正则表达式匹配成功的第一个备选方案将是匹配的方案。

分组和层次匹配

交替允许正则表达式在备选方案中进行选择,但它本身并不令人满意。原因是每个备选方案都是一个完整的正则表达式,但有时我们只希望正则表达式的一部分有备选方案。例如,假设我们要搜索家猫或管家。正则表达式housecat|housekeeper符合要求,但效率低下,因为我们不得不输入两次house。如果正则表达式的某些部分是常量,比如house,而某些部分有备选方案,比如cat|keeper,那就太好了。

分组元字符()解决了这个问题。分组允许正则表达式的部分被视为一个单元。正则表达式的部分通过用括号括起来进行分组。因此,我们可以通过将正则表达式形成为house(cat|keeper)来解决housecat|housekeeper。正则表达式house(cat|keeper)表示匹配house,后面跟着catkeeper。以下是一些其他示例

/(a|b)b/;    # matches 'ab' or 'bb'
/(ac|b)b/;   # matches 'acb' or 'bb'
/(^a|b)c/;   # matches 'ac' at start of string or 'bc' anywhere
/(a|[bc])d/; # matches 'ad', 'bd', or 'cd'

/house(cat|)/;  # matches either 'housecat' or 'house'
/house(cat(s|)|)/;  # matches either 'housecats' or 'housecat' or
                    # 'house'.  Note groups can be nested.

/(19|20|)\d\d/;  # match years 19xx, 20xx, or the Y2K problem, xx
"20" =~ /(19|20|)\d\d/;  # matches the null alternative '()\d\d',
                         # because '20\d\d' can't match

交替在组内和组外的行为方式相同:在给定的字符串位置,允许正则表达式匹配的最左侧备选方案将被采用。因此,在最后一个示例中,在第一个字符串位置,"20"匹配第二个备选方案,但没有剩余的内容来匹配接下来的两位数字\d\d。因此,Perl 转移到下一个备选方案,即空备选方案,并且该方案有效,因为"20"是两位数。

尝试一个备选方案,查看是否匹配,然后继续下一个备选方案,如果它不匹配,则从上一个备选方案尝试的位置回溯到字符串中,这个过程称为回溯。术语“回溯”源于这样一个想法:匹配正则表达式就像在树林里散步。成功匹配正则表达式就像到达目的地。有许多可能的起点,每个起点对应一个字符串位置,并且每个起点都按顺序从左到右尝试。从每个起点可能有多条路径,其中一些路径可以带你到达目的地,而另一些路径则是死胡同。当你沿着一条小路走,走到死胡同的时候,你必须沿着小路回溯到之前的某个点,尝试另一条小路。如果你到达了目的地,你立即停止,不再尝试其他所有的小路。你坚持不懈,只有当你尝试了所有起点的所有可能路径,但没有到达目的地时,你才会宣告失败。为了具体说明,以下是 Perl 在尝试匹配正则表达式时所做操作的逐步分析。

"abcde" =~ /(abd|abc)(df|d|de)/;
  1. 从字符串中的第一个字母 'a' 开始。

  2. 尝试第一个组中的第一个备选方案 'abd'

  3. 匹配 'a' 后面跟着 'b'。到目前为止一切顺利。

  4. 正则表达式中的 'd' 与字符串中的 'c' 不匹配 - 死胡同。因此,回溯两个字符,选择第一个组中的第二个备选方案 'abc'

  5. 匹配 'a' 后面跟着 'b' 后面跟着 'c'。我们正在顺利进行,并且已经满足了第一个组。将 $1 设置为 'abc'

  6. 继续第二个组,并选择第一个备选方案 'df'

  7. 匹配 'd'

  8. 正则表达式中的 'f' 与字符串中的 'e' 不匹配,因此是死胡同。回溯一个字符,选择第二个组中的第二个备选方案 'd'

  9. 'd' 匹配。第二个分组已满足,因此将 $2 设置为 'd'

  10. 我们已经到达正则表达式的末尾,因此我们完成了!我们已经从字符串 "abcde" 中匹配了 'abcd'

关于这个分析,需要注意两点。首先,第二个组中的第三个备选方案 'de' 也允许匹配,但我们在到达它之前就停止了 - 在给定的字符位置,最左边的匹配优先。其次,我们能够在字符串 'a' 的第一个字符位置获得匹配。如果在第一个位置没有匹配,Perl 将移动到第二个字符位置 'b',并再次尝试匹配。只有当所有可能路径在所有可能字符位置都尝试完之后,Perl 才会放弃,并宣告 $string =~ /(abd|abc)(df|d|de)/; 为假。

即使经过了所有这些工作,正则表达式匹配仍然非常快。为了加快速度,Perl 将正则表达式编译成一个紧凑的 opcode 序列,这些序列通常可以放入处理器缓存中。当代码执行时,这些 opcode 可以全速运行并非常快速地进行搜索。

提取匹配项

分组元字符 () 还有另一个完全不同的功能:它们允许提取匹配字符串的各个部分。这对于找出匹配的内容以及一般的文本处理非常有用。对于每个分组,匹配的内部部分将进入特殊变量 $1$2 等。它们可以像普通变量一样使用。

    # extract hours, minutes, seconds
    if ($time =~ /(\d\d):(\d\d):(\d\d)/) {    # match hh:mm:ss format
	$hours = $1;
	$minutes = $2;
	$seconds = $3;
    }

现在,我们知道在标量上下文中,$time =~ /(\d\d):(\d\d):(\d\d)/ 返回真或假值。然而,在列表上下文中,它返回匹配值的列表 ($1,$2,$3)。因此,我们可以将代码更紧凑地写成

# extract hours, minutes, seconds
($hours, $minutes, $second) = ($time =~ /(\d\d):(\d\d):(\d\d)/);

如果正则表达式中的分组是嵌套的,$1 获取具有最左侧开括号的分组,$2 获取下一个开括号,依此类推。这是一个带有嵌套分组的正则表达式

/(ab(cd|ef)((gi)|j))/;
 1  2      34

如果此正则表达式匹配,$1 包含以 'ab' 开头的字符串,$2 设置为 'cd''ef'$3 等于 'gi''j'$4 设置为 'gi'(与 $3 相同),或者保持未定义。

为了方便起见,Perl 将 $+ 设置为由最高编号的 $1$2 等持有的字符串(并且,与之相关的,$^N 设置为 $1$2 等的最近分配的值;即与匹配中使用的最右侧闭括号关联的 $1$2 等)。

反向引用

与匹配变量$1$2等密切相关的是反向引用\g1\g2等。反向引用只是可以在正则表达式内部使用的匹配变量。这是一个非常棒的功能;正则表达式中后面的匹配结果可以依赖于正则表达式中前面的匹配结果。假设我们想要在文本中查找重复的单词,例如“the the”。以下正则表达式可以找到所有中间有空格的 3 个字母的重复单词

/\b(\w\w\w)\s\g1\b/;

分组为\g1分配了一个值,以便在两个部分中使用相同的 3 个字母序列。

类似的任务是查找由两个相同部分组成的单词

% simple_grep '^(\w\w\w\w|\w\w\w|\w\w|\w)\g1$' /usr/dict/words
beriberi
booboo
coco
mama
murmur
papa

正则表达式有一个分组,它考虑 4 个字母的组合,然后是 3 个字母的组合,等等,并使用\g1查找重复。虽然$1\g1代表同一个东西,但应注意仅在正则表达式外部使用匹配变量$1$2等,仅在正则表达式内部使用反向引用\g1\g2等;如果不这样做,可能会导致令人惊讶和不令人满意的结果。

相对反向引用

一旦有多个捕获组,就很难通过计算开括号的数量来获得反向引用的正确编号。Perl 5.10 提供了一种更方便的技术:相对反向引用。要引用紧接在前的捕获组,现在可以写\g-1\g{-1},倒数第二个可以通过\g-2\g{-2}获得,以此类推。

除了可读性和可维护性之外,使用相对反向引用的另一个很好的理由是以下示例,其中使用了一个简单的模式来匹配奇特的字符串

$a99a = '([a-z])(\d)\g2\g1';   # matches a11a, g22g, x33x, etc.

现在我们已经将此模式存储为一个方便的字符串,我们可能会想将其用作其他模式的一部分

$line = "code=e99e";
if ($line =~ /^(\w+)=$a99a$/){   # unexpected behavior!
    print "$1 is valid\n";
} else {
    print "bad line: '$line'\n";
}

但这并不匹配,至少不像人们期望的那样。只有在插入插值的$a99a并查看生成的正则表达式的完整文本后,反向引用失效才变得明显。子表达式(\w+)抢走了编号 1,并将$a99a中的组降级了一级。这可以通过使用相对反向引用来避免

$a99a = '([a-z])(\d)\g{-1}\g{-2}';  # safe for being interpolated

命名反向引用

Perl 5.10 还引入了命名捕获组和命名反向引用。要将名称附加到捕获组,可以写(?<name>...)(?'name'...)。然后可以将反向引用写为\g{name}。允许将同一个名称附加到多个组,但只能引用同名组中最左边的组。在模式之外,可以通过%+哈希访问命名捕获组。

假设我们需要匹配可能以三种格式之一给出的日历日期 yyyy-mm-dd、mm/dd/yyyy 或 dd.mm.yyyy,我们可以编写三个合适的模式,其中我们分别使用'd''m''y' 作为捕获日期相关组件的组的名称。匹配操作将这三个模式组合为备选方案。

$fmt1 = '(?<y>\d\d\d\d)-(?<m>\d\d)-(?<d>\d\d)';
$fmt2 = '(?<m>\d\d)/(?<d>\d\d)/(?<y>\d\d\d\d)';
$fmt3 = '(?<d>\d\d)\.(?<m>\d\d)\.(?<y>\d\d\d\d)';
for my $d (qw(2006-10-21 15.01.2007 10/31/2005)) {
    if ( $d =~ m{$fmt1|$fmt2|$fmt3} ){
        print "day=$+{d} month=$+{m} year=$+{y}\n";
    }
}

如果任何备选方案匹配,则哈希%+ 将包含三个键值对。

备选捕获组编号

另一种捕获组编号技术(也来自 Perl 5.10)处理了在备选方案集中引用组的问题。考虑一个用于匹配一天时间(民用或军用风格)的模式

if ( $time =~ /(\d\d|\d):(\d\d)|(\d\d)(\d\d)/ ){
    # process hour and minute
}

处理结果需要一个额外的 if 语句来确定$1$2 还是 $3$4 包含有用信息。如果我们可以在第二个备选方案中也使用组号 1 和 2,这将更容易,这正是围绕备选方案设置的括号结构(?|...) 所实现的。以下是先前模式的扩展版本

if($time =~ /(?|(\d\d|\d):(\d\d)|(\d\d)(\d\d))\s+([A-Z][A-Z][A-Z])/){
    print "hour=$1 minute=$2 zone=$3\n";
}

在备选编号组中,组号从每个备选方案的相同位置开始。在组之后,编号继续从所有备选方案中达到的最大值加 1 开始。

位置信息

除了匹配的内容之外,Perl 还提供匹配内容的位置作为@-@+ 数组的内容。$-[0] 是整个匹配开始的位置,$+[0] 是结束的位置。类似地,$-[n]$n 匹配开始的位置,$+[n] 是结束的位置。如果$n 未定义,则$-[n]$+[n] 也是未定义的。然后这段代码

$x = "Mmm...donut, thought Homer";
$x =~ /^(Mmm|Yech)\.\.\.(donut|peas)/; # matches
foreach $exp (1..$#-) {
    no strict 'refs';
    print "Match $exp: '$$exp' at position ($-[$exp],$+[$exp])\n";
}

打印

Match 1: 'Mmm' at position (0,3)
Match 2: 'donut' at position (6,11)

即使正则表达式中没有分组,也仍然可以找出字符串中匹配的具体内容。如果使用它们,Perl 会将 `$` 设置为匹配部分之前的字符串部分,将 `$&` 设置为匹配的字符串部分,并将 `$'` 设置为匹配部分之后的字符串部分。示例

$x = "the cat caught the mouse";
$x =~ /cat/;  # $` = 'the ', $& = 'cat', $' = ' caught the mouse'
$x =~ /the/;  # $` = '', $& = 'the', $' = ' cat caught the mouse'

在第二次匹配中,`$` 等于 `''`,因为正则表达式从字符串的第一个字符位置开始匹配并停止;它从未看到第二个 "the"。

如果您的代码要在 5.20 之前的 Perl 版本上运行,值得注意的是使用 `$` 和 `$'` 会使正则表达式匹配速度变慢很多,而 `$&` 会使速度变慢一些,因为如果它们在一个程序中的一个正则表达式中使用,它们会为程序中的所有正则表达式生成。因此,如果您的应用程序的目标是原始性能,则应避免使用它们。如果您需要提取相应的子字符串,请改用 `@-` 和 `@+`

$` is the same as substr( $x, 0, $-[0] )
$& is the same as substr( $x, $-[0], $+[0]-$-[0] )
$' is the same as substr( $x, $+[0] )

从 Perl 5.10 开始,可以使用 `${^PREMATCH}`、`${^MATCH}` 和 `${^POSTMATCH}` 变量。这些变量只有在存在 `/p` 修饰符时才会设置。因此,它们不会影响程序的其余部分。在 Perl 5.20 中,无论是否使用 `/p`(修饰符会被忽略),`${^PREMATCH}`、`${^MATCH}` 和 `${^POSTMATCH}` 都可用,并且 `$`、`$'` 和 `$&` 不会造成任何速度差异。

非捕获分组

一个用于捆绑一组备选方案的组,可能有用也可能没有用作为捕获组。如果不是,它只会为可用捕获组值的集合创建一个多余的添加,在正则表达式内部和外部。非捕获分组,用 `(?:regexp)` 表示,仍然允许将正则表达式视为一个单元,但不会同时建立捕获组。捕获分组和非捕获分组都可以在同一个正则表达式中共存。由于没有提取,非捕获分组比捕获分组更快。非捕获分组对于选择正则表达式的哪些部分要提取到匹配变量也很有用

# match a number, $1-$4 are set, but we only want $1
/([+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)/;

# match a number faster , only $1 is set
/([+-]?\ *(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;

# match a number, get $1 = whole number, $2 = exponent
/([+-]?\ *(?:\d+(?:\.\d*)?|\.\d+)(?:[eE]([+-]?\d+))?)/;

非捕获分组对于从拆分操作中删除由于某种原因需要括号的讨厌元素也很有用

$x = '12aba34ba5';
@num = split /(a|b)+/, $x;    # @num = ('12','a','34','a','5')
@num = split /(?:a|b)+/, $x;  # @num = ('12','34','5')

在 Perl 5.22 及更高版本中,可以使用新的 `/n` 标志将正则表达式中的所有组设置为非捕获

"hello" =~ /(hi|hello)/n; # $1 is not set!

有关更多信息,请参见 "n" in perlre

匹配重复

前面部分的例子展示了一个令人讨厌的弱点。我们只匹配了 3 个字母的单词,或者最多 4 个字母的单词片段。我们希望能够匹配任何长度的单词,或者更一般地说,匹配任何长度的字符串,而无需像 \w\w\w\w|\w\w\w|\w\w|\w 那样写出冗长的备选方案。

这正是量词元字符 '?''*''+'{} 被创建出来解决的问题。它们允许我们限定我们认为匹配的正则表达式部分的重复次数。量词紧跟在我们要指定的字符、字符类或分组之后。它们具有以下含义

如果你愿意,可以在花括号内添加空格(制表符或空格字符),但必须紧挨着它们,以及/或者紧挨着逗号(如果有的话)。

以下是一些例子

/[a-z]+\s+\d*/;  # match a lowercase word, at least one space, and
                 # any number of digits
/(\w+)\s+\g1/;    # match doubled words of arbitrary length
/y(es)?/i;       # matches 'y', 'Y', or a case-insensitive 'yes'
$year =~ /^\d{2,4}$/;  # make sure year is at least 2 but not more
                       # than 4 digits
$year =~ /^\d{ 2, 4 }$/;    # Same; for those who like wide open
                            # spaces.
$year =~ /^\d{2, 4}$/;      # Same.
$year =~ /^\d{4}$|^\d{2}$/; # better match; throw out 3-digit dates
$year =~ /^\d{2}(\d{2})?$/; # same thing written differently.
                            # However, this captures the last two
                            # digits in $1 and the other does not.

% simple_grep '^(\w+)\g1$' /usr/dict/words   # isn't this easier?
beriberi
booboo
coco
mama
murmur
papa

对于所有这些量词,Perl 将尝试匹配尽可能多的字符串,同时仍然允许正则表达式成功。因此,对于 /a?.../,Perl 将首先尝试匹配包含 'a' 的正则表达式;如果失败,Perl 将尝试匹配不包含 'a' 的正则表达式。对于量词 '*',我们得到以下结果

$x = "the cat in the hat";
$x =~ /^(.*)(cat)(.*)$/; # matches,
                         # $1 = 'the '
                         # $2 = 'cat'
                         # $3 = ' in the hat'

这正是我们预期的结果,匹配找到了字符串中唯一的 cat 并锁定它。但是,考虑一下这个正则表达式

$x =~ /^(.*)(at)(.*)$/; # matches,
                        # $1 = 'the cat in the h'
                        # $2 = 'at'
                        # $3 = ''   (0 characters match)

人们最初可能会猜到 Perl 会找到 cat 中的 at 并停在那里,但这不会为第一个量词 .* 提供最长的可能字符串。相反,第一个量词 .* 会尽可能多地获取字符串,同时仍然使正则表达式匹配。在这个例子中,这意味着包含 at 序列,以及字符串中的最后一个 at。这里说明的另一个重要原则是,当正则表达式中存在两个或多个元素时,最左侧的量词(如果有的话)将尽可能多地获取字符串,留下剩余的正则表达式来争夺剩余部分。因此,在我们的例子中,第一个量词 .* 获取了大部分字符串,而第二个量词 .* 获取了空字符串。尽可能多地获取字符串的量词被称为最大匹配贪婪量词。

当正则表达式可以以多种不同的方式匹配字符串时,我们可以使用上述原则来预测正则表达式将以哪种方式匹配

如上所述,原则 0 优先于其他原则。正则表达式将尽早匹配,其他原则决定正则表达式在该最早字符位置如何匹配。

以下是一个这些原则应用的示例

$x = "The programming republic of Perl";
$x =~ /^(.+)(e|r)(.*)$/;  # matches,
                          # $1 = 'The programming republic of Pe'
                          # $2 = 'r'
                          # $3 = 'l'

此正则表达式在字符串的最早位置 'T' 处匹配。人们可能会认为,'e' 作为交替表达式中最左侧的元素,应该被匹配,但 'r' 在第一个量词中产生了最长的字符串。

$x =~ /(m{1,2})(.*)$/;  # matches,
                        # $1 = 'mm'
                        # $2 = 'ing republic of Perl'

这里,最早可能的匹配是在 programming 中的第一个 'm' 处。m{1,2} 是第一个量词,因此它可以匹配最大长度的 mm

$x =~ /.*(m{1,2})(.*)$/;  # matches,
                          # $1 = 'm'
                          # $2 = 'ing republic of Perl'

这里,正则表达式在字符串的开头匹配。第一个量词 .* 尽可能多地匹配,只留下一个 'm' 给第二个量词 m{1,2}

$x =~ /(.?)(m{1,2})(.*)$/;  # matches,
                            # $1 = 'a'
                            # $2 = 'mm'
                            # $3 = 'ing republic of Perl'

这里,.? 在字符串中最早可能的位置 'a'(在 programming 中)处匹配其最大的一个字符,留下 m{1,2} 的机会匹配两个 'm'。最后,

"aXXXb" =~ /(X*)/; # matches with $1 = ''

因为它可以在字符串开头匹配零个 'X'。如果你一定要匹配至少一个 'X',使用 X+,而不是 X*

有时贪婪不是好事。有时,我们希望量词匹配一个最小的字符串片段,而不是一个最大的片段。为此,Larry Wall 创建了最小匹配非贪婪量词 ??*?+?{}?。这些是通常的量词,后面附加了一个 '?'。它们具有以下含义

让我们看看上面的例子,但使用最小量词。

$x = "The programming republic of Perl";
$x =~ /^(.+?)(e|r)(.*)$/; # matches,
                          # $1 = 'Th'
                          # $2 = 'e'
                          # $3 = ' programming republic of Perl'

允许字符串开头 '^' 和交替匹配的最小字符串是 Th,其中交替 e|r 匹配 'e'。第二个量词 .* 可以自由地吞噬字符串的其余部分。

$x =~ /(m{1,2}?)(.*?)$/;  # matches,
                          # $1 = 'm'
                          # $2 = 'ming republic of Perl'

此正则表达式可以匹配的第一个字符串位置是在 programming 中的第一个 'm'。在这个位置,最小 m{1,2}? 只匹配一个 'm'。虽然第二个量词 .*? 宁愿不匹配任何字符,但它受到字符串结尾锚点 '$' 的约束,必须匹配字符串的其余部分。

$x =~ /(.*?)(m{1,2}?)(.*)$/;  # matches,
                              # $1 = 'The progra'
                              # $2 = 'm'
                              # $3 = 'ming republic of Perl'

在这个正则表达式中,您可能期望第一个最小量词 .*? 匹配空字符串,因为它没有受到 '^' 锚点的约束来匹配单词的开头。但是,这里适用原则 0。因为整个正则表达式有可能在字符串开头匹配,所以它 *将* 在字符串开头匹配。因此,第一个量词必须匹配到第一个 'm' 之前的所有内容。第二个最小量词只匹配一个 'm',第三个量词匹配字符串的其余部分。

$x =~ /(.??)(m{1,2})(.*)$/;  # matches,
                             # $1 = 'a'
                             # $2 = 'mm'
                             # $3 = 'ing republic of Perl'

就像在前面的正则表达式中一样,第一个量词 .?? 最早可以在位置 'a' 匹配,所以它就匹配了。第二个量词是贪婪的,所以它匹配 mm,第三个匹配字符串的其余部分。

我们可以修改上面的原则 3 来考虑非贪婪量词。

就像交替一样,量词也容易受到回溯的影响。以下是该示例的逐步分析。

$x = "the cat in the hat";
$x =~ /^(.*)(at)(.*)$/; # matches,
                        # $1 = 'the cat in the h'
                        # $2 = 'at'
                        # $3 = ''   (0 matches)
  1. 从字符串中的第一个字母 't' 开始。

  2. 第一个量词 '.*' 从匹配整个字符串 "the cat in the hat" 开始。

  3. 正则表达式元素 'at' 中的 'a' 与字符串的结尾不匹配。回溯一个字符。

  4. 正则表达式元素 'at' 中的 'a' 仍然与字符串的最后一个字母 't' 不匹配,所以再回溯一个字符。

  5. 现在我们可以匹配'a''t'

  6. 继续到第三个元素'.*'。由于我们已经到达字符串的末尾,并且'.*'可以匹配0次,因此将其分配为空字符串。

  7. 我们完成了!

大多数情况下,所有这些向前和向后回溯都很快发生,搜索速度很快。但是,有一些病态的正则表达式,其执行时间会随着字符串大小呈指数增长。一个典型的结构会让你陷入困境,其形式为

/(a|b+)*/;

问题在于嵌套的不确定量词。在'+''*'之间,将长度为n的字符串划分为不同的部分有许多不同的方法:b+长度为n的一次重复,第一次b+长度为k,第二次长度为n-k的两次重复,m次重复,其位数加起来为长度n,等等。事实上,将字符串划分为其长度的函数,有指数级数量的划分方法。正则表达式可能会在过程中很幸运地匹配,但如果没有匹配,Perl将在放弃之前尝试所有可能性。因此,请谨慎使用嵌套的'*'{n,m}'+'。Jeffrey Friedl 的著作《精通正则表达式》对这个问题和其他效率问题进行了精彩的讨论。

占有量词

在不懈地寻找匹配的过程中进行回溯可能是浪费时间,尤其是在匹配注定会失败的情况下。考虑以下简单的模式

/^\w+\s+\w+$/; # a word, spaces, a word

每当将其应用于不完全符合模式预期的字符串(例如 "abc ""abc def ")时,正则表达式引擎将回溯,大约每个字符回溯一次。但我们知道,没有办法绕过使用所有初始单词字符来匹配第一次重复,所有空格必须被中间部分吞噬,第二次单词也是如此。

Perl 5.10 引入了占有量词,它提供了一种方法来指示正则表达式引擎不要回溯,方法是在通常的量词后面添加一个'+'。这使得它们既贪婪又吝啬;一旦它们成功,它们就不会放弃任何东西来允许另一个解决方案。它们具有以下含义

这些占有量词代表了一个更一般概念的特殊情况,即独立子表达式,见下文。

作为一个占有量词适用的示例,我们考虑匹配出现在几种编程语言中的引号字符串。反斜杠用作转义字符,表示下一个字符将被逐字解释,作为字符串中的另一个字符。因此,在开头的引号之后,我们期望一个(可能为空)的备选序列:要么是除未转义的引号或反斜杠之外的任何字符,要么是转义字符。

/"(?:[^"\\]++|\\.)*+"/;

构建正则表达式

在这一点上,我们已经涵盖了所有基本的正则表达式概念,所以让我们举一个更复杂的正则表达式示例。我们将构建一个匹配数字的正则表达式。

构建正则表达式的第一个任务是决定我们想要匹配什么以及我们想要排除什么。在我们的例子中,我们想要匹配整数和浮点数,并且我们想要拒绝任何不是数字的字符串。

下一个任务是将问题分解成更小的、易于转换为正则表达式的子问题。

最简单的情况是整数。它们由一系列数字组成,前面可能有一个符号。我们可以用 \d+ 表示数字,用 [+-] 表示符号。因此,整数正则表达式为

/[+-]?\d+/;  # matches integers

浮点数可能有一个符号、一个整数部分、一个小数点、一个小数部分和一个指数。这些部分中的一部分或多部分是可选的,因此我们需要检查不同的可能性。格式正确的浮点数包括 123.、0.345、.34、-1e6 和 25.4E-72。与整数一样,前面的符号是完全可选的,可以用 [+-]? 匹配。我们可以看到,如果没有指数,浮点数必须有一个小数点,否则它们就是整数。我们可能会倾向于用 \d*\.\d* 对它们进行建模,但这也会匹配单个小数点,这不是一个数字。因此,没有指数的浮点数的三种情况是

/[+-]?\d+\./;  # 1., 321., etc.
/[+-]?\.\d+/;  # .1, .234, etc.
/[+-]?\d+\.\d+/;  # 1.0, 30.56, etc.

这些可以通过三向交替组合成一个正则表达式

/[+-]?(\d+\.\d+|\d+\.|\.\d+)/;  # floating point, no exponent

在这个交替中,将'\d+\.\d+'放在'\d+\.'之前非常重要。如果'\d+\.'排在前面,正则表达式将很乐意匹配它并忽略数字的小数部分。

现在考虑带有指数的浮点数。这里关键的观察是,整数和带有小数点的数字都可以在指数前面。然后指数,就像整体符号一样,与我们是否匹配带有或不带有小数点的数字无关,并且可以从尾数中“解耦”。正则表达式的整体形式现在变得清晰了

/^(optional sign)(integer | f.p. mantissa)(optional exponent)$/;

指数是一个'e''E',后面跟着一个整数。所以指数正则表达式是

/[eE][+-]?\d+/;  # exponent

将所有部分组合在一起,我们得到一个匹配数字的正则表达式

/^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;  # Ta da!

像这样的长正则表达式可能会给你的朋友留下深刻印象,但可能难以破译。在像这样的复杂情况下,匹配的/x修饰符非常宝贵。它允许你在正则表达式中放入几乎任意的空格和注释,而不会影响它们的含义。使用它,我们可以将我们的“扩展”正则表达式改写成更令人愉悦的形式

/^
   [+-]?         # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+\.\d+  # mantissa of the form a.b
      |\d+\.     # mantissa of the form a.
      |\.\d+     # mantissa of the form .b
      |\d+       # integer of the form a
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

如果空格大多无关紧要,那么如何在扩展正则表达式中包含空格字符呢?答案是反斜杠它'\ '或将其放在字符类中[ ]。对于井号也是一样:使用\#[#]。例如,Perl 允许在符号和尾数或整数之间留有空格,我们可以将它添加到我们的正则表达式中,如下所示

/^
   [+-]?\ *      # first, match an optional sign *and space*
   (             # then match integers or f.p. mantissas:
       \d+\.\d+  # mantissa of the form a.b
      |\d+\.     # mantissa of the form a.
      |\.\d+     # mantissa of the form .b
      |\d+       # integer of the form a
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

在这种形式下,更容易看到简化交替的方法。备选方案 1、2 和 4 都以\d+开头,因此可以将其分解出来

/^
   [+-]?\ *      # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+       # start out with a ...
       (
           \.\d* # mantissa of the form a.b or a.
       )?        # ? takes care of integers of the form a
      |\.\d+     # mantissa of the form .b
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

从 Perl v5.26 开始,指定/xx会更改模式的方括号部分,以忽略制表符和空格字符,除非它们被反斜杠转义。因此,我们可以写

/^
   [ + - ]?\ *   # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+       # start out with a ...
       (
           \.\d* # mantissa of the form a.b or a.
       )?        # ? takes care of integers of the form a
      |\.\d+     # mantissa of the form .b
   )
   ( [ e E ] [ + - ]? \d+ )?  # finally, optionally match an exponent
$/xx;

这并没有真正提高这个例子的可读性,但如果你需要它,它就在那里。将模式压缩成紧凑的形式,我们有

/^[+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;

这是我们最终的正则表达式。回顾一下,我们通过以下步骤构建了一个正则表达式:

这些也是编写计算机程序时通常涉及的步骤。这非常有道理,因为正则表达式本质上是用一种小型的计算机语言编写的程序,它指定了模式。

在 Perl 中使用正则表达式

第一部分的最后一个主题简要介绍了正则表达式如何在 Perl 程序中使用。它们在 Perl 语法中处于什么位置?

我们已经介绍了匹配运算符的默认形式/regexp/和任意分隔符m!regexp!形式。我们已经使用了绑定运算符=~及其否定!~来测试字符串匹配。与匹配运算符相关联,我们讨论了单行/s、多行/m、不区分大小写/i和扩展/x修饰符。关于匹配运算符,你可能还想了解一些其他内容。

禁止替换

如果在第一次替换发生后更改了$pattern,Perl 将忽略它。如果您根本不希望进行任何替换,请使用特殊分隔符m''

@pattern = ('Seuss');
while (<>) {
    print if m'@pattern';  # matches literal '@pattern', not 'Seuss'
}

类似于字符串,m'' 在正则表达式中充当撇号;所有其他'm' 分隔符充当引号。如果正则表达式计算结果为空字符串,则使用最后一次成功匹配中的正则表达式。所以我们有

"dog" =~ /d/;  # 'd' matches
"dogbert" =~ //;  # this matches the 'd' regexp used before

全局匹配

我们将在这里讨论的最后两个修饰符/g/c 与多个匹配有关。修饰符/g 代表全局匹配,并允许匹配运算符在字符串中尽可能多次匹配。在标量上下文中,针对字符串的连续调用将使/g 从一个匹配跳到另一个匹配,在它进行的过程中跟踪字符串中的位置。您可以使用pos() 函数获取或设置位置。

以下示例显示了/g 的用法。假设我们有一个由空格分隔的单词组成的字符串。如果我们事先知道有多少个单词,我们可以使用分组提取单词

$x = "cat dog house"; # 3 words
$x =~ /^\s*(\w+)\s+(\w+)\s+(\w+)\s*$/; # matches,
                                       # $1 = 'cat'
                                       # $2 = 'dog'
                                       # $3 = 'house'

但是,如果我们有不定数量的单词呢?这就是/g 的用武之地。要提取所有单词,请形成简单的正则表达式(\w+) 并使用/(\w+)/g 循环遍历所有匹配项

while ($x =~ /(\w+)/g) {
    print "Word is $1, ends at position ", pos $x, "\n";
}

打印

Word is cat, ends at position 3
Word is dog, ends at position 7
Word is house, ends at position 13

匹配失败或更改目标字符串将重置位置。如果您不希望在匹配失败后重置位置,请添加/c,如/regexp/gc 中所示。字符串中的当前位置与字符串相关联,而不是与正则表达式相关联。这意味着不同的字符串具有不同的位置,并且它们各自的位置可以独立设置或读取。

在列表上下文中,/g 返回匹配分组的列表,或者如果没有分组,则返回对整个正则表达式的匹配列表。因此,如果我们只想要单词,我们可以使用

@words = ($x =~ /(\w+)/g);  # matches,
                            # $words[0] = 'cat'
                            # $words[1] = 'dog'
                            # $words[2] = 'house'

/g 修饰符密切相关的是\G 锚点。\G 锚点匹配上一个/g 匹配结束的位置。\G 允许我们轻松地进行上下文敏感匹配

$metric = 1;  # use metric units
...
$x = <FILE>;  # read in measurement
$x =~ /^([+-]?\d+)\s*/g;  # get magnitude
$weight = $1;
if ($metric) { # error checking
    print "Units error!" unless $x =~ /\Gkg\./g;
}
else {
    print "Units error!" unless $x =~ /\Glbs\./g;
}
$x =~ /\G\s+(widget|sprocket)/g;  # continue processing

/g\G 的组合允许我们一次处理一部分字符串,并使用任意的 Perl 逻辑来决定下一步该做什么。目前,\G 锚点仅在用于锚定到模式开头时才完全受支持。

\G 在使用正则表达式处理固定长度记录时也非常宝贵。假设我们有一段编码区域 DNA,编码为碱基对字母ATCGTTGAAT...,我们想要找到所有终止密码子TGA。在编码区域中,密码子是 3 个字母的序列,因此我们可以将 DNA 片段视为 3 个字母记录的序列。天真的正则表达式

# expanded, this is "ATC GTT GAA TGC AAA TGA CAT GAC"
$dna = "ATCGTTGAATGCAAATGACATGAC";
$dna =~ /TGA/;

不起作用;它可能匹配一个TGA,但不能保证匹配与密码子边界对齐,例如,子字符串GTT GAA会匹配。更好的解决方案是

while ($dna =~ /(\w\w\w)*?TGA/g) {  # note the minimal *?
    print "Got a TGA stop codon at position ", pos $dna, "\n";
}

它会打印

Got a TGA stop codon at position 18
Got a TGA stop codon at position 23

位置 18 很好,但位置 23 是错误的。发生了什么?

答案是我们的正则表达式在遇到最后一个真实匹配之前工作得很好。然后正则表达式将无法匹配同步的TGA,并开始一次向前移动一个字符位置,这不是我们想要的。解决方案是使用\G将匹配锚定到密码子对齐

while ($dna =~ /\G(\w\w\w)*?TGA/g) {
    print "Got a TGA stop codon at position ", pos $dna, "\n";
}

它会打印

Got a TGA stop codon at position 18

这是正确答案。这个例子说明了不仅要匹配想要的内容,还要拒绝不想要的内容的重要性。

(还有其他可用的正则表达式修饰符,例如/o,但它们的使用范围超出了本介绍的范围。)

搜索和替换

正则表达式在 Perl 中的搜索和替换操作中也起着重要作用。搜索和替换是通过s///运算符完成的。一般形式是s/regexp/replacement/modifiers,其中所有我们知道的关于正则表达式和修饰符的内容也适用于这种情况。替换是一个 Perl 双引号字符串,它用与regexp匹配的任何内容替换字符串。运算符=~也用于将字符串与s///关联。如果与$_匹配,则可以删除$_ =~。如果匹配,s///返回进行的替换次数;否则返回 false。以下是一些示例

$x = "Time to feed the cat!";
$x =~ s/cat/hacker/;   # $x contains "Time to feed the hacker!"
if ($x =~ s/^(Time.*hacker)!$/$1 now!/) {
    $more_insistent = 1;
}
$y = "'quoted words'";
$y =~ s/^'(.*)'$/$1/;  # strip single quotes,
                       # $y contains "quoted words"

在最后一个例子中,整个字符串都匹配了,但只有单引号内的部分被分组。使用 `s///` 运算符,匹配的变量 `$1`、`$2` 等立即可用于替换表达式,因此我们使用 `$1` 将引号字符串替换为仅引号内的内容。使用全局修饰符 `s///g` 将搜索并替换字符串中所有出现的正则表达式。

$x = "I batted 4 for 4";
$x =~ s/4/four/;   # doesn't do it all:
                   # $x contains "I batted four for 4"
$x = "I batted 4 for 4";
$x =~ s/4/four/g;  # does it all:
                   # $x contains "I batted four for four"

如果您在本教程中更喜欢使用 "regex" 而不是 "regexp",您可以使用以下程序进行替换。

% cat > simple_replace
#!/usr/bin/perl
$regexp = shift;
$replacement = shift;
while (<>) {
    s/$regexp/$replacement/g;
    print;
}
^D

% simple_replace regexp regex perlretut.pod

在 `simple_replace` 中,我们使用 `s///g` 修饰符来替换每行中所有出现的正则表达式。(即使正则表达式出现在循环中,Perl 也足够聪明,只编译一次。)与 `simple_grep` 一样,`print` 和 `s/$regexp/$replacement/g` 都隐式地使用了 `$_`。

如果您不想让 `s///` 改变您的原始变量,您可以使用非破坏性替换修饰符 `s///r`。这会改变行为,使 `s///r` 返回最终替换的字符串(而不是替换次数)。

$x = "I like dogs.";
$y = $x =~ s/dogs/cats/r;
print "$x $y\n";

该示例将打印 "I like dogs. I like cats"。请注意,原始的 `$x` 变量没有受到影响。替换的总体结果存储在 `$y` 中。如果替换没有影响任何内容,则返回原始字符串。

$x = "I like dogs.";
$y = $x =~ s/elephants/cougars/r;
print "$x $y\n"; # prints "I like dogs. I like dogs."

`s///r` 标志允许的另一件有趣的事情是链接替换。

$x = "Cats are great.";
print $x =~ s/Cats/Dogs/r =~ s/Dogs/Frogs/r =~
    s/Frogs/Hedgehogs/r, "\n";
# prints "Hedgehogs are great."

专门用于搜索和替换的修饰符是 `s///e` 评估修饰符。`s///e` 将替换文本视为 Perl 代码,而不是双引号字符串。代码返回的值将替换匹配的子字符串。`s///e` 在您需要在替换文本的过程中进行一些计算时很有用。此示例计算一行中的字符频率。

$x = "Bill the cat";
$x =~ s/(.)/$chars{$1}++;$1/eg; # final $1 replaces char with itself
print "frequency of '$_' is $chars{$_}\n"
    foreach (sort {$chars{$b} <=> $chars{$a}} keys %chars);

它会打印

frequency of ' ' is 2
frequency of 't' is 2
frequency of 'l' is 2
frequency of 'B' is 1
frequency of 'c' is 1
frequency of 'e' is 1
frequency of 'h' is 1
frequency of 'i' is 1
frequency of 'a' is 1

与匹配 `m//` 运算符一样,`s///` 可以使用其他分隔符,例如 `s!!!` 和 `s{}{} `,甚至 `s{}//`。如果使用单引号 `s'''`,则正则表达式和替换将被视为单引号字符串,并且没有变量替换。在列表上下文中,`s///` 返回与标量上下文中相同的内容,即匹配次数。

split 函数

`split()` 函数是另一个使用正则表达式的地方。`split /regexp/, string, limit` 将 `string` 操作数分成一个子字符串列表并返回该列表。正则表达式必须被设计为匹配构成所需子字符串分隔符的任何内容。`limit`(如果存在)将拆分限制为不超过 `limit` 个字符串。例如,要将字符串拆分为单词,请使用

$x = "Calvin and Hobbes";
@words = split /\s+/, $x;  # $word[0] = 'Calvin'
                           # $word[1] = 'and'
                           # $word[2] = 'Hobbes'

如果使用空正则表达式 `//`,则正则表达式始终匹配,并且字符串将拆分为单个字符。如果正则表达式有分组,则结果列表还包含来自分组的匹配子字符串。例如,

$x = "/usr/bin/perl";
@dirs = split m!/!, $x;  # $dirs[0] = ''
                         # $dirs[1] = 'usr'
                         # $dirs[2] = 'bin'
                         # $dirs[3] = 'perl'
@parts = split m!(/)!, $x;  # $parts[0] = ''
                            # $parts[1] = '/'
                            # $parts[2] = 'usr'
                            # $parts[3] = '/'
                            # $parts[4] = 'bin'
                            # $parts[5] = '/'
                            # $parts[6] = 'perl'

由于 `$x` 的第一个字符与正则表达式匹配,因此 `split` 在列表前面添加了一个空的初始元素。

如果你已经读到这里,恭喜你!你现在已经掌握了使用正则表达式解决各种文本处理问题的基本工具。如果这是你第一次接触本教程,不妨在这里停下来,玩一玩正则表达式……第二部分涉及正则表达式的更深奥的方面,这些概念在开始时并不需要。

第二部分:强大工具

好的,你已经了解了正则表达式的基础知识,并且想要了解更多。如果匹配正则表达式类似于在树林里散步,那么第一部分中讨论的工具类似于地形图和指南针,是我们一直使用的基本工具。第二部分中的大多数工具类似于信号枪和卫星电话。它们在徒步旅行中并不经常使用,但当我们陷入困境时,它们却非常宝贵。

以下是 Perl 正则表达式更高级、使用频率较低或有时比较深奥的功能。在第二部分中,我们将假设你已经熟悉了基础知识,并专注于高级功能。

关于字符、字符串和字符类的更多信息

有一些转义序列和字符类我们还没有介绍。

有几个转义序列可以将字符或字符串在大小写之间转换,它们也可以在模式中使用。\l\u 分别将下一个字符转换为小写或大写

$x = "perl";
$string =~ /\u$x/;  # matches 'Perl' in $string
$x = "M(rs?|s)\\."; # note the double backslash
$string =~ /\l$x/;  # matches 'mr.', 'mrs.', and 'ms.',

\L\U 表示持续的案例转换,直到被 \E 终止或被另一个 \U\L 覆盖

$x = "This word is in lower case:\L SHOUT\E";
$x =~ /shout/;       # matches
$x = "I STILL KEYPUNCH CARDS FOR MY 360";
$x =~ /\Ukeypunch/;  # matches punch card string

如果没有 \E,则将转换大小写,直到字符串结束。正则表达式 \L\u$word\u\L$word$word 的第一个字符转换为大写,其余字符转换为小写。(在 ASCII 字符之外,它会变得更加复杂;\u 实际上执行标题映射,对于大多数字符来说,它与大写相同,但并非所有字符都相同;请参阅 https://unicode.org/faq/casemap_charprop.html#4。)

控制字符可以用 \c 转义,因此控制-Z 字符将与 \cZ 匹配。转义序列 \Q...\E 引用或保护大多数非字母字符。例如,

$x = "\QThat !^*&%~& cat!";
$x =~ /\Q!^*&%~&\E/;  # check for rough language

它不保护'$''@',因此仍然可以替换变量。

\Q\L\l\U\u\E实际上是双引号语法的一部分,而不是正则表达式语法的本身。如果它们出现在直接嵌入程序中的正则表达式中,它们将起作用,但如果它们包含在字符串中并在模式中进行插值,则不会起作用。

Perl 正则表达式可以处理的不仅仅是标准的 ASCII 字符集。Perl 支持Unicode,这是一个用于表示世界上几乎所有书面语言的字母表以及大量符号的标准。Perl 的文本字符串是 Unicode 字符串,因此它们可以包含值(代码点或字符编号)大于 255 的字符。

这对正则表达式意味着什么?好吧,正则表达式用户不需要了解 Perl 对字符串的内部表示。但他们确实需要知道 1) 如何在正则表达式中表示 Unicode 字符,以及 2) 匹配操作将把要搜索的字符串视为字符序列,而不是字节序列。对 1) 的答案是,大于chr(255)的 Unicode 字符使用\x{hex}表示法表示,因为\xXY(没有花括号,XY 是两个十六进制数字)不会超过 255。(从 Perl 5.14 开始,如果你喜欢八进制,你也可以使用\o{oct}。)

/\x{263a}/;   # match a Unicode smiley face :)
/\x{ 263a }/; # Same

注意:在 Perl 5.6.0 中,过去需要说use utf8才能使用任何 Unicode 功能。现在不再是这种情况:对于几乎所有 Unicode 处理,不需要显式utf8 pragma。(唯一重要的情况是,如果你的 Perl 脚本是 Unicode 并且以 UTF-8 编码,那么需要显式use utf8。)

找出你想要的 Unicode 字符的十六进制序列或破译别人的十六进制 Unicode 正则表达式,就像用机器代码编程一样有趣。因此,指定 Unicode 字符的另一种方法是使用命名字符转义序列\N{name}name 是 Unicode 字符的名称,如 Unicode 标准中所指定。例如,如果我们想表示或匹配行星水星的天文符号,我们可以使用

$x = "abc\N{MERCURY}def";
$x =~ /\N{MERCURY}/;   # matches
$x =~ /\N{ MERCURY }/; # Also matches

也可以使用“简短”名称

print "\N{GREEK SMALL LETTER SIGMA} is called sigma.\n";
print "\N{greek:Sigma} is an upper-case sigma.\n";

你还可以通过指定charnames pragma 来限制名称到某个字母表

use charnames qw(greek);
print "\N{sigma} is Greek sigma\n";

Unicode 联盟在线提供字符名称索引,https://www.unicode.org/charts/charindex.html;包含指向其他资源链接的解释性材料,位于 https://www.unicode.org/standard/where

从 Perl v5.32 开始,可以使用\N{...}的替代方法来表示完整名称,即

/\p{Name=greek small letter sigma}/

\p{}中使用字符名称时,大小写无关紧要,大多数空格、下划线和连字符也是如此。(一些异常字符会导致始终忽略所有字符的问题。详细信息(当你更熟练时可以查找,如果需要的话)在https://www.unicode.org/reports/tr44/tr44-24.html#UAX44-LM2中。)

对要求 2) 的答案是,正则表达式(大多数情况下)使用 Unicode 字符。“大多数”是出于混乱的向后兼容性原因,但从 Perl 5.14 开始,在use feature 'unicode_strings'(在use v5.12或更高版本范围内自动打开)范围内编译的任何正则表达式都将把“大多数”变成“始终”。如果你想正确处理 Unicode,你应该确保'unicode_strings'已打开。在内部,它使用 UTF-8 或本机 8 位编码(取决于字符串的历史记录)编码为字节,但在概念上它是一个字符序列,而不是字节序列。有关这方面的教程,请参阅perlunitut

现在让我们讨论 Unicode 字符类,通常称为“字符属性”。它们由 `\p{name}` 转义序列表示。它的否定是 `\P{name}`。例如,要匹配大小写字符,

$x = "BOB";
$x =~ /^\p{IsUpper}/;   # matches, uppercase char class
$x =~ /^\P{IsUpper}/;   # doesn't match, char class sans uppercase
$x =~ /^\p{IsLower}/;   # doesn't match, lowercase char class
$x =~ /^\P{IsLower}/;   # matches, char class sans lowercase

(“`Is`” 是可选的。)

有许多 Unicode 字符属性。完整的列表请参见 perluniprops。大多数属性都有更短的同义词,也列在那里。一些同义词是一个字符。对于这些,你可以省略括号。例如,`\pM` 与 `\p{Mark}` 相同,表示诸如重音符号之类的东西。

Unicode `\p{Script}` 和 `\p{Script_Extensions}` 属性用于将每个 Unicode 字符分类到它所使用的语言脚本中。例如,英语、法语和其他一些欧洲语言都使用拉丁字母。但也有希腊字母、泰语字母、片假名字母等等。(`Script` 是 `Script_Extensions` 的一个较旧、不太先进的形式,仅出于向后兼容性而保留。)你可以测试一个字符是否在特定的脚本中,例如 `\p{Latin}`、`\p{Greek}` 或 `\p{Katakana}`。要测试它是否不在巴厘语脚本中,可以使用 `\P{Balinese}`。(这些都在内部使用 `Script_Extensions`,因为这会产生更好的结果。)

到目前为止,我们描述的是 `\p{...}` 字符类的单一形式。还有一种复合形式,你可能会遇到。它们看起来像 `\p{name=value}` 或 `\p{name:value}`(等号和冒号可以互换使用)。这些比单一形式更通用,事实上,大多数单一形式只是 Perl 定义的常用复合形式的快捷方式。例如,上一段中的脚本示例可以等效地写成 `\p{Script_Extensions=Latin}`、`\p{Script_Extensions:Greek}`、`\p{script_extensions=katakana}` 和 `\P{script_extensions=balinese}`(`{}` 括号之间的字母大小写无关紧要)。你可能永远不需要使用复合形式,但有时是必要的,使用它们可以使你的代码更容易理解。

\X 是一个表示 Unicode *扩展字形簇* 字符类的缩写。它代表一个“逻辑字符”:看起来像单个字符,但实际上可能由多个字符组成。例如,使用 Unicode 全名,例如 "A + COMBINING RING" 是一个字形簇,包含基字符 "A" 和组合字符 "COMBINING RING",在丹麦语中翻译为带圆圈的 "A",就像 Ångstrom 这个词一样。

有关 Unicode 的完整最新信息,请参阅最新的 Unicode 标准或 Unicode 联盟网站 https://www.unicode.org

除了所有这些类之外,Perl 还定义了 POSIX 风格的字符类。它们的形式为 [:name:],其中 name 是 POSIX 类的名称。POSIX 类包括 alphaalnumasciicntrldigitgraphlowerprintpunctspaceupperxdigit,以及两个扩展,word(匹配 \w 的 Perl 扩展)和 blank(GNU 扩展)。/a 修饰符将这些类限制在仅匹配 ASCII 范围内的字符;否则,它们可以匹配与相应的 Perl Unicode 类相同的字符:[:upper:]\p{IsUpper} 相同,等等。(这里有一些例外和陷阱;有关完整讨论,请参阅 perlrecharclass。)[:digit:][:word:][:space:] 分别对应于熟悉的 \d\w\s 字符类。要否定 POSIX 类,在名称前面加上 '^',例如 [:^digit:] 对应于 \D,在 Unicode 下对应于 \P{IsDigit}。Unicode 和 POSIX 字符类可以像 \d 一样使用,但 POSIX 字符类只能在字符类内部使用。

/\s+[abc[:digit:]xyz]\s*/;  # match a,b,c,x,y,z, or a digit
/^=item\s[[:digit:]]/;      # match '=item',
                            # followed by a space and a digit
/\s+[abc\p{IsDigit}xyz]\s+/;  # match a,b,c,x,y,z, or a digit
/^=item\s\p{IsDigit}/;        # match '=item',
                              # followed by a space and a digit

呼!这就是所有其他字符和字符类。

编译和保存正则表达式

在第一部分中,我们提到 Perl 将正则表达式编译成紧凑的 opcode 序列。因此,编译后的正则表达式是一个数据结构,可以存储一次并反复使用。正则表达式引号 qr// 正是做到了这一点:qr/string/string 编译成正则表达式,并将结果转换为可以分配给变量的形式。

$reg = qr/foo+bar?/;  # reg contains a compiled regexp

然后 $reg 可以用作正则表达式。

$x = "fooooba";
$x =~ $reg;     # matches, just like /foo+bar?/
$x =~ /$reg/;   # same thing, alternate form

$reg 也可以插入到更大的正则表达式中。

$x =~ /(abc)?$reg/;  # still matches

与匹配运算符一样,正则表达式引号可以使用不同的分隔符,例如 qr!!qr{}qr~~。使用单引号作为分隔符 (qr'') 将禁止任何插值。

预编译的正则表达式对于创建动态匹配很有用,这些匹配不需要在每次遇到时都重新编译。使用预编译的正则表达式,我们编写了一个 grep_step 程序,该程序搜索一系列模式,一旦满足一个模式,就前进到下一个模式。

% cat > grep_step
#!/usr/bin/perl
# grep_step - match <number> regexps, one after the other
# usage: multi_grep <number> regexp1 regexp2 ... file1 file2 ...

$number = shift;
$regexp[$_] = shift foreach (0..$number-1);
@compiled = map qr/$_/, @regexp;
while ($line = <>) {
    if ($line =~ /$compiled[0]/) {
        print $line;
        shift @compiled;
        last unless @compiled;
    }
}
^D

% grep_step 3 shift print last grep_step
$number = shift;
        print $line;
        last unless @compiled;

将预编译的正则表达式存储在数组 @compiled 中,使我们能够简单地循环遍历正则表达式,而无需任何重新编译,从而在不牺牲速度的情况下获得灵活性。

运行时组合正则表达式

回溯比使用不同的正则表达式进行重复尝试更有效。如果存在多个正则表达式,并且与其中任何一个匹配都是可以接受的,那么可以将它们组合成一组备选方案。如果各个表达式是输入数据,则可以通过编程连接操作来完成。我们将在改进版的 simple_grep 程序中利用这个想法:一个匹配多个模式的程序。

% cat > multi_grep
#!/usr/bin/perl
# multi_grep - match any of <number> regexps
# usage: multi_grep <number> regexp1 regexp2 ... file1 file2 ...

$number = shift;
$regexp[$_] = shift foreach (0..$number-1);
$pattern = join '|', @regexp;

while ($line = <>) {
    print $line if $line =~ /$pattern/;
}
^D

% multi_grep 2 shift for multi_grep
$number = shift;
$regexp[$_] = shift foreach (0..$number-1);

有时,从要分析的输入中构建模式并使用匹配操作左侧的允许值是有利的。作为这种情况的示例,假设我们的输入包含一个命令动词,它应该匹配一组可用命令动词中的一个,并且有一个额外的变化,即命令可以缩写,只要给定的字符串是唯一的。下面的程序演示了基本算法。

% cat > keymatch
#!/usr/bin/perl
$kwds = 'copy compare list print';
while( $cmd = <> ){
    $cmd =~ s/^\s+|\s+$//g;  # trim leading and trailing spaces
    if( ( @matches = $kwds =~ /\b$cmd\w*/g ) == 1 ){
        print "command: '@matches'\n";
    } elsif( @matches == 0 ){
        print "no such command: '$cmd'\n";
    } else {
        print "not unique: '$cmd' (could be one of: @matches)\n";
    }
}
^D

% keymatch
li
command: 'list'
co
not unique: 'co' (could be one of: copy compare)
printer
no such command: 'printer'

我们不是尝试将输入与关键字匹配,而是将关键字的组合集与输入匹配。模式匹配操作 $kwds =~ /\b($cmd\w*)/g 同时做了几件事。它确保给定的命令从关键字开始的地方开始 (\b)。它由于添加的 \w* 而容忍缩写。它告诉我们匹配的数量 (scalar @matches) 以及实际匹配的所有关键字。你几乎不能要求更多。

在正则表达式中嵌入注释和修饰符

从本节开始,我们将讨论 Perl 的一组扩展模式。这些是传统正则表达式语法的扩展,为模式匹配提供了强大的新工具。我们已经看到了以最小匹配结构形式的扩展,例如 ??*?+?{n,m}?{n,}?{,n}?。大多数以下扩展的形式为 (?char...),其中 char 是一个字符,它决定了扩展的类型。

第一个扩展是嵌入式注释 (?#text)。这将注释嵌入到正则表达式中,而不影响其含义。注释的文本中不应包含任何闭合括号。一个例子是

/(?# Match an integer:)[+-]?\d+/;

这种注释风格在很大程度上已被 /x 修饰符允许的原始、自由形式的注释所取代。

大多数修饰符,例如 /i/m/s/x(或任何组合),也可以使用 (?i)(?m)(?s)(?x) 嵌入到正则表达式中。例如,

/(?i)yes/;  # match 'yes' case insensitively
/yes/i;     # same thing
/(?x)(          # freeform version of an integer regexp
         [+-]?  # match an optional sign
         \d+    # match a sequence of digits
     )
/x;

嵌入式修饰符比通常的修饰符有两个重要的优势。嵌入式修饰符允许为每个正则表达式模式设置自定义的修饰符集。这对于匹配必须具有不同修饰符的正则表达式数组非常有用。

$pattern[0] = '(?i)doctor';
$pattern[1] = 'Johnson';
...
while (<>) {
    foreach $patt (@pattern) {
        print if /$patt/;
    }
}

第二个优势是嵌入式修饰符(除了 /p,它修改整个正则表达式)只影响包含嵌入式修饰符的组内的正则表达式。因此,分组可用于局部化修饰符的影响。

/Answer: ((?i)yes)/;  # matches 'Answer: yes', 'Answer: YES', etc.

嵌入式修饰符也可以通过使用例如(?-i) 来关闭任何已存在的修饰符。修饰符也可以组合成一个表达式,例如(?s-i) 打开单行模式并关闭大小写不敏感。

嵌入式修饰符也可以添加到非捕获分组中。(?i-m:regexp) 是一个非捕获分组,它以不区分大小写的方式匹配 regexp 并关闭多行模式。

前瞻和后顾

本节涉及前瞻和后顾断言。首先,一些背景知识。

在 Perl 正则表达式中,大多数正则表达式元素在匹配时会“吃掉”一定量的字符串。例如,正则表达式元素 [abc] 在匹配时会吃掉字符串中的一个字符,这意味着 Perl 在匹配后会移动到字符串中的下一个字符位置。但是,有一些元素在匹配时不会吃掉字符(前进字符位置)。我们到目前为止看到的例子是锚点。锚点 '^' 匹配行的开头,但不会吃掉任何字符。类似地,词边界锚点 \b 匹配 \w 匹配的字符与不匹配的字符相邻的位置,但它本身不会吃掉任何字符。锚点是零宽度断言的例子:零宽度,因为它们不消耗任何字符;断言,因为它们测试字符串的某些属性。在我们关于正则表达式匹配的森林漫步类比中,大多数正则表达式元素会让我们沿着小路前进,但锚点会让我们停下来观察周围的环境。如果本地环境符合要求,我们可以继续前进。但如果本地环境不满足我们的要求,我们必须回溯。

检查环境需要向前或向后查看,或两者兼顾。'^' 向后查看,以确保前面没有字符。'$' 向前查看,以确保后面没有字符。\b 同时向前和向后查看,以确保两侧的字符在“单词性”上有所不同。

前瞻和后顾断言是锚点概念的推广。前瞻和后顾是零宽度断言,它们允许我们指定要测试的字符。前瞻断言用(?=regexp)表示,或者(从 5.32 版本开始,在 5.28 版本中实验性地使用)(*pla:regexp)(*positive_lookahead:regexp);后顾断言用(?<=fixed-regexp)表示,或者(从 5.32 版本开始,在 5.28 版本中实验性地使用)(*plb:fixed-regexp)(*positive_lookbehind:fixed-regexp)。一些例子如下:

$x = "I catch the housecat 'Tom-cat' with catnip";
$x =~ /cat(*pla:\s)/;   # matches 'cat' in 'housecat'
@catwords = ($x =~ /(?<=\s)cat\w+/g);  # matches,
                                       # $catwords[0] = 'catch'
                                       # $catwords[1] = 'catnip'
$x =~ /\bcat\b/;  # matches 'cat' in 'Tom-cat'
$x =~ /(?<=\s)cat(?=\s)/; # doesn't match; no isolated 'cat' in
                          # middle of $x

请注意,这些括号是非捕获的,因为它们是零宽度断言。因此,在第二个正则表达式中,捕获的子字符串是整个正则表达式本身的子字符串。前瞻可以匹配任意正则表达式,但 5.30 版本之前的后顾(?<=fixed-regexp) 仅适用于固定宽度的正则表达式,即固定数量的字符长度。因此,(?<=(ab|bc)) 是可以的,但 5.30 版本之前的 (?<=(ab)*) 则不行。

前瞻和后顾断言的否定版本分别用(?!regexp)(?<!fixed-regexp) 表示。或者,从 5.32 版本开始(在 5.28 版本中实验性地使用),(*nla:regexp)(*negative_lookahead:regexp)(*nlb:regexp)(*negative_lookbehind:regexp)。如果正则表达式不匹配,则它们的值为真。

$x = "foobar";
$x =~ /foo(?!bar)/;  # doesn't match, 'bar' follows 'foo'
$x =~ /foo(?!baz)/;  # matches, 'baz' doesn't follow 'foo'
$x =~ /(?<!\s)foo/;  # matches, there is no \s before 'foo'

以下是一个示例,其中包含空格分隔的单词、数字和单个连字符的字符串将被拆分为其组成部分。单独使用/\s+/ 无法实现,因为连字符之间、单词和连字符之间不需要空格。通过向前和向后查看,可以建立额外的拆分位置。

$str = "one two - --6-8";
@toks = split / \s+              # a run of spaces
              | (?<=\S) (?=-)    # any non-space followed by '-'
              | (?<=-)  (?=\S)   # a '-' followed by any non-space
              /x, $str;          # @toks = qw(one two - - - 6 - 8)

使用独立的子表达式来防止回溯

独立子表达式(或原子子表达式)是在更大的正则表达式上下文中,独立于更大正则表达式运行的正则表达式。也就是说,它们可以根据自己的意愿消耗字符串中的尽可能多或尽可能少的字符,而不会考虑更大正则表达式是否能够匹配。独立子表达式用(?>regexp)表示,或者(从 5.32 版本开始,在 5.28 版本中实验性地支持)用(*atomic:regexp)表示。我们可以通过首先考虑一个普通的正则表达式来说明它们的特性

$x = "ab";
$x =~ /a*ab/;  # matches

这显然匹配,但在匹配过程中,子表达式a*首先抓取了'a'。但是,这样做不会让整个正则表达式匹配,所以经过回溯后,a*最终放弃了'a'并匹配了空字符串。在这里,a*匹配的内容依赖于正则表达式其余部分匹配的内容。

将此与独立子表达式进行对比

$x =~ /(?>a*)ab/;  # doesn't match!

独立子表达式(?>a*)不关心正则表达式的其余部分,因此它看到一个'a'并抓取它。然后正则表达式的其余部分ab无法匹配。因为(?>a*)是独立的,所以没有回溯,独立子表达式不会放弃它的'a'。因此,整个正则表达式的匹配失败。类似的行为发生在完全独立的正则表达式中

$x = "ab";
$x =~ /a*/g;   # matches, eats an 'a'
$x =~ /\Gab/g; # doesn't match, no 'a' available

这里/g\G创建了一个字符串从一个正则表达式到另一个正则表达式的“团队合作”传递。带有独立子表达式的正则表达式非常类似于此,将字符串传递给独立子表达式,并将字符串传递回封闭的正则表达式。

独立子表达式阻止回溯的能力非常有用。假设我们想要匹配一个非空字符串,该字符串用括号括起来,深度最多为两层。那么以下正则表达式匹配

$x = "abc(de(fg)h";  # unbalanced parentheses
$x =~ /\( ( [ ^ () ]+ | \( [ ^ () ]* \) )+ \)/xx;

正则表达式匹配一个左括号、一个或多个交替的副本和一个右括号。交替是双向的,第一个交替[^()]+匹配没有括号的子字符串,第二个交替\([^()]*\)匹配用括号分隔的子字符串。这个正则表达式的问题在于它是有病的:它有形式为(a+|b)+的嵌套不确定量词。我们在第一部分讨论了如何这种嵌套量词在没有匹配可能的情况下可能需要指数级的时间来执行。为了防止指数级爆炸,我们需要在某个时刻阻止无用的回溯。这可以通过将内部量词作为独立子表达式来完成

$x =~ /\( ( (?> [ ^ () ]+ ) | \([ ^ () ]* \) )+ \)/xx;

在这里,(?>[^()]+)通过尽可能多地吞噬字符串并保留它来打破字符串分割的退化。然后匹配失败会更快地失败。

条件表达式

条件表达式 是一种 if-then-else 语句的形式,它允许根据某些条件选择要匹配的模式。条件表达式有两种类型:(?(condition)yes-regexp)(?(condition)yes-regexp|no-regexp)(?(condition)yes-regexp) 类似于 Perl 中的 'if () {}' 语句。如果 condition 为真,则匹配 yes-regexp。如果 condition 为假,则跳过 yes-regexp,Perl 将继续处理下一个正则表达式元素。第二种形式类似于 Perl 中的 'if () {} else {}' 语句。如果 condition 为真,则匹配 yes-regexp,否则匹配 no-regexp

condition 可以有多种形式。第一种形式是在括号中简单的整数 (integer)。如果相应的反向引用 \integer 在正则表达式中之前匹配过,则它为真。可以使用与捕获组关联的名称来完成相同的事情,写成 (<name>)('name')。第二种形式是裸的零宽度断言 (?...),可以是前瞻、后顾或代码断言(下一节讨论)。第三组形式提供了测试,如果表达式在递归中执行((R))或从某个捕获组调用,则返回真,该捕获组通过数字((R1)(R2) 等)或名称((R&name))引用。

condition 的整数或名称形式允许我们根据正则表达式中之前匹配的内容,更灵活地选择要匹配的内容。这将搜索 "$x$x""$x$y$y$x" 形式的单词

% simple_grep '^(\w+)(\w+)?(?(2)\g2\g1|\g1)$' /usr/dict/words
beriberi
coco
couscous
deed
...
toot
toto
tutu

后顾 condition 允许,连同反向引用,匹配的早期部分影响匹配的后期部分。例如,

/[ATGC]+(?(?<=AA)G|C)$/;

匹配 DNA 序列,使其以 AAG 或其他碱基对组合和 'C' 结尾。注意,形式是 (?(?<=AA)G|C) 而不是 (?((?<=AA))G|C);对于前瞻、后顾或代码断言,条件周围的括号是不需要的。

定义命名模式

一些正则表达式在多个地方使用相同的子模式。从 Perl 5.10 开始,可以在模式的一部分中定义命名子模式,以便可以在模式中的任何地方按名称调用它们。此定义组的语法模式是 (?(DEFINE)(?<name>pattern)...)。命名模式的插入写成 (?&name)

以下示例使用前面介绍的浮点数模式说明此功能。三个重复使用的子模式是可选符号、整数的数字序列和小数部分。模式末尾的DEFINE组包含它们的定义。请注意,小数部分模式是第一个可以重用整数模式的地方。

/^ (?&osg)\ * ( (?&int)(?&dec)? | (?&dec) )
   (?: [eE](?&osg)(?&int) )?
 $
 (?(DEFINE)
   (?<osg>[-+]?)         # optional sign
   (?<int>\d++)          # integer
   (?<dec>\.(?&int))     # decimal fraction
 )/x

递归模式

此功能(在 Perl 5.10 中引入)极大地扩展了 Perl 模式匹配的功能。通过使用结构(?group-ref)在模式中的任何位置引用其他捕获组,引用组中的模式将用作独立的子模式来代替组引用本身。由于组引用可能包含在它引用的组内部,因此现在可以将模式匹配应用于以前需要递归解析器才能完成的任务。

为了说明此功能,我们将设计一个模式,如果字符串包含回文,则匹配该模式。(这是一个单词或句子,忽略空格、标点符号和大小写,从后往前读和从前往后读一样。我们首先观察到空字符串或只包含一个单词字符的字符串是回文。否则它必须在前面有一个单词字符,在末尾有相同的字符,中间还有另一个回文。

/(?: (\w) (?...Here be a palindrome...) \g{ -1 } | \w? )/x

在两端添加\W*以消除要忽略的内容,我们已经有了完整的模式

my $pp = qr/^(\W* (?: (\w) (?1) \g{-1} | \w? ) \W*)$/ix;
for $s ( "saippuakauppias", "A man, a plan, a canal: Panama!" ){
    print "'$s' is a palindrome\n" if $s =~ /$pp/;
}

(?...)中,可以使用绝对和相对反向引用。可以使用(?R)(?0)重新插入整个模式。如果您更喜欢命名您的组,可以使用(?&name)递归到该组。

一点魔法:在正则表达式中执行 Perl 代码

通常,正则表达式是 Perl 表达式的一部分。代码评估表达式通过允许任意 Perl 代码成为正则表达式的一部分来扭转这种关系。代码评估表达式表示为(?{code}),其中code是 Perl 语句的字符串。

代码表达式是零宽度断言,它们返回的值取决于其环境。有两种可能性:代码表达式用作条件表达式(?(condition)...)中的条件,或者它不是。如果代码表达式是条件,则会评估代码,并将结果(,最后一条语句的结果)用于确定真假。如果代码表达式不用作条件,则断言始终评估为真,并将结果放入特殊变量$^R中。变量$^R然后可以在正则表达式中后面的代码表达式中使用。以下是一些愚蠢的示例

$x = "abcdef";
$x =~ /abc(?{print "Hi Mom!";})def/; # matches,
                                     # prints 'Hi Mom!'
$x =~ /aaa(?{print "Hi Mom!";})def/; # doesn't match,
                                     # no 'Hi Mom!'

请仔细注意下一个示例

$x =~ /abc(?{print "Hi Mom!";})ddd/; # doesn't match,
                                     # no 'Hi Mom!'
                                     # but why not?

乍一看,你会认为它不应该打印,因为显然ddd不会匹配目标字符串。但是看看这个例子

$x =~ /abc(?{print "Hi Mom!";})[dD]dd/; # doesn't match,
                                        # but _does_ print

嗯。这里发生了什么?如果你一直在关注,你就会知道上面的模式应该与最后一个模式基本相同(几乎);将'd'包含在字符类中不会改变它匹配的内容。那么为什么第一个不打印而第二个打印呢?

答案在于正则表达式引擎所做的优化。在第一种情况下,引擎看到的所有内容都是普通的旧字符(除了?{}结构)。它足够聪明,可以在实际运行模式之前意识到字符串'ddd'没有出现在我们的目标字符串中。但在第二种情况下,我们欺骗它认为我们的模式更复杂。它看一眼,看到我们的字符类,并决定它必须实际运行模式才能确定它是否匹配,并且在运行它的过程中,它在发现我们没有匹配之前遇到了打印语句。

要更仔细地了解引擎如何进行优化,请参阅下面的"Pragmas and debugging"部分。

更多关于?{}的乐趣

$x =~ /(?{print "Hi Mom!";})/;         # matches,
                                       # prints 'Hi Mom!'
$x =~ /(?{$c = 1;})(?{print "$c";})/;  # matches,
                                       # prints '1'
$x =~ /(?{$c = 1;})(?{print "$^R";})/; # matches,
                                       # prints '1'

当正则表达式在搜索匹配的过程中回溯时,标题部分中提到的魔法就会发生。如果正则表达式回溯到代码表达式,并且如果表达式中使用的变量使用local局部化,则代码表达式产生的变量更改将被撤消!因此,如果我们想计算一个字符在组内被匹配的次数,我们可以使用,例如

$x = "aaaa";
$count = 0;  # initialize 'a' count
$c = "bob";  # test if $c gets clobbered
$x =~ /(?{local $c = 0;})         # initialize count
       ( a                        # match 'a'
         (?{local $c = $c + 1;})  # increment count
       )*                         # do this any number of times,
       aa                         # but match 'aa' at the end
       (?{$count = $c;})          # copy local $c var into $count
      /x;
print "'a' count is $count, \$c variable is '$c'\n";

它会打印

'a' count is 2, $c variable is 'bob'

如果我们将 (?{local $c = $c + 1;})替换为 (?{$c = $c + 1;}),则变量更改在回溯期间不会被撤消,我们得到

'a' count is 4, $c variable is 'bob'

请注意,只有局部变量的更改会被撤销。代码表达式执行的其他副作用是永久性的。因此

$x = "aaaa";
$x =~ /(a(?{print "Yow\n";}))*aa/;

产生

Yow
Yow
Yow
Yow

结果 $^R 会自动本地化,以便在回溯存在的情况下正常运行。

此示例在条件语句中使用代码表达式来匹配定冠词,例如英语中的 'the' 或德语中的 'der|die|das'

$lang = 'DE';  # use German
...
$text = "das";
print "matched\n"
    if $text =~ /(?(?{
                      $lang eq 'EN'; # is the language English?
                     })
                   the |             # if so, then match 'the'
                   (der|die|das)     # else, match 'der|die|das'
                 )
                /xi;

请注意,这里的语法是 (?(?{...})yes-regexp|no-regexp),而不是 (?((?{...}))yes-regexp|no-regexp)。换句话说,在代码表达式的情况下,我们不需要在条件周围添加额外的括号。

如果您尝试在代码文本包含在插值变量中而不是在模式中直接出现的地方使用代码表达式,Perl 可能会让您感到意外

$bar = 5;
$pat = '(?{ 1 })';
/foo(?{ $bar })bar/; # compiles ok, $bar not interpolated
/foo(?{ 1 })$bar/;   # compiles ok, $bar interpolated
/foo${pat}bar/;      # compile error!

$pat = qr/(?{ $foo = 1 })/;  # precompile code regexp
/foo${pat}bar/;      # compiles ok

如果正则表达式包含一个插值代码表达式的变量,Perl 会将该正则表达式视为错误。但是,如果代码表达式预编译到一个变量中,插值是允许的。问题是,为什么这是一个错误?

原因是变量插值和代码表达式一起构成了安全风险。这种组合很危险,因为许多编写搜索引擎的程序员经常获取用户输入并将其直接插入正则表达式中

$regexp = <>;       # read user-supplied regexp
$chomp $regexp;     # get rid of possible newline
$text =~ /$regexp/; # search $text for the $regexp

如果 $regexp 变量包含一个代码表达式,用户就可以执行任意 Perl 代码。例如,一些恶作剧者可以搜索 system('rm -rf *'); 来删除您的文件。从这个意义上说,插值和代码表达式的组合会污染您的正则表达式。因此,默认情况下,不允许在同一个正则表达式中同时使用插值和代码表达式。如果您不担心恶意用户,可以通过调用 use re 'eval' 来绕过此安全检查

use re 'eval';       # throw caution out the door
$bar = 5;
$pat = '(?{ 1 })';
/foo${pat}bar/;      # compiles ok

另一种代码表达式形式是模式代码表达式。模式代码表达式类似于常规代码表达式,只是代码评估的结果被视为正则表达式并立即匹配。一个简单的例子是

$length = 5;
$char = 'a';
$x = 'aaaaabb';
$x =~ /(??{$char x $length})/x; # matches, there are 5 of 'a'

此最后一个示例包含普通代码表达式和模式代码表达式。它检测二进制字符串 1101010010001...'1' 的斐波那契间距 0,1,1,2,3,5,... 是否存在

    $x = "1101010010001000001";
    $z0 = ''; $z1 = '0';   # initial conditions
    print "It is a Fibonacci sequence\n"
        if $x =~ /^1         # match an initial '1'
                    (?:
                       ((??{ $z0 })) # match some '0'
                       1             # and then a '1'
		       (?{ $z0 = $z1; $z1 .= $^N; })
                    )+   # repeat as needed
                  $      # that is all there is
                 /x;
    printf "Largest sequence matched was %d\n", length($z1)-length($z0);

请记住,$^N 被设置为最后一个完成的捕获组匹配的内容。这将打印

It is a Fibonacci sequence
Largest sequence matched was 5

哈!用你的普通正则表达式包试试吧...

请注意,当编译正则表达式时,变量 $z0$z1 不会被替换,就像在代码表达式之外的普通变量一样。相反,整个代码块在 Perl 编译包含文字正则表达式模式的代码的同时被解析为 Perl 代码。

这个正则表达式没有/x修饰符,

/^1(?:((??{ $z0 }))1(?{ $z0 = $z1; $z1 .= $^N; }))+$/

这表明代码部分仍然可能包含空格。然而,在处理代码和条件表达式时,正则表达式的扩展形式几乎是创建和调试正则表达式所必需的。

回溯控制动词

Perl 5.10 引入了一些控制动词,旨在通过直接影响正则表达式引擎和提供监控技术来提供对回溯过程的详细控制。有关详细说明,请参见"perlre 中的特殊回溯控制动词"

下面只是一个例子,说明了控制动词(*FAIL),它可以缩写为(*F)。如果将其插入正则表达式中,它将导致正则表达式失败,就像在模式和字符串之间出现不匹配时一样。正则表达式的处理将继续进行,就像在任何“正常”失败之后一样,因此,例如,将尝试字符串中的下一个位置或其他备选方案。由于匹配失败不会保留捕获组或产生结果,因此可能需要将其与嵌入代码结合使用。

%count = ();
"supercalifragilisticexpialidocious" =~
    /([aeiou])(?{ $count{$1}++; })(*FAIL)/i;
printf "%3d '%s'\n", $count{$_}, $_ for (sort keys %count);

该模式以匹配字母子集的类开头。每当它匹配时,就会执行类似$count{'a'}++;的语句,增加字母的计数器。然后(*FAIL)按字面意思执行,正则表达式引擎按照书本上的方法进行:只要没有到达字符串的末尾,就会在查找另一个元音之前将位置向前移动。因此,匹配与否都没有区别,正则表达式引擎会一直处理,直到检查完整个字符串。(值得注意的是,使用类似

$count{lc($_)}++ for split('', "supercalifragilisticexpialidocious");
printf "%3d '%s'\n", $count2{$_}, $_ for ( qw{ a e i o u } );

的替代解决方案要慢得多。)

编译指示和调试

说到调试,Perl 中有几个编译指示可用于控制和调试正则表达式。我们在上一节中已经遇到过一个编译指示,use re 'eval';,它允许变量插值和代码表达式在正则表达式中共存。其他编译指示是

use re 'taint';
$tainted = <>;
@parts = ($tainted =~ /(\w+)\s+(\w+)/; # @parts is now tainted

taint 编译指示会导致与受污染变量匹配的任何子字符串也被污染,前提是你的 perl 支持污染(参见perlsec)。通常情况下并非如此,因为正则表达式通常用于从受污染变量中提取安全位。当你不提取安全位,而是执行其他一些处理时,使用tainttainteval 编译指示都是词法作用域的,这意味着它们只在包含编译指示的块结束之前有效。

use re '/m';  # or any other flags
$multiline_string =~ /^foo/; # /m is implied

re '/flags' 编译指示(在 Perl 5.14 中引入)会打开给定的正则表达式标志,直到词法作用域结束。有关更多详细信息,请参见"re 中的'/flags' 模式"

use re 'debug';
/^(.*)$/s;       # output debugging info

use re 'debugcolor';
/^(.*)$/s;       # output debugging info in living color

全局的 debugdebugcolor 编译指示允许您获取有关正则表达式编译和执行的详细调试信息。debugcolor 与 debug 相同,只是调试信息在可以显示 termcap 颜色序列的终端上以彩色显示。以下是一个示例输出

% perl -e 'use re "debug"; "abc" =~ /a*b+c/;'
Compiling REx 'a*b+c'
size 9 first at 1
   1: STAR(4)
   2:   EXACT <a>(0)
   4: PLUS(7)
   5:   EXACT <b>(0)
   7: EXACT <c>(9)
   9: END(0)
floating 'bc' at 0..2147483647 (checking floating) minlen 2
Guessing start of match, REx 'a*b+c' against 'abc'...
Found floating substr 'bc' at offset 1...
Guessed: match at offset 0
Matching REx 'a*b+c' against 'abc'
  Setting an EVAL scope, savestack=3
   0 <> <abc>           |  1:  STAR
                         EXACT <a> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   1 <a> <bc>           |  4:    PLUS
                         EXACT <b> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   2 <ab> <c>           |  7:      EXACT <c>
   3 <abc> <>           |  9:      END
Match successful!
Freeing REx: 'a*b+c'

如果您已经学习到本教程的这一部分,您可能已经猜到调试输出的不同部分告诉您什么。第一部分

Compiling REx 'a*b+c'
size 9 first at 1
   1: STAR(4)
   2:   EXACT <a>(0)
   4: PLUS(7)
   5:   EXACT <b>(0)
   7: EXACT <c>(9)
   9: END(0)

描述了编译阶段。STAR(4) 表示存在一个带星号的对象,在本例中为 'a',如果它匹配,则跳转到第 4 行,即 PLUS(7)。中间几行描述了在匹配之前执行的一些启发式方法和优化

floating 'bc' at 0..2147483647 (checking floating) minlen 2
Guessing start of match, REx 'a*b+c' against 'abc'...
Found floating substr 'bc' at offset 1...
Guessed: match at offset 0

然后执行匹配,剩余的几行描述了该过程

Matching REx 'a*b+c' against 'abc'
  Setting an EVAL scope, savestack=3
   0 <> <abc>           |  1:  STAR
                         EXACT <a> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   1 <a> <bc>           |  4:    PLUS
                         EXACT <b> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   2 <ab> <c>           |  7:      EXACT <c>
   3 <abc> <>           |  9:      END
Match successful!
Freeing REx: 'a*b+c'

每一步都采用 n <x> <y> 的形式,其中 <x> 是匹配的字符串部分,<y> 是尚未匹配的字符串部分。 | 1: STAR 表示 Perl 位于上述编译列表中的第 1 行。有关更多详细信息,请参阅 "perldebguts 中的调试正则表达式"

调试正则表达式的另一种方法是在正则表达式中嵌入 print 语句。这将提供关于交替中回溯的逐一说明

"that this" =~ m@(?{print "Start at position ", pos, "\n";})
                 t(?{print "t1\n";})
                 h(?{print "h1\n";})
                 i(?{print "i1\n";})
                 s(?{print "s1\n";})
                     |
                 t(?{print "t2\n";})
                 h(?{print "h2\n";})
                 a(?{print "a2\n";})
                 t(?{print "t2\n";})
                 (?{print "Done at position ", pos, "\n";})
                @x;

打印

Start at position 0
t1
h1
t2
h2
a2
t2
Done at position 4

另请参阅

这只是一个教程。有关 Perl 正则表达式的完整说明,请参阅 perlre 正则表达式参考页面。

有关匹配 m// 和替换 s/// 运算符的更多信息,请参阅 "perlop 中的正则表达式引号类运算符"。有关 split 操作的信息,请参阅 "perlfunc 中的 split"

有关正则表达式维护的全面资源,请参阅 Jeffrey Friedl 编著的 Mastering Regular Expressions 一书(由 O'Reilly 出版,ISBN 1556592-257-3)。

作者和版权

版权所有 (c) 2000 Mark Kvale。保留所有权利。现在由 Perl 维护者维护。

本文件可以在与 Perl 本身相同的条款下分发。

致谢

停止密码子 DNA 示例的灵感来自 Mastering Regular Expressions 第 7 章中的邮政编码示例。

作者感谢 Jeff Pinyan、Andrew Johnson、Peter Haworth、Ronald J Kimball 和 Joe Smith 的宝贵意见。