Co to są grupy równoważące wyrażenia regularne?


92

Właśnie czytałem pytanie o to, jak uzyskać dane w podwójnych nawiasach klamrowych ( to pytanie ), a potem ktoś poruszył grupy równoważące. Nadal nie jestem do końca pewien, czym one są i jak ich używać.

Przeczytałem definicję grupy balansującej , ale wyjaśnienie jest trudne do zrozumienia i nadal jestem dość zdezorientowany w pytaniach, które wspomniałem.

Czy ktoś mógłby po prostu wyjaśnić, czym są grupy równoważące i do czego są przydatne?


Zastanawiam się, ile języków regex jest faktycznie obsługiwanych.
Mike de Klerk

2
@MikedeKlerk Jest obsługiwany przynajmniej w silniku .NET Regex.
NIE jest.

Odpowiedzi:


175

O ile wiem, grupy równoważące są unikalne dla smaku regex .NET.

Poza tym: powtarzające się grupy

Po pierwsze, musisz wiedzieć, że .NET jest (znowu, o ile wiem) jedynym typem wyrażenia regularnego, który umożliwia dostęp do wielu przechwyceń jednej grupy przechwytywania (nie w odwołaniach wstecznych, ale po zakończeniu dopasowania).

Aby zilustrować to przykładem, rozważ wzór

(.)+

i sznurek "abcd".

we wszystkich innych odmianach wyrażeń regularnych grupa przechwytywania 1da po prostu jeden wynik: d(uwaga, pełne dopasowanie będzie oczywiście abcdzgodne z oczekiwaniami). Dzieje się tak, ponieważ każde nowe użycie grupy przechwytywania zastępuje poprzednie przechwytywanie.

Z drugiej strony .NET pamięta je wszystkie. I robi to w stosie. Po dopasowaniu powyższego wyrażenia regularnego, takiego jak

Match m = new Regex(@"(.)+").Match("abcd");

znajdziesz to

m.Groups[1].Captures

To element, CaptureCollectionktórego elementy odpowiadają czterem przechwyceniom

0: "a"
1: "b"
2: "c"
3: "d"

gdzie liczba jest indeksem do CaptureCollection. Zasadniczo więc za każdym razem, gdy grupa jest ponownie używana, na stos odkładany jest nowy bicie.

Staje się bardziej interesujące, jeśli używamy nazwanych grup przechwytywania. Ponieważ .NET pozwala na wielokrotne używanie tej samej nazwy, moglibyśmy napisać wyrażenie regularne, takie jak

(?<word>\w+)\W+(?<word>\w+)

aby umieścić dwa słowa w tej samej grupie. Ponownie, za każdym razem, gdy napotkana jest grupa o określonej nazwie, przechwycenie jest odkładane na jej stos. Więc stosując to wyrażenie regularne do danych wejściowych "foo bar"i sprawdzając

m.Groups["word"].Captures

znajdujemy dwa ujęcia

0: "foo"
1: "bar"

To pozwala nam nawet umieszczać rzeczy na jednym stosie z różnych części wyrażenia. Ale nadal jest to tylko funkcja .NET, która umożliwia śledzenie wielu przechwyceń, które są wymienione w tym artykule CaptureCollection. Ale powiedziałem, ta kolekcja to stos . Więc czy możemy z tego wyskoczyć ?

Enter: Balancing Groups

Okazuje się, że możemy. Jeśli użyjemy grupy podobnej do grupy (?<-word>...), to ostatnie przechwycenie jest zdejmowane ze stosu, wordjeśli podwyrażenie ...pasuje. Więc jeśli zmienimy nasze poprzednie wyrażenie na

(?<word>\w+)\W+(?<-word>\w+)

Następnie druga grupa wyskoczy z przechwytywania pierwszej grupy, a my CaptureCollectionna końcu otrzymamy pusty . Oczywiście ten przykład jest dość bezużyteczny.

Ale jest jeszcze jeden szczegół dotyczący składni minus: jeśli stos jest już pusty, grupa zawodzi (niezależnie od jej pod-wzorca). Możemy wykorzystać to zachowanie do liczenia poziomów zagnieżdżenia - i stąd pochodzi nazwa grupy równoważącej (i stąd robi się interesująca). Powiedzmy, że chcemy dopasować ciągi, które są poprawnie umieszczone w nawiasach. Wsuwamy każdy nawias otwierający na stos i usuwamy po jednym przechwyceniu dla każdego nawiasu zamykającego. Jeśli napotkamy jeden nawias zamykający za dużo, spróbuje zdjąć pusty stos i spowoduje niepowodzenie wzorca:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$

Mamy więc trzy możliwości w powtórzeniu. Pierwsza alternatywa pochłania wszystko, co nie jest nawiasem. Druga alternatywa dopasowuje (s, wpychając je na stos. Trzecia alternatywa pasuje do )s podczas zdejmowania elementów ze stosu (jeśli to możliwe!).

Uwaga: dla wyjaśnienia sprawdzamy tylko, czy nie ma niedopasowanych nawiasów! Oznacza to, że łańcuch nie zawierający w ogóle nawiasów będzie pasował, ponieważ nadal są one poprawne składniowo (w niektórych składniach, w których trzeba dopasować nawiasy). Jeśli chcesz zapewnić co najmniej jeden zestaw nawiasów, po prostu dodaj znak wyprzedzenia (?=.*[(])tuż po ^.

Ten wzór nie jest jednak doskonały (ani całkowicie poprawny).

Finał: wzorce warunkowe

Jest jeszcze jeden haczyk: nie gwarantuje to, że stos jest pusty na końcu łańcucha (stąd (foo(bar)byłby prawidłowy). NET (i wiele innych odmian) ma jeszcze jedną konstrukcję, która pomaga nam tutaj: wzorce warunkowe. Ogólna składnia to

(?(condition)truePattern|falsePattern)

gdzie falsePatternjest opcjonalne - jeśli zostanie pominięte, zawsze będzie pasować. Warunek może być wzorcem lub nazwą grupy przechwytywania. Skoncentruję się tutaj na tym drugim przypadku. Jeśli jest to nazwa grupy przechwytywania, truePatternjest używana wtedy i tylko wtedy, gdy stos przechwytywania dla tej konkretnej grupy nie jest pusty. Oznacza to, że wzorzec warunkowy, taki jak (?(name)yes|no)reads, "jeśli namedopasował i przechwycił coś (co nadal jest na stosie), użyj wzorca, w yesprzeciwnym razie użyj wzorca no".

Więc na końcu powyższego wzorca moglibyśmy dodać coś takiego, (?(Open)failPattern)co powoduje niepowodzenie całego wzorca, jeśli Open-stack nie jest pusty. Najprostszą rzeczą, która powoduje bezwarunkowe niepowodzenie wzorca, jest (?!)(puste negatywne spojrzenie w przód). Mamy więc nasz ostateczny wzór:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$

Zauważ, że ta warunkowa składnia nie ma per se nic wspólnego z równoważeniem grup, ale konieczne jest wykorzystanie ich pełnej mocy.

Stąd tylko niebo jest granicą. Możliwych jest wiele bardzo wyrafinowanych zastosowań i są pewne pułapki w połączeniu z innymi funkcjami .NET-Regex, takimi jak lookbehinds o zmiennej długości ( których sam musiałem się nauczyć ). Jednak główne pytanie zawsze brzmi: czy twój kod jest nadal możliwy do utrzymania podczas korzystania z tych funkcji? Musisz to naprawdę dobrze udokumentować i mieć pewność, że każdy, kto nad nim pracuje, jest również świadomy tych funkcji. W przeciwnym razie może być lepiej, po prostu przechodząc przez ciąg ręcznie znak po znaku i licząc poziomy zagnieżdżenia w liczbie całkowitej.

Dodatek: O co chodzi ze (?<A-B>...)składnią?

Kredyty za tę część należą do Kobi (zobacz jego odpowiedź poniżej, aby uzyskać więcej informacji).

Teraz, mając wszystko powyższe, możemy sprawdzić, czy łańcuch jest poprawnie umieszczony w nawiasach. Byłoby jednak o wiele bardziej przydatne, gdybyśmy mogli faktycznie uzyskać (zagnieżdżone) przechwytywania dla wszystkich zawartości tych nawiasów. Oczywiście moglibyśmy zapamiętać otwieranie i zamykanie nawiasów w osobnym stosie przechwytywania, który nie jest opróżniany, a następnie w oddzielnym kroku wykonać pewne wyodrębnianie podciągów na podstawie ich pozycji.

Ale .NET zapewnia jeszcze jedną wygodną funkcję: jeśli używamy (?<A-B>subPattern), nie tylko przechwytywanie jest usuwane ze stosu B, ale także wszystko między tym przechwyceniem Ba bieżącą grupą jest wypychane na stos A. Więc jeśli użyjemy takiej grupy jako nawiasów zamykających, podczas zdejmowania poziomów zagnieżdżenia z naszego stosu, możemy również wypchnąć zawartość pary na inny stos:

^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$

Kobi dostarczył to Live-Demo w swojej odpowiedzi

Biorąc wszystkie te rzeczy razem, możemy:

  • Zapamiętaj arbitralnie wiele ujęć
  • Sprawdź poprawność struktur zagnieżdżonych
  • Przechwytuj każdy poziom zagnieżdżenia

Wszystko w jednym wyrażeniu regularnym. Jeśli to nie jest ekscytujące ...;)

Niektóre zasoby, które okazały się pomocne, gdy po raz pierwszy się o nich dowiedziałem:



40

Tylko mały dodatek do doskonałej odpowiedzi M. Buettnera:

O co chodzi ze (?<A-B>)składnią?

(?<A-B>x)różni się nieco od (?<-A>(?<B>x)). Dają ten sam przepływ sterowania * , ale wychwytują inaczej.
Na przykład spójrzmy na wzór dla zrównoważonych szelek:

(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))

Pod koniec dopasowania mamy zrównoważony ciąg, ale to wszystko, co mamy - nie wiemy, gdzie są nawiasy klamrowe, ponieważ Bstos jest pusty. Ciężka praca, jaką wykonał za nas silnik, minęła.
( przykład na Regex Storm )

(?<A-B>x)jest rozwiązaniem tego problemu. W jaki sposób? To nie uchwycić xw $A: to oddaje zawartość między poprzednim wychwytywania Bi aktualnej pozycji.

Wykorzystajmy to w naszym wzorze:

(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))

Spowoduje to przechwycenie $Contentsznurków między klamrami (i ich pozycji) dla każdej pary po drodze.
Dla łańcucha {1 2 {3} {4 5 {6}} 7}nie byłoby cztery zrzuty: 3, 6, 4 5 {6}, i 1 2 {3} {4 5 {6}} 7- znacznie lepiej niż nic lub } } } }.
( przykład - kliknij tablezakładkę i spójrz ${Content}, przechwytuje )

W rzeczywistości można go używać bez balansowania w ogóle: (?<A>).(.(?<Content-A>).)przechwytuje pierwsze dwa znaki, nawet jeśli są oddzielone grupami.
(Lookahead jest tutaj częściej używany, ale nie zawsze jest skalowany: może powielać twoją logikę).

(?<A-B>)to mocna cecha - daje ci dokładną kontrolę nad twoimi zbiórek. Miej to na uwadze, gdy próbujesz wyciągnąć więcej ze swojego wzoru.


@FYI, kontynuując dyskusję z pytania, które Ci się nie podobało, w nowej odpowiedzi na to pytanie. :)
zx81

Próbuję wymyślić sposób wykonania sprawdzenia wyrażenia regularnego w zrównoważonych nawiasach klamrowych z ucieczką nawiasów klamrowych wewnątrz ciągów. EG następujący kod przejdzie: public class Foo {private const char BAR = '{'; prywatny ciąg _qux = "{{{"; } Czy ktoś to zrobił?
Pan Anderson

@MrAnderson - Wystarczy dodać |'[^']*'we właściwym miejscu: przykład . Jeśli potrzebujesz również znaków ucieczki, jest tutaj przykład: (Regex dla pasujących literałów ciągu C #) [ stackoverflow.com/a/4953878/7586] .
Kobi
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.