11

我正在尝试将嵌套{}括号与 Perl 中的正则表达式匹配,以便我可以从文件中提取某些文本。这是我目前拥有的:

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

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

在某些时候,这可以按预期工作。例如,如果$str = "abc {{xyz} abc} {xyz}"我获得:

美国广播公司
{{xyz} abc}
{xyz}

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

{abc} {{xyz}}
美国广播公司

这不是我所期望的。我本来希望{abc}并且{{xyz}}处于不同的行中,因为每个行在括号方面都是独立的。我的正则表达式有问题吗?如果是这样,我将如何解决它?

4

7 回答 7

18

您对您的模式如何匹配感到惊讶,但没有人解释它?以下是您的模式匹配方式:

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

于 2013-03-08T20:57:49.310 回答
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

于 2013-03-08T20:01:17.783 回答
6

要在每个嵌套级别仅匹配一对嵌套括号,
但可以使用任意数量的级别,例如{1{2{3}}},您可以使用

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

要在任何嵌套级别可能有多个对时进行匹配,例如{1{2}{2}{2}},您可以使用

/(?>\{(?:[^{}]*|(?R))*\})|\w+/g

(?R)用于递归匹配整个模式 。

为了匹配包含在一对括号中的文本,引擎必须匹配(?:[^{}]*|(?R))*
即,[^{}]*或者(?R),零次或多次*

所以在 eg"{abc {def}}"中,开局"{"匹配后,[^{}]*将匹配 the "abc "(?R)将匹配 the "{def}",则匹配闭"}"包。

"{def}"匹配是因为(?R)它只是整个模式的缩写
(?>\{(?:[^{}]*|(?R))*\})|\w+,正如我们刚刚看到的,它将匹配 a"{"后跟文本匹配[^{}]*,然后再匹配"}".

原子分组(?>...)用于防止正则表达式引擎在匹配后回溯到括号中的文本。这对于确保正则表达式在找不到匹配项时快速失败非常重要。

于 2013-03-08T19:39:20.373 回答
5

你需要一个递归正则表达式。这应该有效:

my @matches;
push @matches, $1 while $str =~ /( [^{}\s]+ | ( \{ (?: [^{}]+ | (?2) )* \} ) )/xg;

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

my @matches = $str =~ /[^{}\s]+ | \{ (?: (?R) | [^{}]+ )+ \} /gx;
于 2013-03-08T19:39:04.877 回答
4

哇。这么简单的事情有一堆复杂的答案。

你遇到的问题是你在贪婪模式下匹配。也就是说,您正在使正则表达式引擎尽可能匹配,同时使表达式为真。

为避免贪婪匹配,只需添加一个“?” 在你的量词之后。这使得比赛尽可能短。

所以,我改变了你的表达方式:

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

到:

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

...现在它完全按照您的预期工作。

高温高压

弗朗西斯科

于 2013-03-15T13:15:39.517 回答
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}}
于 2013-03-08T20:00:07.517 回答
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被视为有效输入。

于 2013-03-08T19:35:18.037 回答