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 1
da po prostu jeden wynik: d
(uwaga, pełne dopasowanie będzie oczywiście abcd
zgodne 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, CaptureCollection
któ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, word
jeś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 CaptureCollection
na 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 falsePattern
jest 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, truePattern
jest 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 name
dopasował i przechwycił coś (co nadal jest na stosie), użyj wzorca, w yes
przeciwnym 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 B
a 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: