2013-03-08 13 views
5

Próbuję dopasować nawiasy zagnieżdżone z wyrażeń regularnych w Perlu, aby móc wyodrębnić określone fragmenty tekstu z pliku. To jest to, co mam obecnie:Wyrażenie regularne Perl: dopasuj zagnieżdżone nawiasy

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

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

W pewnych okresach To działa zgodnie z oczekiwaniami. Na przykład, jeśli otrzymam: $str = "abc {{xyz} abc} {xyz}", otrzymam:

 
abc 
{{xyz} abc} 
{xyz} 

zgodnie z oczekiwaniami. Ale w przypadku innych ciągów wejściowych nie działa zgodnie z oczekiwaniami. Na przykład, jeśli $str = "{abc} {{xyz}} abc", wyjście to:

 
{abc} {{xyz}} 
abc 

co nie jest tym, czego się spodziewałem. Chciałbym, aby {abc} i {{xyz}} były w osobnych liniach, ponieważ każda z nich jest równoważona sama w sobie w nawiasach. Czy jest jakiś problem z moim regularnym wyrażeniem? Jeśli tak, jak mam to naprawić?

+0

wierzę [uprzedzona] (http://www.regular-expressions.info/lookaround.html) mógłby pomóc –

Odpowiedz

11

Byłeś zaskoczony, jak twój wzór pasował, ale nikt tego nie wyjaśnił? Oto w jaki sposób wzorzec dopasowania:

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

Jak widać, problem jest, że/\{.*\}/pasuje zbytnio. Jaki powinien być tam jest coś, co pasuje

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

gdzie ... jest

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

Więc trzeba trochę rekursji. Wymienione grupy są łatwym sposobem na zrobienie tego.

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

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

Ale zamiast odkrywać nowe koło, dlaczego nie używać Text::Balanced.

+0

wow, w jaki sposób stworzyłeś ten pierwszy obraz ASCII? użyłeś narzędzia? –

+3

@ Артём Царионов, Zrobiłem to ręcznie. – ikegami

+0

wow. musisz mieć dużo czasu na swoich rękach! –

1

Podobnie modyfikuje i rozszerza classic solution nieco:

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

Demo (To jest w PCRE zachowanie różni się nieco od Perl, jeśli chodzi o rekurencyjnego regex, ale myślę, że powinno produkować ten sam rezultat. w tym przypadku).

Po pewnym zmaganiu (nie znam Perla!), Jest to wersja demo pod numerem ideone. $& odnosi się do ciągu pasującego do całego wyrażenia regularnego.

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

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

Należy pamiętać, że to rozwiązanie zakłada, że ​​dane wejściowe są prawidłowe. Zachowa się raczej losowo na nieprawidłowych danych wejściowych. Można go lekko zmodyfikować, aby zatrzymał się, gdy napotkano nieprawidłowe dane wejściowe. W tym celu potrzebuję więcej szczegółów na temat formatu wejściowego (najlepiej jako gramatyki), na przykład, czy abc{xyz}asd jest uważane za prawidłowe wejście, czy nie.

+0

Jakie są '' ++ dla? – Borodin

+0

@Borodin: Pewna optymalizacja, aby zapobiec cofaniu. Zakłada jednak, że łańcuch jest poprawny. – nhahtdh

4

Potrzebujesz rekursywnego wyrażenia regularnego. To powinno działać:

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

lub, jeśli wolisz wersję non-loop:

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

Dzięki za odpowiedź. Miałem też '\ w +', które chciałem dopasować, więc czy mogę po prostu dopisać '| \ w +' na końcu? – arshajii

+0

Naprawiłem to, aby pozwolić na to – Borodin

+0

Wydaje się to dziwnie powielać wszystkie mecze. – arshajii

2

Aby dopasować wsporniki zagnieżdżonych z tylko jednej pary na każdym poziomie zagnieżdżenia,
ale dowolną liczbę poziomów np {1{2{3}}}, można użyć

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

Aby dopasować gdy może istnieć wiele par na każdym poziomie zagnieżdżenia, np {1{2}{2}{2}}, można użyć

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

(?R) służy do dopasowywania cały wzór rekurencyjnie.

przez tekst zawarty w parę wsporników silnik musi dopasować (?:[^{}]*|(?R))*,
znaczy albo [^{}]* lub (?R) zero lub więcej razy *.

Tak np."{abc {def}}", po otwarciu "{" zostanie dopasowany, [^{}]* będzie pasować do "abc ", a (?R) będzie pasować do "{def}", a następnie zamknięcie "}" zostanie dopasowane.

"{def}" jest dopasowany bo (?R) jest po prostu skrót całego wzoru
(?>\{(?:[^{}]*|(?R))*\})|\w+, która jak już widzieliśmy dopasuje "{" następnie tekstu pasującego [^{}]*, a następnie "}".

Grupowanie Atomowe (?> ... ) służy do zapobiegania wczytywaniu silnika regex do tekstu w nawiasach po dopasowaniu. Jest to ważne, aby zapewnić, że wyrażenie regularne szybko się zawiesza, jeśli nie może znaleźć dopasowania.

+0

Rekurencja ma zająć się dowolną liczbą poziomów zagnieżdżania. Nie jest konieczne, jeśli znana jest maksymalna liczba poziomów zagnieżdżenia, ale należy ją wykorzystać w inny sposób. – nhahtdh

+0

Wydaje się, że nie działa to dla wejścia '' {{}}}} (które powinno pasować do wszystkich). – arshajii

+0

@ A.R.S. Dodano rekursywną wersję takiego wejścia. – MikeM

1

Jeden sposób korzystania z wbudowanego modułu Text::Balanced.

Zawartość 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 

Run to lubią:

perl script.pl 

że plony:

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

{abc} {{xyz}} abc 
==================== 
{abc} 
{{xyz}} 
+0

Właśnie miałem rozpocząć taką odpowiedź! Zobacz przykład na wolności tutaj, mały parser TeX: https://github.com/jberger/MakeBeamerInfo/blob/master/lib/App/makebeamerinfo.pm#L230 –

10

Problem dopasowania wyważone i zagnieżdżonych ograniczników jest pokryta perlfaq5 i I Zostaw to im, aby objąć wszystkie opcje, w tym (?PARNO) i Regexp::Common.

Ale dopasowywanie zrównoważonych elementów jest trudne i podatne na błędy, chyba że naprawdę chcesz uczyć się i utrzymywać zaawansowane wyrażenia, zostaw to modułowi. Na szczęście istnieje Text::Balanced do obsługi tego i tak wiele więcej. Jest to szwajcarska piła łańcuchowa o zrównoważonym dopasowywaniu tekstu.

Niestety it does not handle escaping on bracketed delimiters.

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"; 
} 

Można myśleć o extract_multiple jak bardziej ogólne i potężny split.

1

Wow. Co za garść skomplikowanych odpowiedzi na coś tak prostego.

Problem polega na tym, że pasujesz do trybu chciwości. Oznacza to, że uruchamiasz silnik regex, aby dopasować jak najwięcej, jednocześnie czyniąc wyrażenie prawdziwym.

Aby uniknąć chciwego meczu, po prostu dodaj znak "?" po kwantyfikatorze. To sprawia, że ​​mecz jest tak krótki, jak to tylko możliwe.

Tak, zmieniłem swój wyraz od:

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

Do:

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

... a teraz to działa dokładnie tak, jak oczekujesz.

HTH

Francisco