内容

名称

perlfaq6 - 正则表达式

版本

版本 5.20210520

描述

本节内容相对较少,因为其他 FAQ 章节中充斥着与正则表达式相关的答案。例如,解码 URL 和检查某个值是否为数字都可以使用正则表达式,但这些答案在本文档的其他地方(具体来说,在 perlfaq9:“如何解码或创建 Web 上的 %-编码”和 perlfaq4:“如何确定标量是否为数字/整数/浮点数”中)。

如何使用正则表达式而不产生难以阅读和维护的代码?

有三种技术可以使正则表达式更易于维护和理解。

正则表达式外部的注释

使用普通的 Perl 注释描述你正在做什么以及如何做。

# turn the line into the first word, a colon, and the
# number of characters on the rest of the line
s/^(\w+)(.*)/ lc($1) . ":" . length($2) /meg;
正则表达式内部的注释

/x 修饰符会忽略正则表达式模式中的空白字符(字符类和少数其他地方除外),并且还允许你在其中使用普通的注释。正如你所想,空白字符和注释非常有用。

/x 允许你将以下代码

s{<(?:[^>'"]*|".*?"|'.*?')+>}{}gs;

转换为以下代码

s{ <                    # opening angle bracket
    (?:                 # Non-backreffing grouping paren
        [^>'"] *        # 0 or more things that are neither > nor ' nor "
            |           #    or else
        ".*?"           # a section between double quotes (stingy match)
            |           #    or else
        '.*?'           # a section between single quotes (stingy match)
    ) +                 #   all occurring one or more times
    >                   # closing angle bracket
}{}gsx;                 # replace with nothing, i.e. delete

它仍然不如散文清晰,但对于描述模式各个部分的含义非常有用。

不同的分隔符

虽然我们通常认为模式是用 / 字符分隔的,但它们几乎可以用任何字符分隔。 perlre 描述了这一点。例如,上面的 s/// 使用花括号作为分隔符。选择其他分隔符可以避免在模式中引用分隔符

s/\/usr\/local/\/usr\/share/g;    # bad delimiter choice
s#/usr/local#/usr/share#g;        # better

使用逻辑上成对的分隔符可以更易于阅读

s{/usr/local/}{/usr/share}g;      # better still

我在匹配多行时遇到了问题。问题出在哪里?

要么你查看的字符串中没有多行(很可能),要么你没有在模式中使用正确的修饰符(有可能)。

将多行数据放入字符串有很多方法。如果您希望在读取输入时自动发生,您需要设置 $/(可能设置为 '' 用于段落或 undef 用于整个文件)以允许您一次读取多行。

阅读 perlre 以帮助您决定要使用哪些 /s/m(或两者):/s 允许点包含换行符,而 /m 允许插入符号和美元符号匹配换行符旁边的位置,而不仅仅是字符串的末尾。您需要确保您确实在其中包含了一个多行字符串。

例如,此程序检测重复的单词,即使它们跨越换行符(但不跨越段落)。对于此示例,我们不需要 /s,因为我们没有在正则表达式中使用点,而我们希望该正则表达式跨越行边界。我们也不需要 /m,因为我们不希望插入符号或美元符号在记录内部的任何位置匹配换行符旁边的位置。但是,必须将 $/ 设置为除默认值以外的其他值,否则我们实际上永远不会读取多行记录。

$/ = '';          # read in whole paragraph, not just one line
while ( <> ) {
    while ( /\b([\w'-]+)(\s+\g1)+\b/gi ) {     # word starts alpha
        print "Duplicate $1 at paragraph $.\n";
    }
}

以下是一些代码,用于查找以“From ”开头的句子(这些句子会被许多邮件程序破坏)

$/ = '';          # read in whole paragraph, not just one line
while ( <> ) {
    while ( /^From /gm ) { # /m makes ^ match next to \n
    print "leading From in paragraph $.\n";
    }
}

以下代码用于查找段落中 START 和 END 之间的所有内容

undef $/;          # read in whole file, not just one line or paragraph
while ( <> ) {
    while ( /START(.*?)END/sgm ) { # /s makes . cross line boundaries
        print "$1\n";
    }
}

如何提取两条模式之间的行,而这两条模式本身位于不同的行上?

您可以使用 Perl 的有点奇特的 .. 运算符(在 perlop 中有说明)

perl -ne 'print if /START/ .. /END/' file1 file2 ...

如果您想要文本而不是行,您将使用

perl -0777 -ne 'print "$1\n" while /START(.*?)END/gs' file1 file2 ...

但是,如果您想要 STARTEND 的嵌套出现,您将遇到本节中关于匹配平衡文本的问题中描述的问题。

以下是用 .. 的另一个示例

while (<>) {
    my $in_header =   1  .. /^$/;
    my $in_body   = /^$/ .. eof;
# now choose between them
} continue {
    $. = 0 if eof;    # fix $.
}

如何使用正则表达式匹配 XML、HTML 或其他令人讨厌的丑陋内容?

不要使用正则表达式。使用模块并忘记正则表达式。 XML::LibXMLHTML::TokeParserHTML::TreeBuilder 模块是一个良好的开端,尽管每个命名空间都有其他解析模块专门用于某些任务以及不同的执行方式。从 CPAN 搜索(http://metacpan.org/)开始,并惊叹于人们已经为您完成的所有工作!:)

我把一个正则表达式放入了 $/,但它不起作用。怎么了?

$/ 必须是字符串。如果你真的需要这样做,可以使用这些示例。

如果你有 File::Stream,这很简单。

use File::Stream;

my $stream = File::Stream->new(
    $filehandle,
    separator => qr/\s*,\s*/,
    );

print "$_\n" while <$stream>;

如果你没有 File::Stream,你需要做更多工作。

你可以使用 sysread 的四参数形式不断地添加到缓冲区。在添加到缓冲区后,检查你是否拥有完整的行(使用你的正则表达式)。

local $_ = "";
while( sysread FH, $_, 8192, length ) {
    while( s/^((?s).*?)your_pattern// ) {
        my $record = $1;
        # do stuff here.
    }
}

如果你不介意你的整个文件最终都在内存中,你可以使用 foreach 和使用 c 标志和 \G 锚点的匹配来做同样的事情。

local $_ = "";
while( sysread FH, $_, 8192, length ) {
    foreach my $record ( m/\G((?s).*?)your_pattern/gc ) {
        # do stuff here.
    }
    substr( $_, 0, pos ) = "" if pos;
}

如何在左侧进行不区分大小写的替换,同时保留右侧的大小写?

这是 Larry Rosler 给出的一个很棒的 Perl 解决方案。它利用了 ASCII 字符串上按位异或的特性。

$_= "this is a TEsT case";

$old = 'test';
$new = 'success';

s{(\Q$old\E)}
{ uc $new | (uc $1 ^ $1) .
    (uc(substr $1, -1) ^ substr $1, -1) x
    (length($new) - length $1)
}egi;

print;

这是作为子例程,根据上面的模型。

sub preserve_case {
    my ($old, $new) = @_;
    my $mask = uc $old ^ $old;

    uc $new | $mask .
        substr($mask, -1) x (length($new) - length($old))
}

$string = "this is a TEsT case";
$string =~ s/(test)/preserve_case($1, "success")/egi;
print "$string\n";

这将打印

this is a SUcCESS case

作为替代方案,为了保留替换词的大小写(如果它比原始词更长),你可以使用 Jeff Pinyan 的这段代码。

sub preserve_case {
    my ($from, $to) = @_;
    my ($lf, $lt) = map length, @_;

    if ($lt < $lf) { $from = substr $from, 0, $lt }
    else { $from .= substr $to, $lf }

    return uc $to | ($from ^ uc $from);
}

这将句子更改为“this is a SUcCess case”。

为了表明 C 程序员可以在任何编程语言中编写 C 代码,如果你更喜欢更类似 C 的解决方案,以下脚本使替换的大小写与原始大小写相同,逐个字母。(它也恰好比 Perl 解决方案慢约 240%)。如果替换的字符比被替换的字符串多,则使用最后一个字符的大小写来替换剩余的字符。

# Original by Nathan Torkington, massaged by Jeffrey Friedl
#
sub preserve_case
{
    my ($old, $new) = @_;
    my $state = 0; # 0 = no change; 1 = lc; 2 = uc
    my ($i, $oldlen, $newlen, $c) = (0, length($old), length($new));
    my $len = $oldlen < $newlen ? $oldlen : $newlen;

    for ($i = 0; $i < $len; $i++) {
        if ($c = substr($old, $i, 1), $c =~ /[\W\d_]/) {
            $state = 0;
        } elsif (lc $c eq $c) {
            substr($new, $i, 1) = lc(substr($new, $i, 1));
            $state = 1;
        } else {
            substr($new, $i, 1) = uc(substr($new, $i, 1));
            $state = 2;
        }
    }
    # finish up with any remaining new (for when new is longer than old)
    if ($newlen > $oldlen) {
        if ($state == 1) {
            substr($new, $oldlen) = lc(substr($new, $oldlen));
        } elsif ($state == 2) {
            substr($new, $oldlen) = uc(substr($new, $oldlen));
        }
    }
    return $new;
}

如何使 \w 匹配国家字符集?

在你的脚本中添加 use locale;。\w 字符类取自当前区域设置。

有关详细信息,请参阅 perllocale

如何匹配 /[a-zA-Z]/ 的区域设置智能版本?

你可以使用 POSIX 字符类语法 /[[:alpha:]]/,它在 perlre 中有说明。

无论你身处哪个区域设置,字母字符都是 \w 中的字符,不包括数字和下划线。作为正则表达式,它看起来像 /[^\W\d_]/。它的补集,非字母字符,则是 \W 中的所有内容以及数字和下划线,或者 /[\W\d_]/

如何在正则表达式中引用变量?

除非分隔符是单引号,否则 Perl 解析器将在正则表达式中扩展 $variable 和 @variable 引用。请记住,s/// 替换的右侧被视为双引号字符串(有关更多详细信息,请参阅 perlop)。还要记住,除非您在替换之前加上 \Q,否则任何正则表达式特殊字符都会被执行。以下是一个示例

$string = "Placido P. Octopus";
$regex  = "P.";

$string =~ s/$regex/Polyp/;
# $string is now "Polypacido P. Octopus"

因为 . 在正则表达式中是特殊的,可以匹配任何单个字符,所以这里的正则表达式 P. 匹配了原始字符串中的 <Pl>。

为了转义 . 的特殊含义,我们使用 \Q

$string = "Placido P. Octopus";
$regex  = "P.";

$string =~ s/\Q$regex/Polyp/;
# $string is now "Placido Polyp Octopus"

使用 \Q 会导致正则表达式中的 . 被视为普通字符,因此 P. 匹配一个 P 后面跟着一个点。

/o 选项到底有什么用?

(由 brian d foy 贡献)

正则表达式的 /o 选项(在 perlopperlreref 中有说明)告诉 Perl 只编译一次正则表达式。这只有在模式包含变量时才有用。Perl 5.6 及更高版本会在模式不变的情况下自动处理这种情况。

由于匹配运算符 m//、替换运算符 s/// 和正则表达式引用运算符 qr// 是双引号构造,因此您可以将变量插入模式中。有关更多详细信息,请参阅“如何在正则表达式中引用变量?”的答案。

此示例从参数列表中获取一个正则表达式,并打印与之匹配的输入行

my $pattern = shift @ARGV;

while( <> ) {
    print if m/$pattern/;
}

Perl 5.6 之前的版本会为每次迭代重新编译正则表达式,即使 $pattern 没有改变。/o 会通过告诉 Perl 第一次编译模式,然后在后续迭代中重复使用该模式来防止这种情况。

my $pattern = shift @ARGV;

while( <> ) {
    print if m/$pattern/o; # useful for Perl < 5.6
}

在 5.6 及更高版本的 Perl 中,如果变量没有改变,Perl 不会重新编译正则表达式,因此您可能不需要 /o 选项。它不会造成伤害,但也不会带来帮助。如果您希望任何版本的 Perl 只编译一次正则表达式,即使变量改变(因此只使用其初始值),您仍然需要 /o

您可以观察 Perl 的正则表达式引擎的工作情况,以亲自验证 Perl 是否正在重新编译正则表达式。use re 'debug' 编译指示(包含在 Perl 5.005 及更高版本中)会显示详细信息。在 5.6 之前的 Perl 中,您应该看到 re 报告它在每次迭代中都编译了正则表达式。在 Perl 5.6 或更高版本中,您应该只看到 re 在第一次迭代中报告这种情况。

use re 'debug';

my $regex = 'Perl';
foreach ( qw(Perl Java Ruby Python) ) {
    print STDERR "-" x 73, "\n";
    print STDERR "Trying $_...\n";
    print STDERR "\t$_ is good!\n" if m/$regex/;
}

如何使用正则表达式从文件中去除 C 语言风格的注释?

虽然这实际上是可以做到的,但比你想象的要难得多。例如,这行代码

perl -0777 -pe 's{/\*.*?\*/}{}gs' foo.c

在许多情况下都能正常工作,但并非所有情况下都能正常工作。你看,对于某些类型的 C 程序来说,它过于简单,特别是那些在引号字符串中包含看似注释的代码。为此,你需要像这样,由 Jeffrey Friedl 创建,后来由 Fred Curtis 修改。

$/ = undef;
$_ = <>;
s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $2 ? $2 : ""#gse;
print;

当然,这可以用 /x 修饰符更清晰地写出来,添加空格和注释。以下是 Fred Curtis 扩展后的版本。

s{
   /\*         ##  Start of /* ... */ comment
   [^*]*\*+    ##  Non-* followed by 1-or-more *'s
   (
     [^/*][^*]*\*+
   )*          ##  0-or-more things which don't start with /
               ##    but do end with '*'
   /           ##  End of /* ... */ comment

 |         ##     OR  various things which aren't comments:

   (
     "           ##  Start of " ... " string
     (
       \\.           ##  Escaped char
     |               ##    OR
       [^"\\]        ##  Non "\
     )*
     "           ##  End of " ... " string

   |         ##     OR

     '           ##  Start of ' ... ' string
     (
       \\.           ##  Escaped char
     |               ##    OR
       [^'\\]        ##  Non '\
     )*
     '           ##  End of ' ... ' string

   |         ##     OR

     .           ##  Anything other char
     [^/"'\\]*   ##  Chars which doesn't start a comment, string or escape
   )
 }{defined $2 ? $2 : ""}gxse;

一个小的修改也去除了 C++ 注释,可能跨越多行使用续行符

s#/\*[^*]*\*+([^/*][^*]*\*+)*/|//([^\\]|[^\n][\n]?)*?\n|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $3 ? $3 : ""#gse;

我可以使用 Perl 正则表达式匹配平衡文本吗?

(由 brian d foy 贡献)

你第一次尝试应该使用 Text::Balanced 模块,该模块自 Perl 5.8 以来就包含在 Perl 标准库中。它有各种函数来处理棘手的文本。 Regexp::Common 模块也可以通过提供你可以使用的预制模式来提供帮助。

从 Perl 5.10 开始,你可以使用正则表达式匹配平衡文本,使用递归模式。在 Perl 5.10 之前,你必须使用各种技巧,例如在 (??{}) 序列中使用 Perl 代码。

以下是一个使用递归正则表达式的示例。目标是捕获所有尖括号内的文本,包括嵌套尖括号内的文本。此示例文本有两个“主要”组:一个组有一级嵌套,另一个组有两级嵌套。尖括号内共有五个组

I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.

匹配平衡文本的正则表达式使用了两个新的(对于 Perl 5.10)正则表达式功能。这些在 perlre 中有介绍,这个例子是该文档中一个例子的修改版本。

首先,在任何量词中添加新的占有量词 + 会找到最长的匹配项,并且不会回溯。这很重要,因为你希望通过递归处理所有尖括号,而不是回溯。组 [^<>]++ 找到一个或多个非尖括号,不回溯。

其次,新的 (?PARNO) 指的是由 PARNO 给出的特定捕获组中的子模式。在下面的正则表达式中,第一个捕获组找到(并记住)平衡文本,你需要在第一个缓冲区中使用相同的模式才能越过嵌套文本。这就是递归部分。(?1) 使用外部捕获组中的模式作为正则表达式的独立部分。

将所有这些放在一起,你得到

#!/usr/local/bin/perl5.10.0

my $string =<<"HERE";
I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.
HERE

my @groups = $string =~ m/
        (                   # start of capture group 1
        <                   # match an opening angle bracket
            (?:
                [^<>]++     # one or more non angle brackets, non backtracking
                  |
                (?1)        # found < or >, so recurse to capture group 1
            )*
        >                   # match a closing angle bracket
        )                   # end of capture group 1
        /xg;

$" = "\n\t";
print "Found:\n\t@groups\n";

输出显示 Perl 找到了两个主要组

Found:
    <brackets in <nested brackets> >
    <another group <nested once <nested twice> > >

做一些额外的工作,你就可以获得所有尖括号内的组,即使它们也在其他尖括号内。每次获得平衡匹配时,删除其外部定界符(这就是你刚刚匹配的,所以不要再匹配它),并将其添加到要处理的字符串队列中。继续这样做,直到你没有匹配项

#!/usr/local/bin/perl5.10.0

my @queue =<<"HERE";
I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.
HERE

my $regex = qr/
        (                   # start of bracket 1
        <                   # match an opening angle bracket
            (?:
                [^<>]++     # one or more non angle brackets, non backtracking
                  |
                (?1)        # recurse to bracket 1
            )*
        >                   # match a closing angle bracket
        )                   # end of bracket 1
        /x;

$" = "\n\t";

while( @queue ) {
    my $string = shift @queue;

    my @groups = $string =~ m/$regex/g;
    print "Found:\n\t@groups\n\n" if @groups;

    unshift @queue, map { s/^<//; s/>$//; $_ } @groups;
}

输出显示所有组。最外层的匹配项首先显示,嵌套的匹配项随后显示。

Found:
    <brackets in <nested brackets> >
    <another group <nested once <nested twice> > >

Found:
    <nested brackets>

Found:
    <nested once <nested twice> >

Found:
    <nested twice>

正则表达式贪婪是什么意思?如何避免它?

大多数人认为贪婪的正则表达式会尽可能多地匹配。从技术上讲,实际上是量词(?*+{})是贪婪的,而不是整个模式;Perl 更倾向于局部贪婪和即时满足,而不是整体贪婪。要获得相同量词的非贪婪版本,请使用(??*?+?{}?)。

一个例子

my $s1 = my $s2 = "I am very very cold";
$s1 =~ s/ve.*y //;      # I am cold
$s2 =~ s/ve.*?y //;     # I am very cold

注意第二个替换是如何在遇到“y ”后立即停止匹配的。*? 量词实际上告诉正则表达式引擎尽快找到匹配项并将控制权传递给下一个匹配项,就像你在玩热土豆一样。

如何处理每行上的每个单词?

使用 split 函数

while (<>) {
    foreach my $word ( split ) {
        # do something with $word here
    }
}

请注意,这在英语意义上并不真正是一个单词;它只是连续的非空格字符块。

要处理仅包含字母数字序列(包括下划线)的内容,您可以考虑

while (<>) {
    foreach $word (m/(\w+)/g) {
        # do something with $word here
    }
}

如何打印出单词频率或行频率摘要?

要做到这一点,您必须解析输入流中的每个单词。我们假设您所说的单词是指字母、连字符或撇号的块,而不是上一个问题中给出的非空格块的单词概念。

my (%seen);
while (<>) {
    while ( /(\b[^\W_\d][\w'-]+\b)/g ) {   # misses "`sheep'"
        $seen{$1}++;
    }
}

while ( my ($word, $count) = each %seen ) {
    print "$count $word\n";
}

如果您想对行做同样的事情,您不需要正则表达式。

my (%seen);

while (<>) {
    $seen{$_}++;
}

while ( my ($line, $count) = each %seen ) {
    print "$count $line";
}

如果您希望这些输出按排序顺序排列,请参阅 perlfaq4:“如何对哈希进行排序(可选地按值而不是键排序)?”。

如何进行近似匹配?

请参阅 CPAN 上提供的模块 String::Approx

如何高效地同时匹配多个正则表达式?

(由 brian d foy 贡献)

您需要避免每次要匹配时都编译正则表达式。在这个例子中,perl 必须为 foreach 循环的每次迭代重新编译正则表达式,因为 $pattern 会发生变化。

my @patterns = qw( fo+ ba[rz] );

LINE: while( my $line = <> ) {
    foreach my $pattern ( @patterns ) {
        if( $line =~ m/\b$pattern\b/i ) {
            print $line;
            next LINE;
        }
    }
}

qr// 运算符会编译正则表达式,但不会应用它。当您使用预编译版本的正则表达式时,perl 会执行更少的操作。在本例中,我插入了一个 map 来将每个模式转换为其预编译形式。脚本的其余部分相同,但速度更快。

my @patterns = map { qr/\b$_\b/i } qw( fo+ ba[rz] );

LINE: while( my $line = <> ) {
    foreach my $pattern ( @patterns ) {
        if( $line =~ m/$pattern/ ) {
            print $line;
            next LINE;
        }
    }
}

在某些情况下,您可能能够将多个模式组合成一个正则表达式。但要注意需要回溯的情况。在本例中,正则表达式只编译一次,因为 $regex 在迭代之间不会改变。

my $regex = join '|', qw( fo+ ba[rz] );

while( my $line = <> ) {
    print if $line =~ m/\b(?:$regex)\b/i;
}

CPAN 上的 "Data::Munge 中的 list2re 函数" 也可用于形成一个匹配文字字符串列表(而不是正则表达式)的单个正则表达式。

有关正则表达式效率的更多详细信息,请参阅 Jeffrey Friedl 的《精通正则表达式》。他解释了正则表达式引擎的工作原理以及为什么某些模式出奇地效率低下。一旦您了解 perl 如何应用正则表达式,您就可以针对特定情况对其进行调整。

为什么使用 \b 进行词边界搜索对我无效?

(由 brian d foy 贡献)

确保您了解 \b 的真正含义:它是词字符 \w 和非词字符之间的边界。这个非词字符可能是 \W,但也可能是字符串的开头或结尾。

它不是(不是!)空格和非空格之间的边界,也不是我们用来创建句子的单词之间的内容。

在正则表达式中,词边界(\b)是一个“零宽度断言”,这意味着它不代表字符串中的字符,而是在特定位置的一个条件。

对于正则表达式 /\bPerl\b/,在“P”之前和“l”之后必须有一个词边界。只要“P”之前和“l”之后是除词字符以外的任何字符,该模式就会匹配。这些字符串与 /\bPerl\b/ 匹配。

"Perl"    # no word char before "P" or after "l"
"Perl "   # same as previous (space is not a word char)
"'Perl'"  # the "'" char is not a word char
"Perl's"  # no word char before "P", non-word char after "l"

这些字符串与 /\bPerl\b/ 不匹配。

"Perl_"   # "_" is a word char!
"Perler"  # no word char before "P", but one after "l"

您不必使用 \b 来匹配单词。您可以查找被词字符包围的非词字符。这些字符串与模式 /\b'\b/ 匹配。

"don't"   # the "'" char is surrounded by "n" and "t"
"qep'a'"  # the "'" char is surrounded by "p" and "a"

这些字符串与 /\b'\b/ 不匹配。

"foo'"    # there is no word char after non-word "'"

您还可以使用 \b 的补码 \B 来指定不应该存在词边界。

在模式 /\Bam\B/ 中,“a”之前和“m”之后必须有一个词字符。这些模式与 /\Bam\B/ 匹配。

"llama"   # "am" surrounded by word chars
"Samuel"  # same

这些字符串与 /\Bam\B/ 不匹配。

"Sam"      # no word boundary before "a", but one after "m"
"I am Sam" # "am" surrounded by non-word chars

为什么使用 $&$`$' 会降低我的程序速度?

(由 Anno Siegel 贡献)

一旦 Perl 发现你的程序中需要使用这些变量中的任何一个,它就会在每次模式匹配时提供它们。这意味着在每次模式匹配时,整个字符串都会被复制,一部分到 $`,一部分到 $&,一部分到 $'。因此,对于长字符串和频繁匹配的模式,这种惩罚最为严重。如果可以,请避免使用 $&、$' 和 $`,但如果无法避免,一旦你使用过它们,就随意使用它们,因为你已经付出了代价。请记住,某些算法确实很欣赏它们。从 5.005 版本开始,$& 变量不再像其他两个变量那样“昂贵”。

从 Perl 5.6.1 开始,特殊变量 @- 和 @+ 可以从功能上替代 $`、$& 和 $'。这些数组包含指向每个匹配的开始和结束的指针(有关完整信息,请参阅 perlvar),因此它们本质上提供了相同的信息,但不会出现过度字符串复制的风险。

Perl 5.10 添加了三个特殊变量,${^MATCH}${^PREMATCH}${^POSTMATCH},它们可以完成相同的工作,但不会造成全局性能损失。只有在使用 /p 修饰符编译或执行正则表达式时,Perl 5.10 才会设置这些变量。

\G 在正则表达式中有什么用?

使用 \G 锚点可以从上一个匹配结束的位置开始下一个匹配。正则表达式引擎不能跳过任何字符来使用此锚点查找下一个匹配,因此 \G 与字符串开头锚点 ^ 相似。\G 锚点通常与 g 修饰符一起使用。它使用 pos() 的值作为下一个匹配的开始位置。当匹配运算符进行连续匹配时,它会使用最后一个匹配的下一个字符的位置(或下一个匹配的第一个字符的位置,具体取决于你的看法)更新 pos()。每个字符串都有自己的 pos() 值。

假设你想匹配字符串 "1122a44" 中所有连续的数字对,并在遇到非数字时停止匹配。你想匹配 1122,但字母 a 出现在 2244 之间,你想在 a 处停止。简单地匹配数字对会跳过 a,并且仍然匹配 44

$_ = "1122a44";
my @pairs = m/(\d\d)/g;   # qw( 11 22 44 )

如果你使用 \G 锚点,你将强制 22 之后的匹配从 a 开始。正则表达式无法在那里匹配,因为它没有找到数字,因此下一个匹配失败,匹配运算符返回它已经找到的配对。

$_ = "1122a44";
my @pairs = m/\G(\d\d)/g; # qw( 11 22 )

你也可以在标量上下文中使用 \G 锚点。你仍然需要 g 修饰符。

$_ = "1122a44";
while( m/\G(\d\d)/g ) {
    print "Found $1\n";
}

如果匹配在字母 a 处失败,perl 会重置 pos(),并且在同一个字符串上的下一次匹配将从开头开始。

$_ = "1122a44";
while( m/\G(\d\d)/g ) {
    print "Found $1\n";
}

print "Found $1 after while" if m/(\d\d)/g; # finds "11"

您可以使用 c 修饰符禁用匹配失败时的 pos() 重置,这在 perlopperlreref 中有说明。后续匹配将从上次成功匹配结束的位置(pos() 的值)开始,即使在此期间同一个字符串上的匹配失败了。在这种情况下,while() 循环后的匹配将从 a(上次匹配停止的位置)开始,并且由于它不使用任何锚点,因此可以跳过 a 来查找 44

$_ = "1122a44";
while( m/\G(\d\d)/gc ) {
    print "Found $1\n";
}

print "Found $1 after while" if m/(\d\d)/g; # finds "44"

通常,当您想要在匹配失败时尝试不同的匹配(例如在标记器中)时,会将 \G 锚点与 c 修饰符一起使用。Jeffrey Friedl 提供了以下示例,该示例在 5.004 或更高版本中有效。

while (<>) {
    chomp;
    PARSER: {
        m/ \G( \d+\b    )/gcx   && do { print "number: $1\n";  redo; };
        m/ \G( \w+      )/gcx   && do { print "word:   $1\n";  redo; };
        m/ \G( \s+      )/gcx   && do { print "space:  $1\n";  redo; };
        m/ \G( [^\w\d]+ )/gcx   && do { print "other:  $1\n";  redo; };
    }
}

对于每一行,PARSER 循环首先尝试匹配一系列数字,后面跟着一个词边界。此匹配必须从上次匹配结束的位置(或第一次匹配时的字符串开头)开始。由于 m/ \G( \d+\b )/gcx 使用了 c 修饰符,如果字符串与该正则表达式不匹配,perl 不会重置 pos(),并且下一次匹配将从相同的位置开始,以尝试不同的模式。

Perl 正则表达式是 DFA 还是 NFA?它们是否符合 POSIX 标准?

虽然 Perl 的正则表达式类似于 egrep(1) 程序的 DFA(确定性有限自动机),但它们实际上是作为 NFA(非确定性有限自动机)实现的,以允许回溯和反向引用。它们也不符合 POSIX 标准,因为 POSIX 标准保证所有情况下的最坏情况行为。(似乎有些人更喜欢一致性的保证,即使保证的是速度慢。)有关这些问题的详细信息,请参阅 Jeffrey Friedl 编著的“精通正则表达式”(O'Reilly 出版)一书(完整引用见 perlfaq2)。

在空上下文中使用 grep 有什么问题?

问题是 grep 会构建一个返回值列表,无论上下文如何。这意味着您让 Perl 费力构建一个您随后会丢弃的列表。如果列表很大,您会浪费时间和空间。如果您打算迭代列表,则为此目的使用 for 循环。

在 5.8.1 之前的 perl 版本中,map 也存在此问题。但从 5.8.1 开始,这个问题已修复,map 变得上下文感知 - 在空上下文中,不会构建任何列表。

如何匹配包含多字节字符的字符串?

从 Perl 5.6 开始,Perl 就支持一定程度的多字节字符。建议使用 Perl 5.8 或更高版本。支持的多字节字符集包括 Unicode 和通过 Encode 模块提供的传统编码。参见 perluniintroperlunicodeEncode

如果你使用的是旧版本的 Perl,可以使用 Unicode::String 模块进行 Unicode 操作,并使用 Unicode::Map8Unicode::Map 模块进行字符转换。如果你使用的是日语编码,可以尝试使用 jperl 5.005_03。

最后,以下方法由 Jeffrey Friedl 提供,他在《Perl 杂志》第 5 期中的一篇文章中谈到了这个问题。

假设你有一些奇怪的火星编码,其中 ASCII 大写字母对编码单个火星字母(例如,两个字节“CV”构成一个火星字母,两个字节“SG”、“VS”、“XX”等也是如此)。其他字节表示单个字符,就像 ASCII 一样。

因此,火星字符串“I am CVSGXX!”使用 12 个字节来编码 9 个字符 'I'、' '、'a'、'm'、' '、'CV'、'SG'、'XX'、'!'。

现在,假设你想搜索单个字符 /GX/。Perl 不知道火星编码,因此它会在“I am CVSGXX!”字符串中找到两个字节“GX”,即使该字符不存在:它看起来像存在,因为“SG”紧挨着“XX”,但实际上没有真正的“GX”。这是一个大问题。

以下是一些痛苦的解决方法

# Make sure adjacent "martian" bytes are no longer adjacent.
$martian =~ s/([A-Z][A-Z])/ $1 /g;

print "found GX!\n" if $martian =~ /GX/;

或者像这样

my @chars = $martian =~ m/([A-Z][A-Z]|[^A-Z])/g;
# above is conceptually similar to:     my @chars = $text =~ m/(.)/g;
#
foreach my $char (@chars) {
    print "found GX!\n", last if $char eq 'GX';
}

或者像这样

while ($martian =~ m/\G([A-Z][A-Z]|.)/gs) {  # \G probably unneeded
    if ($1 eq 'GX') {
        print "found GX!\n";
        last;
    }
}

以下是 Benjamin Goldberg 提供的另一种稍微不那么痛苦的方法,他使用了一个零宽度负向后顾断言。

print "found GX!\n" if    $martian =~ m/
    (?<![A-Z])
    (?:[A-Z][A-Z])*?
    GX
    /x;

如果字符串中存在“火星”字符 GX,则此方法成功,否则失败。如果你不喜欢使用 (?<!),即零宽度负向后顾断言,可以将 (?<![A-Z]) 替换为 (?:^|[^A-Z])。

它确实有一个缺点,就是将错误的内容放入 $-[0] 和 $+[0] 中,但这通常可以解决。

如何匹配存储在变量中的正则表达式?

(由 brian d foy 贡献)

我们不必将模式硬编码到匹配运算符(或任何其他使用正则表达式的运算符)中。我们可以将模式存储在变量中以供以后使用。

匹配运算符是一个双引号上下文,因此你可以像双引号字符串一样插入你的变量。在这种情况下,你将正则表达式作为用户输入读取并将其存储在 $regex 中。一旦你在 $regex 中获得了模式,你就可以在匹配运算符中使用该变量。

chomp( my $regex = <STDIN> );

if( $string =~ m/$regex/ ) { ... }

$regex 中的任何正则表达式特殊字符仍然是特殊的,并且模式必须有效,否则 Perl 会报错。例如,在这个模式中,有一个未配对的括号。

my $regex = "Unmatched ( paren";

"Two parens to bind them all" =~ m/$regex/;

当 Perl 编译正则表达式时,它将括号视为内存匹配的开始。当它找不到结束括号时,它会报错

Unmatched ( in regex; marked by <-- HERE in m/Unmatched ( <-- HERE  paren/ at script line 3.

您可以通过几种方法来解决这个问题,具体取决于您的情况。首先,如果您不希望字符串中的任何字符是特殊字符,您可以在使用字符串之前使用 `quotemeta` 对它们进行转义。

chomp( my $regex = <STDIN> );
$regex = quotemeta( $regex );

if( $string =~ m/$regex/ ) { ... }

您也可以使用 `\Q` 和 `\E` 序列在匹配运算符中直接执行此操作。`\Q` 告诉 Perl 从哪里开始转义特殊字符,而 `\E` 告诉它在哪里停止(有关更多详细信息,请参阅 perlop)。

chomp( my $regex = <STDIN> );

if( $string =~ m/\Q$regex\E/ ) { ... }

或者,您可以使用 `qr//`,即正则表达式引用运算符(有关更多详细信息,请参阅 perlop)。它引用并可能编译模式,您可以将正则表达式标志应用于模式。

chomp( my $input = <STDIN> );

my $regex = qr/$input/is;

$string =~ m/$regex/  # same as m/$input/is;

您可能还想通过在整个内容周围包装一个 `eval` 块来捕获任何错误。

chomp( my $input = <STDIN> );

eval {
    if( $string =~ m/\Q$input\E/ ) { ... }
};
warn $@ if $@;

或者...

my $regex = eval { qr/$input/is };
if( defined $regex ) {
    $string =~ m/$regex/;
}
else {
    warn $@;
}

作者和版权

版权所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者(如所述)。保留所有权利。

本文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。

无论其分发方式如何,本文件中的所有代码示例均在此置于公共领域。您被允许并鼓励在您自己的程序中使用此代码,无论出于娱乐目的还是为了盈利,都可以随心所欲。在代码中添加一个简单的评论以表示感谢将是礼貌的,但不是必需的。