【问题标题】:Why does perl regex with /mg modifier match past end-of-line?为什么带有 /mg 修饰符的 perl 正则表达式匹配行尾?
【发布时间】:2020-09-24 12:17:03
【问题描述】:

这与perl multiline regex to separate comments within paragraphs有关, 但只关注一个正则表达式语法问题。

根据perlre: Modifiers, /m 正则表达式修饰符意味着

将匹配的字符串视为多行。即,将“^”和“$”从匹配字符串第一行的开头和最后一行的结尾改为匹配字符串中每一行的开头和结尾。

因此,使用以下代码:

#!/usr/bin/perl
use strict; use warnings;
$/ = ''; # one paragraph at a time
while(<DATA>)
{
    print "original:\n";
    print; 
    s/^([^B]*)(B.*?)$/>$1|$2</mg;
    print "\n\nafter substitution:\n";
    print; 
}

__DATA__
aaaaBaBaBB
bbbbBbadbe
cccc
dddd
eeeeBeeeee
ffff
gggg

我希望正则表达式引擎的行为如下。

第 1 行:匹配,因为它在这一行的开始和结束之间找到两个模式。

第 2 行:同上。

第 3 行:no 匹配。第一个正则表达式组(在第一组括号中)匹配。 但是当我们到达终点时, 我们仍在寻找B,开始 第二个正则表达式组。由于我们指定了/m,因此这一行的末尾意味着我们已经到达$,但没有满足整个模式。

第 4 行:我们开始一个新行,所以我们遇到一个新的^。同样,不匹配。

第 5 行:匹配。两个正则表达式组都位于行的开头和结尾之间,即^ 和$ 之间,与指定的完全相同。

所以我希望看到

>aaaa|BaBaBB<
>bbbb|Bbadbe<
cccc
dddd
>eeee|Beeeee<
ffff
gggg

相反,在第 3 行,引擎忽略行尾并搜索它。 它将第 3--5 行视为单行,如果我们突然愿意忽略表示行尾的$,它将满足正则表达式。这是我们看到的:

>aaaa|BaBaBB<
>bbbb|Bbadbe<
>cccc
dddd
eeee|Beeeee<
ffff
gggg

这与/m 规范有何一致性?这种行为记录在哪里?

> perl --version

This is perl 5, version 18, subversion 4 (v5.18.4) built for darwin-thread-multi-2level
(with 2 registered patches, see perl -V for more detail)

【问题讨论】:

  • 您确定需要段落模式吗?似乎您仅使用 /m 来补偿使用段落模式,因此 ^ 和 $ 将匹配每一行。删除它,代码将起作用。如果你确实需要它,你应该在运行正则表达式之前在换行符上分割段落。

标签: regex perl regex-group multiline


【解决方案1】:

[^B]* 将匹配尽可能多的非 B 字符,包括换行符。 将其替换为 [^B\n]* 可能会满足您的需求。

【讨论】:

  • 当正则表达式以 $ 结尾时,为什么它会匹配换行符? perldoc 用 /m 表示,$ 匹配行尾。我认为你所说的对 /s 来说是正确的。
  • 为什么它不匹配换行符?换行符的唯一例外是点元字符.,它通常与换行符不匹配,除非使用了/s 修饰符。 [^B]* 表示 匹配除 B 之外的任何字符 0 次或更多次 它匹配换行符的原因是您将所有行都拼成了一个字符串。在逐行模式下,它无法做到这一点。
  • /m 允许 $ 匹配行尾或字符串结尾之前的条件。 perlre 表示“$ 匹配字符串的结尾(或在字符串末尾的换行符之前;如果使用 /m,则在任何换行符之前)”
  • 正则表达式的元素通常以从左到右的顺序应用,直到全部通过,或者失败时,回溯并尝试另一种选择。在您的情况下,第一个元素 [^B]* 尽可能多地吸食非 B,然后 'B' 元素与 B 匹配,然后是 .*?元素 slurps 尽可能少的非换行符,然后是 '$' 元素反复无法匹配行尾并触发回溯,直到重试 .*?啜饮足够的字符以使“$”成功。
【解决方案2】:

根据perldoc perlretut:

当一个正则表达式可以用几种不同的方式匹配一个字符串时,我们可以使用 上面的原则来预测正则表达式的匹配方式:

原则0:作为一个整体,任何正则表达式都会匹配到 字符串中最早可能的位置。

原则 1:在交替 a|b|c... 中,最左边的选择 允许匹配整个正则表达式的将是使用的。

原则2:最大匹配量词'?' , '*' , '+' 和 {n,m} 通常会匹配尽可能多的字符串,而 仍然允许整个正则表达式匹配。

原则 3:如果一个正则表达式中有两个或多个元素,则 最左边的贪婪量词,如果有的话,将匹配尽可能多的字符串 尽可能同时仍然允许整个正则表达式匹配。下一个 最左边的贪婪量词,如果有的话,将尝试匹配尽可能多的 字符串尽可能保持可用,同时仍然允许 整个正则表达式匹配。依此类推,直到所有正则表达式元素都 满意。

正如我们在上面看到的,原则 0 优先于其他原则。正则表达式 将尽可能早地与其他原则匹配 确定正则表达式如何匹配最早的字符 位置。
[...]
我们可以修改上面的原则 3 以考虑非贪婪 量词:

原则 3:如果一个正则表达式中有两个或多个元素,则 最左边的贪婪(非贪婪)量词,如果有的话,将尽可能匹配 (很少)字符串,同时仍然允许整个 正则表达式匹配。下一个最左边的贪心(非贪心)量词,如果 任何,将尝试匹配剩余字符串的尽可能多(很少) 尽可能提供给它,同时仍然允许整个正则表达式 匹配。以此类推,直到满足所有正则表达式元素。

所以对于这种情况

my $str = 'cccc
dddd
eeeeBeeeee
ffff
gggg';
$str =~ s/^([^B]*)(B.*?)$/>$1|$2</m;

我们使用原则 0 和原则 3,因此它将匹配 $str 中的开始位置(位置 0)。根据原则3,我们从最左边的元素开始:

^([^B]*)

它将匹配“尽可能多的字符串,同时仍然允许整个正则表达式匹配。”,这意味着它将能够从字符串的开头匹配到第一个B。然后引擎考虑下一个元素

(B.*?)$

仍然,根据原则 3:它将匹配 “尽可能少的字符串,同时仍然允许整个正则表达式匹配。” 所以它会从 B 匹配到第一个找到新行。

【讨论】:

  • 有时这被称为“最左边最长”。
【解决方案3】:

/m 和 /s 修饰符和字符类的 Perl 文档可以从连接点和添加更多示例中受益,我将在这里尝试。

不管/m 和/s 修饰符如何,字符类可以匹配换行符。这就是[^B]* 匹配\n 并得到扩展的原因在您的情况下通过多个换行符。实际上,您可以指定一个明确包含 ([\n]) 或不包含 ([^\n]) 换行符的字符类。除了换行符(\n),还有一个非换行符(\N)。

/s 修饰符只改变. 的行为(它允许. 匹配换行符)。它不会改变任何其他字符类的行为。

单独使用/m 和/s 修饰符可以获得明显不同的行为,如下面的示例所示。此行为已记录在案,因此是可预测的,但并不总是直观的。我通常一起使用这些修饰符 (/ms),并发现它使我的代码更加直观和可维护。这样,我不必每次都考虑换行匹配行为。事实上,作为习惯,我通常在我自己的代码中的大多数正则表达式中使用 /xms 修饰符,/x 使代码更具可读性和可维护性 (Conway (2005), p. 236-241, Vromans ( 2006))。

参考:

perlrecharclass - Perl Regular Expression Character Classes: Backslash sequences

\N Match a character that isn't a newline.

perlreref - Perl Regular Expressions Reference: CHARACTER CLASSES

\N A non newline (when not followed by '{NAME}';;
not valid in a character class; equivalent to [^\n]; it's
like '.' without /s modifier)

perlre - Perl regular expressions: Modifiers

m
将匹配的字符串视为多行。也就是说,将"^"和"$"从匹配字符串第一行的开头和最后一行的结尾更改为匹配字符串中每一行的开头和结尾。

s
将字符串视为单行。也就是说,更改 "." 以匹配任何字符,甚至是换行符,通常它不会匹配。

作为/ms 一起使用,它们让"." 匹配任何字符,同时仍然允许“^”和“$”分别匹配字符串中换行符之后和之前。

(注意它没有提到/m 或/s 改变了'.' 以外的字符类,所以我们可以从这里推断它们没有被改变)

使用/xms 修饰符:

  1. 始终使用 /x 标志。
  2. 始终使用 /m 标志。
  3. 始终使用 /s 标志。

(Conway (2005),第 236-241 页,Vromans (2006))

Damian Conway (2005) Perl 最佳实践:开发可维护代码的标准和风格。奥莱利媒体。 https://www.amazon.com/Perl-Best-Practices-Developing-Maintainable/dp/0596001738/

Perl 最佳实践:参考指南:https://www.squirrel.nl/pub/PBP_refguide-1.02.00.pdf

示例:

use strict;
use warnings;
use feature qw( say );

my @strings = (
    "abcd\n",        # single-line string
    "abcd\nabcd\n",  # multi-line string (first string repeated twice)
    "abXd\nabcd\n",  # multi-line string, same as above, but missing first 'c'
    "abcd\nabXd\n",  # multi-line string, same as above, but missing first 'c'
);
my @regexes = ( '^([^c]*)(c.*?)$' );

foreach my $string ( @strings ) {
    foreach my $regex ( @regexes ) {
        my @matches;
        say "\n###";
        say "# \$string='$string'; \$regex='$regex'";
        
        @matches = map { "'$_'" } $string =~ /$regex/;
        say "regex_modifiers='';   \@matches=@matches;";
        
        @matches = map { "'$_'" } $string =~ /$regex/m;
        say "regex_modifiers='m';  \@matches=@matches;";
        
        @matches = map { "'$_'" } $string =~ /$regex/s;
        say "regex_modifiers='s';  \@matches=@matches;";
        
        @matches = map { "'$_'" } $string =~ /$regex/ms;
        say "regex_modifiers='ms'; \@matches=@matches;";
        
    }
}

输出:

###
# $string='abcd
'; $regex='^([^c]*)(c.*?)$'
regex_modifiers='';   @matches='ab' 'cd';   # ok
regex_modifiers='m';  @matches='ab' 'cd';   # /m, /s modifiers do not matter in single-line string
regex_modifiers='s';  @matches='ab' 'cd';   # /m, /s modifiers do not matter in single-line string
regex_modifiers='ms'; @matches='ab' 'cd';   # /m, /s modifiers do not matter in single-line string

###
# $string='abcd
abcd
'; $regex='^([^c]*)(c.*?)$'
regex_modifiers='';   @matches=;            # '.' does not match newline, cannot reach end of string
regex_modifiers='m';  @matches='ab' 'cd';   # '$' matches first newline
regex_modifiers='s';  @matches='ab' 'cd
abcd';                                      # '.' matches newline, so the end of string is reached
                                            # and '$' matches it.
regex_modifiers='ms'; @matches='ab' 'cd';   # non-greedy '.*?' causes '$' to match the first newline

###
# $string='abXd
abcd
'; $regex='^([^c]*)(c.*?)$'
regex_modifiers='';   @matches='abXd
ab' 'cd';                                    # [^c] matches newline, /m, /s modifiers do not matter 
regex_modifiers='m';  @matches='abXd
ab' 'cd';                                    # [^c] matches newline, /m, /s modifiers do not matter
regex_modifiers='s';  @matches='abXd
ab' 'cd';                                    # [^c] matches newline, /m, /s modifiers do not matter
regex_modifiers='ms'; @matches='abXd
ab' 'cd';                                    # [^c] matches newline, /m, /s modifiers do not matter

###
# $string='abcd
abXd
'; $regex='^([^c]*)(c.*?)$'
regex_modifiers='';   @matches=;             # '.' does not match newline, cannot reach end of string
regex_modifiers='m';  @matches='ab' 'cd';    # matches second line 
regex_modifiers='s';  @matches='ab' 'cd
abXd';                                       # '.' matches newline, so the end of string is reached
                                             # and '$' matches it.
regex_modifiers='ms'; @matches='ab' 'cd';    # non-greedy '.*?' causes '$' to match the first newline

【讨论】:

  • 为什么习惯性地使用/xms 修饰符?这似乎是一个很奇怪的选择。
  • @TLP 感谢您的评论!我第一次遇到这种做法时也觉得很奇怪,但尝试过并喜欢它。我澄清了来源并添加了对 Damian Conway 的“Perl 最佳实践”一书的引用和参考。我意识到这不是一种常见的做法(在 SO 上肯定不会如此),而是风格和个人喜好的问题。 :)
猜你喜欢
  • 1970-01-01
  • 2012-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-17
相关资源
最近更新 更多