Perl正则表达式:匹配嵌套的括号

11
我正试图在Perl中使用正则表达式匹配嵌套的{}括号,以便从文件中提取特定的文本片段。目前我的代码如下:
my @matches = $str =~ /\{(?:\{.*\}|[^\{])*\}|\w+/sg;

foreach (@matches) {
    print "$_\n";
}

有时它按预期工作。例如,如果$str = "abc {{xyz} abc} {xyz}",我会得到:

abc
{{xyz} abc}
{xyz}

如预期的那样。但对于其他输入字符串,它不像预期的那样运行。例如,如果$str = "{abc} {{xyz}} abc",输出是:

{abc} {{xyz}}
abc

这不是我期望的结果。由于每个括号都是平衡的,我希望{abc}{{xyz}}在单独的行上。我的正则表达式是否存在问题?如果是,我该如何解决?


我相信前瞻可以帮助。 - i--
7个回答

19

你很惊讶你的模式匹配上了,但没有人给你解释吗?以下是你的模式匹配方式:

my @matches = $str =~ /\{(?:\{.*\}|[^{])*\}|\w+/sg;
                       ^    ^ ^ ^  ^      ^
                       |    | | |  |      |
{ ---------------------+    | | |  |      |
a --------------------------)-)-)--+      |
b --------------------------)-)-)--+      |
c --------------------------)-)-)--+      |
} --------------------------)-)-)--+      |
  --------------------------)-)-)--+      |
{ --------------------------+ | |         |
{ ----------------------------+ |         |
x ----------------------------+ |         |
y ----------------------------+ |         |
z ----------------------------+ |         |
} ------------------------------+         |
} ----------------------------------------+

正如您所看到的,问题在于“/\{.*\}/”匹配了太多内容。应该有一个能够匹配“

”的东西存在其中。

(?: \s* (?: \{ ... \} | \w+ ) )*

其中...代表的是

(?: \s* (?: \{ ... \} | \w+ ) )*

所以你需要一些递归。命名组是实现这一目的的简单方法。

say $1
   while /
      \G \s*+ ( (?&WORD) | (?&BRACKETED) )

      (?(DEFINE)
         (?<WORD>      \s* \w+ )
         (?<BRACKETED> \s* \{ (?&TEXT)? \s* \} )
         (?<TEXT>      (?: (?&WORD) | (?&BRACKETED) )+ )
      )
   /xg;

但是,为什么不使用Text::Balanced呢?这样可以避免重复造轮子。


1
哇,你是怎么制作那个 ASCII 图像的?你用了什么工具吗? - Alex Gordon
5
我用手完成了它。 - ikegami
1
@Артём Царионов,那很容易。编写底部的代码花费了更多时间。你可能会喜欢YAPE::Regex::Explain - ikegami
它可以匹配像 abc {{xyz} abc} {xyz} 这样的字符串,就像 OP 所要求的那样。它的实现非常易读。// 我不仅提供了正则表达式,还包括使用它的代码。 - ikegami
2
@bashophil,它默认为$_。如果您愿意,可以自由匹配$str - ikegami
显示剩余5条评论

14

perlfaq5中详细介绍了匹配平衡和嵌套定界符的问题,包括(?PARNO)Regexp::Common等所有选项,我将让它们来涵盖这些内容。

但是,匹配平衡项目很棘手,容易出错,除非您真的想学习和维护高级正则表达式,否则请使用模块。幸运的是,有Text::Balanced来处理此类问题,而且远不止于此。它是平衡文本匹配的瑞士军刀。

不幸的是,它无法处理带括号定界符的转义字符

use v5.10;
use strict;
use warnings;

use Text::Balanced qw(extract_multiple extract_bracketed);

my @strings = ("abc {{xyz} abc} {xyz}", "{abc} {{xyz}} abc");

for my $string (@strings) {
    say "Extracting from $string";

    # Extract all the fields, rather than one at a time.
    my @fields = extract_multiple(
        $string,
        [
            # Extract {...}
            sub { extract_bracketed($_[0], '{}') },
            # Also extract any other non whitespace
            qr/\S+/
        ],
        # Return all the fields
        undef,
        # Throw out anything which does not match
        1
    );

    say join "\n", @fields;
    print "\n";
}

你可以把extract_multiple看作是一个更通用和强大的split


6

要匹配只有一个括号对,但是嵌套级别可以任意多的嵌套括号,例如{1{2{3}}},您可以使用以下方法

/\{[^}]*[^{]*\}|\w+/g

为了匹配任意嵌套层级中可能存在多个对的情况,例如{1{2}{2}{2}},您可以使用以下代码:
/(?>\{(?:[^{}]*|(?R))*\})|\w+/g
(?R)被用于递归匹配整个模式。
为了匹配括号内的文本,引擎必须匹配(?:[^{}]*|(?R))* ,即要么匹配[^{}]*,要么匹配(?R)零次或多次*
所以,在例如"{abc {def}}"中,在匹配开头的"{"之后,[^{}]*将匹配"abc ",而(?R)将匹配"{def}",然后匹配结尾的"}""{def}"是匹配的,因为(?R)简单地代表整个模式(?>\{(?:[^{}]*|(?R))*\})|\w+,正如我们刚才看到的,它将匹配一个"{",后跟匹配[^{}]*的文本,再跟着一个"}"
原子组合(?>...)用于防止正则表达式引擎在匹配括号内文本后进行回溯。这对于确保正则表达式能够快速失败(如果无法找到匹配项)非常重要。

递归是处理任意层数嵌套的方法。如果已知最大嵌套层数,则不必要使用,否则必须使用。 - nhahtdh
这似乎不能处理输入 "{{} {}}"(它应该匹配所有内容)。 - arshajii
@A.R.S. 为这样的输入添加了递归版本。 - MikeM
@nhahtdh。这个问题没有指定多个嵌套级别的同一层次的多个对,我的第一个正则表达式可以处理任意数量的嵌套级别,但不能处理同一层次的多个对。这就是为什么我说在这种情况下递归是不必要的。 - MikeM
@nhahtdh 是的,我指的是他这样做之前我早先的评论。说得好,所以我已经把“+”改成了“*”。 - MikeM
显示剩余2条评论

5
您需要一个递归正则表达式。以下内容应该可以工作:
my @matches;
push @matches, $1 while $str =~ /( [^{}\s]+ | ( \{ (?: [^{}]+ | (?2) )* \} ) )/xg;

或者,如果你更喜欢非循环版本:

my @matches = $str =~ /[^{}\s]+ | \{ (?: (?R) | [^{}]+ )+ \} /gx;

谢谢你的回答。我还想匹配一个\w+,那么我只需要在末尾添加|\w+吗? - arshajii
我已经修复了它,以允许那样做。 - Borodin
这似乎在奇怪地复制所有匹配项。 - arshajii
@A.R.S.:你需要像我回答中的循环。 - nhahtdh
1
@A.R.S.:问题在于列表上下文中的正则表达式将返回所有捕获内容。它只是编写“while”循环的简洁方式。我已经将我的正则表达式放入上下文中,以便您可以看到它的工作原理。 - Borodin
显示剩余3条评论

4

哇,这个简单的问题却有一堆复杂的答案。

您遇到的问题是使用了贪婪模式匹配。也就是说,您让正则表达式引擎尽可能多地匹配以满足表达式。

为了避免贪婪匹配,只需在量词后面添加“?”即可使匹配尽可能短。

因此,我将您的表达式改为:

my @matches = $str =~ /\{(?:\{.*\}|[^\{])*\}|\w+/sg;

致:

my @matches = $str =~ /\{(?:\{.*?\}|[^\{])*?\}|\w+/sg;

...现在它的工作方式与您的预期完全一致。

希望对您有所帮助。

Francisco


2

使用内置模块 Text::Balanced 的一种方法。

script.pl 的内容:

#!/usr/bin/env perl

use warnings;
use strict;
use Text::Balanced qw<extract_bracketed>;

while ( <DATA> ) { 

    ## Remove '\n' from input string.
    chomp;

    printf qq|%s\n|, $_; 
    print "=" x 20, "\n";


    ## Extract all characters just before first curly bracket.
    my @str_parts = extract_bracketed( $_, '{}', '[^{}]*' );

    if ( $str_parts[2] ) { 
        printf qq|%s\n|, $str_parts[2];
    }   

    my $str_without_prefix = "@str_parts[0,1]";


    ## Extract data of balanced curly brackets, remove leading and trailing
    ## spaces and print.
    while ( my $match = extract_bracketed( $str_without_prefix, '{}' ) ) { 
        $match =~ s/^\s+//;
        $match =~ s/\s+$//;
        printf qq|%s\n|, $match;

    }   

    print "\n";
}

__DATA__
abc {{xyz} abc} {xyz}
{abc} {{xyz}} abc

使用以下方式运行:

perl script.pl

这将产生:

abc {{xyz} abc} {xyz}
====================
abc 
{{xyz} abc}
{xyz}

{abc} {{xyz}} abc
====================
{abc}
{{xyz}}

1
我正要开始回答类似这样的问题!在这里可以看到一个很好的例子,一个小型的TeX解析器:https://github.com/jberger/MakeBeamerInfo/blob/master/lib/App/makebeamerinfo.pm#L230 - Joel Berger

1

只需稍微修改和扩展经典解决方案即可:

(\{(?:(?1)|[^{}]*+)++\})|[^{}\s]++

演示(这是在PCRE中。当涉及到递归正则表达式时,其行为与Perl略有不同,但我认为对于这种情况应该产生相同的结果)。

经过一番努力(我不熟悉Perl!),这是ideone上的演示。 $&指的是整个正则表达式匹配的字符串。

my $str = "abc {{xyz} abc} {xyz} {abc} {{xyz}} abc";

while ($str =~ /(\{(?:(?1)|[^{}]*+)++\})|[^{}\s]++/g) {
    print "$&\n"
}

请注意,此解决方案假定输入是有效的。对于无效输入,它将表现得相当随机。可以稍微修改它以在遇到无效输入时停止。为此,我需要更多关于输入格式的详细信息(最好作为语法),例如是否认为abc {xyz} asd 是有效输入。

@Borodin:一些优化以防止回溯。尽管如此,它假定字符串是有效的。 - nhahtdh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接