正则表达式和匹配


Perl 操作文本的强大能力部分来源于对一个名为正则表达式的计算概念的囊括。正则 表达式(通常简化为regexregexp)是一个模式,它描述了某文本字符串的 特征。正则表达式引擎解释一个模式并将其应用到文本字符串上,以识别何者匹配。

Perl 的核心文档丰富而详细地描述了 Perl 的正则表达式;请参考perldoc perlretut教程、perldoc perlre完整文档和perldoc perlreref参考指南。Jeffrey Friedl 的著作精通正则表达式解释了正则表达式工作背后的理论和机制。即便这些参考书看上 去令人望而生畏,正则表达式却很像 Perl ———— 你可以在懂得很少的情况下完成很多任务。

字面值

最简单的正则表达式就是简单的子字符串模式:

my $name = 'Chatfield';
    say "Found a hat!" if $name =~ /hat/;

匹配操作符(//或者,更为正式的m//)中包含一正则表达式————在这个例子中是hat。即使读上去像一个单词,它的意思其实是“h字符,后接a字符,再接t字符,出现在该字符串的任何地方”。在hat中的每一个字符都是一个正则表达式中的原子:该模式独立的单元。正则表达式绑定操作符(=~)是一个中缀操作符(词缀性) ,它将位于其右的正则表达式应用于左边由表达式产生的字符串。当在标量上下文中求值时, 一个成功的匹配将得到真值。

绑定操作符的否定形式(!~)在匹配成功时得到一个假值。

qr// 操作符和正则表达式组合

在现代化的 Perl 中,当由qr//操作符创建时,正则表达式是第一级实体:

my $hat = qr/hat/;
    say 'Found a hat!' if $name =~ /$hat/;
你可以通过内插和组合将他们变为更大更复杂的模式:

my $hat   = qr/hat/;
    my $field = qr/field/;

    say 'Found a hat in a field!' if $name =~ /$hat$field/;

    # 或

    like( $name,qr/$hat$field/,'Found a hat in a field!' );

量词

正则表达式比前例演示的更为强大;你可以使用index操作符在字符串内搜索 子字符串字面值。但用正则表达式引擎达成这项任务就像驾驶自治区战斗直升机去 拐角小店购买备用奶酪。

通过使用正则表达式量词,正则表达式可以变得更为强大,使你能够指定一个正则表达式 组件在匹配字符串中出现的频率。最简单的量词是零个或一个量词,或者说?

my $cat_or_ct = qr/ca?t/;

    like( 'cat',$cat_or_ct,"'cat' matches /ca?t/" );
    like( 'ct',"'ct' matches /ca?t/"  );
在正则表达式中,任意原子后接?字符意味着“匹配此原子零次或一次”。这个正则表 达式匹配c后立即跟随零个a字符再接一个t字符。它同时也匹配在ct字符间有一个a

一个或多个量词,或者说+,在字符串中只匹配量词前原子至少出现一次的情况:

my $one_or_more_a = qr/ca+t/;

    like( 'cat',$one_or_more_a,"'cat' matches /ca+t/"    );
    like( 'caat',"'caat' matches /ca+t/"   );
    like( 'caaat',"'caaat' matches /ca+t/"  );
    like( 'caaaat',"'caaaat' matches /ca+t/" );

    unlike( 'ct',"'ct' does not match /ca+t/" );
能匹配多少原子没有什么理论上的限制。

零个或多个量词*;它匹配量化原子在字符串中出现的零个或多个实例:

my $zero_or_more_a = qr/ca*t/;

    like( 'cat',$zero_or_more_a,"'cat' matches /ca*t/"    );
    like( 'caat',"'caat' matches /ca*t/"   );
    like( 'caaat',"'caaat' matches /ca*t/"  );
    like( 'caaaat',"'caaaat' matches /ca*t/" );
    like( 'ct',"'ct' matches /ca*t/"     );
这看上去可能不很有用,但是它和其他正则表达式功能可以组合得很好,让你不必关心在 特定位置是否出现某模式。即便如此,大多数正则表达式从使用?+量词 中获益远多于*量词,因为它们可以避免昂贵的回溯,并将你的意图表达得更为清晰。

最后,你可以通过数值量词指定某原子匹配的次数{n}意味着确切匹配n次。

# equivalent to qr/cat/;
    my $only_one_a = qr/ca{1}t/;

    like( 'cat',$only_one_a,"'cat' matches /ca{1}t/" );
{n,}意味着匹配次数必须至少为n次,同时可以匹配更多次数

# equivalent to qr/ca+t/;
    my $at_least_one_a = qr/ca{1,}t/;

    like( 'cat',$at_least_one_a,"'cat' matches /ca{1,}t/"    );
    like( 'caat',"'caat' matches /ca{1,}t/"   );
    like( 'caaat',"'caaat' matches /ca{1,}t/"  );
    like( 'caaaat',"'caaaat' matches /ca{1,}t/" );
my $one_to_three_a = qr/ca{1,3}t/;

    like(   'cat',$one_to_three_a,3}t/"           );
    like(   'caat',3}t/"          );
    like(   'caaat',3}t/"         );
    unlike( 'caaaat',"'caaaat' does not match /ca{1,3}t/" );

贪心性

+*自身来说,它们是贪心量词;它们尽可能多地匹配输入字符串。在利 用.*来匹配“任何数量的任何字符”时尤其有害:

# a poor regex
    my $hot_meal = qr/hot.*meal/;

    say 'Found a hot meal!' if 'I have a hot meal' =~ $hot_meal;
    say 'Found a hot meal!'
         if 'I did some one-shot,piecemeal work!' =~ $hot_meal;
贪心量词总是试图先行匹配尽可能多的输入字符串,仅在匹配明显不成功时回退。如 果你用如下方式查找单词“loam(土壤)”,你将无法把所有结果塞进 7 Down 的四个盒子 里面:

my $seven_down = qr/l${letters_only}*m/;
作为新手,你将得到AlabamaBelgium以及Bethlehem。那里的土壤也许不错, 但是它们全都太长了————并且,匹配是从单词的中间开始的。

让贪心量词变成非贪心量词只需在其后加上?量词:

my $minimal_greedy_match = qr/hot.*?meal/;
当给予非贪心量词,正则表达式引擎将偏向最短的、可能的潜在匹配,并仅在目前 数目无法满足匹配要求时,增加.*?标记组合识别的字符数量。由于*匹配 零或更多次,对应这个标记组合的最小潜在匹配是零个字符:

say 'Found a hot meal' if 'ilikeahotmeal' =~ /$minimal_greedy_match/;
使用+量词可以匹配某项一次或多次:

my $minimal_greedy_at_least_one = qr/hot.+?meal/;

    unlike( 'ilikeahotmeal',$minimal_greedy_at_least_one );

    like( 'i like a hot meal',$minimal_greedy_at_least_one );
?量词修饰符也可以应用于?(零或一次匹配)和范围量词。在每种情况下,它使 得正则表达式尽可能地少匹配。

贪心修饰符.+.*是诱人但危险的。如果你编写了贪心匹配正则表达式,请用 综合自动化测试套件和代表性数据完整地测试它,以将产生令人不快结果的可能性降到最 小。

正则表达式锚点

正则表达式锚点强制在字符串某位置进行匹配。字符串开头锚点\A)确保任 何匹配都将从字符串开头开始:

# 也匹配 "lammed"、"lawmaker" 和 "layman"
    my $seven_down = qr/\Al${letters_only}{2}m/;
字符串末尾锚点\Z)确保任何匹配都将结束于字符串末尾:

# 也匹配 "loom",它足够接近
    my $seven_down = qr/\Al${letters_only}{2}m\Z/;
单词边界元字符\b)仅匹配一单词字符 (\w) 和另一非单词字符 (\W) 之间的边界。因此,查找loam而非Belgium,可以使用加锚点的正则表达式:

my $seven_down = qr/\bl${letters_only}{2}m\b/;

与 Perl 类似,达成某个目的的正则表达式也有许多写法。请考虑从中挑出最有表达力 也是最易维护的一个

元字符

正则表达式随着原子的一般化而变得更为强大。举例来说,在正则表达式内,.字符 的意思是“匹配除换行外的任意字符”。玩填字游戏时,如果你想在一个单词列表里查找每 一个匹配的 7 Down(“Rich soil”),你可以这样写:

for my $word (@words)
    {
        next unless length( $word ) == 4;
        next unless $word =~ /l..m/;
        say "Possibility: $word";
    }
当然,如果你的候选匹配列表由单词外的东西构成,这个元字符可能导致假阳性,因为它同时 匹配标点符号、空格、数字以及其他的非单词字符。\w元字符代表所有字母数字符(按 Unicode 处理————Unicode and Strings)还有下划线:

next unless $word =~ /l\w\wm/;
\d元字符匹配数字————不是你预期的 0-9,而是 Unicode 数字:

# 并非一个健壮的电话号码匹配器
    next unless $potential_phone_number =~ /\d{3}-\d{3}-\d{4}/;
    say "I have your number: $potential_phone_number";
可以使用\s元字符匹配空白,无论是字面空格、制表符、硬回车、换页符或者换行:

my $two_three_letter_words = qr/\w{3}\s\w{3}/;
这些元字符也有否定形式。要匹配单词外的其他字符,使用\W。要匹配非数字 字符,使用\D。要匹配非空白字符,使用\S

正则表达式引擎将所有元字符作为原子对待。

字符类

如果允许字符的范围在上述四组里不够具体,通过把它们用中括号围起,你可以自行指定字符类

my $vowels    = qr/[aeIoU]/;
    my $maybe_cat = qr/c${vowels}t/;

标量变量名$vowels外的大括号帮助对变量名称消歧。如果没有它,语法分析器将把 变量名解释为$vowelst,这样不是因未知变量导致编译期错误就是把已存在$vowelst变量的内容内插进正则表达式。

如果字符集内的字符构成了一个连续的范围,你可以使用连字符(-)作为表达该范围的 快捷方式。

my $letters_only = qr/[a-zA-Z]/;
将连字符添加到字符类的开头或结尾可以将其包括进此字符类中:

my $interesting_punctuation = qr/[-!?]/;
……或对其进行转义:

my $line_characters = qr/[|=\-_]/;
就像单词和数字类元字符(\w\d)有自己的否定形式,你也可以否定一个字 符类。用插入符号(^)作为字符类的第一个元素意味着“这些外的所有字符”:

my $not_a_vowel = qr/[^aeIoU]/;

在此之外使用插入符号使其成为该字符类的一个成员。要在否定字符类里包含一个连字符, 可以将它放在插入符号后,或者在字符类的最后,再或者对其转义。

捕获

通常的做法是先匹配字符串的一部分并在稍后对其进行处理;也许你想从一个字符串中提取 一个地址或美国电话号码:

my $area_code    = qr/\(\d{3}\)/;
    my $local_number = qr/\d{3}-?\d{4}/;
    my $phone_number = qr/$area_code\s?$local_number/;

正则表达式中的括号是元字符;$area_code对它们进行了转义。

具名捕获

给出一个字符串,$contact_info,包含有联系信息,你可以将$phone_number正则表 达式应用其上并通过具名捕获来将任何匹配结果捕获并存入变量中:

if ($contact_info =~ /(?<phone>$phone_number)/)
    {
        say "Found a number $+{phone}";
    }

捕捉结构可能看上去像是一大个摇晃的标点,当你可以将其作为整体认读时,它还是比较 简单的:

(?<capture name> ... )

括号包围了整个捕获。?< name >结构必须紧跟左括号。它为捕获缓冲区提供了名 称。此结构位于括号内的其余部分是一个正则表达式。如果当正则表达式匹配该片段,Perl 将字符串被捕获的部分存储在神奇变量%+中:一个以捕获缓冲区名为键、匹配正则表 达式的字符串部分为值的哈希。

对于 Perl 5 正则表达式来说,括号是特殊的;认和常规的 Perl 代码一样,它们的行为 就是进行分组。它们也将匹配部分组成的一个或多个原子包围在内。要在正则表达式内使用 字面括号,你必须添加反斜杠,就像$area_code变量里那样。

编号捕获

具名捕获是 Perl 5.10 的新功能,但捕获早已在 Perl 中存在了许多年头。你也会碰到编号捕获

if ($contact_info =~ /($phone_number)/)
    {
        say "Found a number $1";
    }

括号把要捕获片段包围在内,但是没有正则表达式元字符给出捕获的名称。作为代替, Perl 将捕获的子字符串存放在一系列以$1开头的神奇变量中,并延续至正则表达式 中提供所有捕获组。Perl 找到的一个匹配捕获存放在$1,第二个存放在$2, 等等。捕获计数起始于捕获的括号;因而第一个左括号将捕获存入$1,第二个 存入$2,等等。

虽然具名捕获的语法比编号捕获来得长一些,但它提供了额外的清晰度。你不需要统计开 括号的的个数来指出某捕获会被存入$4还是$5,并且基于较短的正则表达式编写 更长的正则表达式相对容易一些,因为它们通常对位置的变更或是否出现在单个原子中不 那么敏感。

当你将一处匹配在列表上下文中求值时,编号捕获相对不那么令人沮丧:

if (my ($number) = $contact_info =~ /($phone_number)/)
    {
        say "Found a number $number";
    }

Perl 将按捕获顺序赋值给左值:

成组和选项

前面的例子将全部量词应用于简单原子上。它们也可以应用于一个更为复杂的子模式整体:

my $pork  = qr/pork/;
    my $beans = qr/beans/;

    like( 'pork and beans',qr/\A$pork?.*?$beans/,'maybe pork,definitely beans' );

如果手动扩展该正则表达式,结果可能令你感到惊讶:

like( 'pork and beans',qr/\Apork?.*?beans/,definitely beans' );

这样仍然匹配,但考虑一个更为具体的模式:

my $pork  = qr/pork/;
    my $and   = qr/and/;
    my $beans = qr/beans/;

    like( 'pork and beans',qr/\A$pork? $and? $beans/,maybe and,definitely beans' );

一些正则表达式不是匹配这项就是匹配另一项。使用选项元字符 (|) 即可:

my $rice  = qr/rice/;
    my $beans = qr/beans/;

    like( 'rice',qr/$rice|$beans/,'Found some rice'  );
    like( 'beans','Found some beans' );

选项元字符意味着匹配前述任一片段。但请注意解释为正则表达式片段的内容

like(   'rice',qr/rice|beans/,'Found some rice'  );
    like(   'beans','Found some beans' );
    unlike( 'ricb','Found some weird hybrid' );

模式rice|beans可能会解释为ric,后接eb,再跟上eans————但是,这是不正确的。选项总是将离正则表达式分隔符最近的算作整个片 段,无论该分隔符是模式开头和结尾,外围的括号,还是另一个选项字符或者中括号。

为了减少迷惑性,可以像变量 ($rice|$beans) 这样使用具名片段,或者将候选项 包括非捕获分组中:

my $starches = qr/(?:pasta|potatoes|rice)/;

(?:)序列将一系列原子成组但跳过捕获行为。此例中,它包括了三个选项。

其他转义序列

Perl 将正则表达式内的若干字符解释为元字符,它们代表不同于他们字面形式的意义。 中括号总是标示一个字符类,括号则将片段成组且可选地进行捕获。

要匹配一个元字符的字面实例,可以用反斜杠(\)对其进行转义。因此,\(意指单个左括号而\]意指单个右中括号。\.指的是一个字面点号,而非“匹配除换行符 外所有字符”的原子。

其他通常需要转义的有用的元字符是管道符(|)和美元符号($)。同时不要忘记量词:*?

为避免处处转义(和担心忘记转义内插的值),可以使用元字符禁用字符\Q元字符 禁用对元字符的处理直到它碰到\E序列。当取用你无法控制的正则表达式来匹配文本时, 这个个功能尤其有用:

my ($text,$literal_text) = @_;

    return $text =~ /\Q$literal_text\E/;

$literal_text参数可以包含任何内容————例如字符串** ALERT **。使用\Q\E,Perl 不会将“零或多个”量词解释为量词。相反,它会将此正则表达式解释为\*\* ALERT \*\*并试图匹配字面星号。

在处理来自不可信任的用户输入时须特别小心。构造一个恶意正则表达式对你的程序进行 有效的拒绝服务攻击是完全可以办到的。

断言

正则表达式锚点(\A\Z)是一种正则表达式断言的形式,字符串需要满足 此条件,但并不实际匹配字符串中的某个字符。就是说,正则表达式qr/\A/一直匹配,无论字符串内容为何。元字符\b\B也是断言。

零宽度断言匹配一个模式,不仅仅是一个字符串中的条件。最重要的是,它们不消耗 它们匹配模式中的位置。例如,你只要找一只“cat(猫)”,你可以是用单词边界断言:

my $just_a_cat = qr/cat\b/;

……但如果想找一非灾难性的“cat”,你也许会用到零宽度否定前瞻断言:

my $safe_feline = qr/cat(?!astrophe)/;

(?!...)结构仅在astrophe不紧随其后时匹配短语cat

零宽度否定前瞻断言

my $disastrous_feline = qr/cat(?=astrophe)/;

……仅在短语astrophe紧随其后时匹配cat。这看上去不怎么有用,一个普通的正 则表达式就能完成同样的任务,但考虑下述情况,如果你想在字典中查找所有非“catastrophic” 但以cat开头的单词。一种可能的情况是:

my $disastrous_feline = qr/cat(?!astrophe)/;

    while (<$words>)
    {
        chomp;
        next unless /\A(?<some_cat>$disastrous_feline.*)\Z/;
        say "Found a non-catastrophe '$+{some_cat}'";
    }

因为断言宽度为零,它不消耗源字符串。因此,带锚点的.*\Z模式片段必须出现;否则 就只将捕获源字符串中的cat部分。

零宽度后顾断言也是存在的。不像前瞻断言那样,这些断言的模式的长度必须固定;你不可 以在这些模式中使用量词。

要对你的猫绝不会出现在行首做出断言,你可以使用零宽度否定后顾断言

my $middle_cat = qr/(?<!^>cat/;

……此处的(?结构包含定长模式。特别的,你可以用零宽度肯定后顾断言表达cat必须总是立即出现在空格符之后:

my $space_cat = qr/(?<=\s)cat/;

……此处的(?<=...)结构包含定长模式。这种方式在用\G修饰符进行全局正则表 达式匹配时非常有用,但这是一个你不会经常用到的高级特性。

正则表达式修饰符

正则表达式操作符允许若干修饰符改变匹配的行为。这些修饰符出现在匹配、替换和qr//操作符的结尾。例如,要启用大小写不敏感的匹配:

my $pet = 'CaMeLiA';

    like( $pet,qr/Camelia/,'You have a nice butterfly there'       );
    like( $pet,qr/Camelia/i,'Your butterfly has a broken shift key' );

一个like()会失败,因为这些字符串包含不同的字母。第二个like()将通过,因 为/i修饰符使得正则表达式忽略大小写的区别。因修饰符的关系,Mm在第二 例中是等价的。

你也可以在模式中内嵌修饰符:

my $find_a_cat = qr/(?<feline>(?i)cat)/;

(?i)语法仅为它所包围的组启用大小写不敏感匹配:此例中,即整个feline捕获组。 你可以以此形式使用多个修饰符(在模式合适的部分)。你也可以通过前缀-来禁用特定 的修饰符:

my $find_a_rational = qr/(?<number>(?-i)Rat)/;

多行操作符,/m,允许^$锚点匹配字符串中任意行开头和结尾。

/s修饰符将源字符串作为一行对待,如此.元字符便匹配换行符。damian Conway 对助记符提出建议,/m修改多个(multiple)正则表达式元字符的行为,而/s修改单个(single)正则表达式元字符的行为。

/x修饰符允许你在模式中内嵌额外的空白和注释而不会改变它们的原意。此修饰符生效时, 正则表达式引擎将空白和注释字符(#)及其后的内容统统作为注释并忽略它们。这允许你编 写更可读的正则表达式:

my $attr_re = qr{
        ^                     # 行首

        # 杂项
        (?:
          [;\n\s]*            # 空白和伪分号
          (?:/\*.*?\*/)?      # C 注释 
        )*

        # 属性标记
        ATTR

        # 类型
        \s+
        (   U?INTVAL
          | FLOATVAL
          | STRING\s+\*
          | PMC\s+\*
          | \w*
        )
    }x;

这个正则表达式不简单,但注释和空白提高了它的可读性。即便你利用已编译的片段一起 编写正则表达式,/x修饰符还是能够帮助提高你的代码质量。

/g修饰符对字符串从头到脚执行某正则表达式行为。它和替换一起使用时候比较合理:

# appease the Mitchell estate
    my $contents = slurp( $file );
    $contents    =~ s/Scarlett O'Hara/Mauve Midway/g;

当和匹配一起使用时────并非替换────\G元字符允许你在循环中按块处理字符串。\G在最近一次匹配结束的位置进行匹配。为了按逻辑块处理一个全是美国电话号码的不正确编码 文件,你可以编写:

while ($contents =~ /\G(\w{3})(\w{3})(\w{4})/g)
    {
        push @numbers,"($1) $2-$3";
    }

注意\G锚点将从字符串中前一次迭代匹配的那一点开始着手。如果前一次匹配以诸如.*之类的贪心匹配结束,则接下来的可以用于匹配的部分将减少。前瞻断言的使用在这里很重要, 因为它们不消耗欲匹配的字符串。

/e修饰符允许你在替换操作右边写入任意 Perl 5 代码。如果成功匹配,正则表达式引擎将运 行这段代码,并用它的返回值作为替换的值。前面的全局替换例子中,替换不幸主角姓名的部分可 以变得更加健壮:

# appease the Mitchell estate
    my $contents = slurp( $file );
    $contents    =~ s{Scarlett( O'Hara)?}
                     { 'Mauve' . defined $1 ? ' Midway' : '' }ge;

你可以向一次替换操作添加任意多的/e修饰符。每一处额外的修饰符将对表达式的结果进行又 一次的求值,通常只有 Perl 高尔夫手才会使用/ee以及更加复杂的语句。

智能匹配

智能匹配操作符,~~,对两个操作符进行比较并在互相匹配时返回真值。定义的模糊恰 好反映了此操作符的智能程度:比较操作由操作数两者共同决定。之前你已经见识了这种行 为────givenGiven/When)进行的就是隐式智能匹配。

智能匹配操作符是一个中缀操作符:

say 'They match (somehow)' if $loperand ~~ $roperand;

比较的类型大致先由右操作符的类型决定然后再是左操作符。例如,如果右操作符是一个 带数值成分的标量,则比较将使用数值等于。如果右操作符是一个正则表达式,则比较将 是一个 grep 操作或模式匹配。如果右操作符是一个数组,比较将是 grep 操作或递归的 智能匹配。如果右操作符是一个哈希,比较操作将检查一个或多个键是否存在。

例如:

# 标量数值比较
    my $x = 10;
    my $y = 20;
    say 'Not equal numerically' unless $x ~~ $y;

    # 标量类数值比较
    my $x = 10;
    my $y = '10 little endians';
    say 'Equal numeric-ishally' if $x ~~ $y;

……以及:

my $needlepat = qr/needle/;

    say 'Pattern match'          if $needle   ~~ $needlepat;
    say 'Grep through array'     if @haystack ~~ $needlepat;
    say 'Grep through hash keys' if %hayhash  ~~ $needlepat;

……再及:

say 'Grep through array'                 if $needlepat  ~~ @haystack;
    say 'Array elements exist as hash keys'  if %hayhash    ~~ @haystack;
    say 'Array elements smart match'         if @strawstack ~~ @haystack;

……又及:

say 'Grep through hash keys'            if $needlepat ~~ %hayhash;
    say 'Array elements exist as hash keys' if @haystack  ~~ %hayhach;
    say 'Hash keys identical'               if %hayhash   ~~ %haymap;

这些比较操作在某操作数是给出数据类型的引用时也能正常工作。举例来说:

say 'Hash keys identical' if %hayhash ~~ \%hayhash;

你可以在对象上重载(重载)智能匹配操作符。如果你不这样做,智能匹配 操作符在你尝试将某对象用作操作数时会抛出异常。

你也可以使用如undef等其他数据类型,以及函数引用作为智能匹配操作数。请参 考perldoc perlsyn中的表格来获取更多细节。

相关文章

正则替换html代码中img标签的src值在开发富文本信息在移动端...
正则表达式
AWK是一种处理文本文件的语言,是一个强大的文件分析工具。它...
正则表达式是特殊的字符序列,利用事先定义好的特定字符以及...
Python界一名小学生,热心分享编程学习。
收集整理每周优质开发者内容,包括、、等方面。每周五定期发...