Nie trzeba dodawać, że TAK! Z całą pewnością możesz napisać wzorzec wyrażenia regularnego Java, aby dopasować a n b n . Używa pozytywnego wyprzedzenia dla potwierdzenia i jednego zagnieżdżonego odniesienia do „zliczania”.
Zamiast natychmiast podawać wzór, ta odpowiedź poprowadzi czytelników przez proces jego wyprowadzania. Podawane są różne wskazówki, ponieważ rozwiązanie jest powoli konstruowane. W tym aspekcie mam nadzieję, że ta odpowiedź będzie zawierała znacznie więcej niż tylko kolejny zgrabny wzorzec wyrażenia regularnego. Miejmy nadzieję, że czytelnicy nauczą się również, jak „myśleć w regex” i jak harmonijnie łączyć różne konstrukcje, aby w przyszłości mogli samodzielnie wyprowadzić więcej wzorców.
Językiem używanym do opracowania rozwiązania będzie PHP ze względu na zwięzłość. Ostatni test po sfinalizowaniu wzorca zostanie wykonany w języku Java.
Krok 1: Wypatruj asercji
Zacznijmy od prostszego problemu: chcemy dopasować a+
na początku łańcucha, ale tylko wtedy, gdy zaraz po nim następuje b+
. Możemy użyć ^
do zakotwiczenia naszego dopasowania, a ponieważ chcemy dopasować tylko a+
bezb+
znaku , możemy użyć asercji lookahead(?=…)
.
Oto nasz wzór z prostą wiązką testową:
function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
Dane wyjściowe to ( jak widać na ideone.com ):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
To jest dokładnie to, czego szukamy: dopasowujemy a+
, tylko jeśli znajduje się na początku ciągu i tylko wtedy, gdy bezpośrednio po nim następuje b+
.
Lekcja : do tworzenia asercji można używać wzorców w obejrzeniach.
Krok 2: Przechwytywanie z wyprzedzeniem (i swobodnym - tryb odstępów)
Teraz powiedzmy, że chociaż nie chcemy, b+
aby był częścią dopasowania, i tak chcemy go przechwycić do grupy 1. Ponadto, ponieważ przewidujemy bardziej skomplikowany wzorzec, użyjmy x
modyfikatora do swobodnych odstępów więc może uczynić nasze wyrażenie regularne bardziej czytelnym.
Opierając się na naszym poprzednim fragmencie kodu PHP, mamy teraz następujący wzorzec:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
Wynik to teraz ( jak widać na ideone.com ):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
Zauważ, że np. aaa|b
Jest wynikiem join
-ing, czym każda grupa przechwyciła '|'
. W tym przypadku grupa 0 (tj. Co pasował do wzorca) przechwycona aaa
, a grupa 1 przechwycona b
.
Lekcja : Możesz uchwycić wewnątrz widok wokół. Aby poprawić czytelność, możesz użyć wolnych odstępów.
Krok 3: refaktoryzacja wyprzedzenia do „pętli”
Zanim będziemy mogli wprowadzić nasz mechanizm liczenia, musimy dokonać jednej modyfikacji naszego wzoru. Obecnie antycypowanie znajduje się poza +
„pętlą” powtórzeń. Jak dotąd jest to w porządku, ponieważ chcieliśmy tylko zapewnić, że istnieje b+
śledzenie naszego a+
, ale tak naprawdę chcemy ostatecznie zapewnić, że dla każdego a
dopasowanego elementu wewnątrz „pętli” jest odpowiadającyb
.
Nie martwmy się na razie o mechanizm liczenia i po prostu zróbmy refaktoryzację w następujący sposób:
- Pierwszy Refactor
a+
do (?: a )+
(uwaga:(?:…)
to grupa non-przechwytywania)
- Następnie przesuń antycypację wewnątrz tej nieprzechwytywanej grupy
- Zauważ, że musimy teraz „pominąć”,
a*
zanim będziemy mogli „zobaczyć” b+
, więc odpowiednio zmodyfikuj wzór
Mamy więc teraz następujące rzeczy:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
Dane wyjściowe są takie same jak wcześniej ( jak widać na ideone.com ), więc nie ma zmian w tym względzie. Ważną rzeczą jest to, że teraz robimy twierdzenie przy każdej iteracji w +
„pętli”. Przy naszym obecnym wzorcu nie jest to konieczne, ale w następnej kolejności sprawimy, że grupa 1 będzie „liczyć” się za nas, używając samoodniesienia.
Lekcja : Możesz chwytać wewnątrz grupy, która nie jest przejmowana. Lookarounds można powtórzyć.
Krok 4: To jest krok, od którego zaczynamy liczyć
Oto, co zamierzamy zrobić: przepisujemy grupę 1 w taki sposób, że:
- Pod koniec pierwszej iteracji
+
, kiedy pierwszya
jest dopasowana, powinna przechwycićb
- Pod koniec drugiej iteracji, kiedy inna
a
jest dopasowana, powinna przechwycićbb
- Pod koniec trzeciej iteracji powinien przechwycić
bbb
- ...
- Pod koniec n- tej iteracji grupa 1 powinna przechwycić b n
- Jeśli nie ma ich wystarczająco dużo
b
do zaliczenia do grupy 1, to twierdzenie po prostu zawodzi
Tak więc grupa 1, która jest teraz (b+)
, będzie musiała zostać przepisana na coś podobnego (\1 b)
. Oznacza to, że próbujemy „dodać” a b
do grupy 1 przechwyconej w poprzedniej iteracji.
Występuje tutaj niewielki problem polegający na tym, że w tym wzorcu brakuje „przypadku podstawowego”, tj. Przypadku, w którym można go dopasować bez odniesienia do siebie. Podstawowy przypadek jest wymagany, ponieważ grupa 1 zaczyna się „niezainicjowana”; nie przechwycił jeszcze niczego (nawet pustego łańcucha), więc próba odwołania się zawsze kończy się niepowodzeniem.
Jest wiele sposobów obejścia tego problemu, ale na razie ustawmy dopasowywanie odniesień jako opcjonalne , tj \1?
. To może, ale nie musi, działać idealnie, ale zobaczmy, co to robi, a jeśli wystąpi jakiś problem, przejdziemy przez ten most, kiedy do niego dojdziemy. Ponadto dodamy więcej przypadków testowych, gdy już to zrobimy.
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
Wynik to teraz ( jak widać na ideone.com ):
aaa 0
aaab 1 aaa|b # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b # yes!
aabb 1 aa|bb # YES!!
aaabbbbb 1 aaa|bbb # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....
Aha! Wygląda na to, że jesteśmy teraz naprawdę blisko rozwiązania! Udało nam się zmusić grupę 1 do „liczenia” za pomocą odniesienia do siebie! Ale czekaj ... coś jest nie tak z drugim i ostatnim przypadkiem testowym !! Nie ma wystarczająco dużo b
i jakoś źle się liczy! W następnym kroku zbadamy, dlaczego tak się stało.
Lekcja : Jednym ze sposobów „zainicjowania” grupy odwołującej się do siebie jest uczynienie dopasowywania samoodniesień opcjonalnym.
Krok 4½: Zrozumienie, co poszło nie tak
Problem polega na tym, że skoro ustawiliśmy dopasowywanie odniesień jako opcjonalne, „licznik” może „zresetować” z powrotem do 0, gdy nie ma wystarczającej liczby wartości b
. Przyjrzyjmy się dokładnie, co dzieje się w każdej iteracji naszego wzorca aaaaabbb
jako dane wejściowe.
a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
_
a a a a a b b b
↑
# 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
# so it matched and captured just b
___
a a a a a b b b
↑
# 2nd iteration: Group 1 matched \1b and captured bb
_____
a a a a a b b b
↑
# 3rd iteration: Group 1 matched \1b and captured bbb
_
a a a a a b b b
↑
# 4th iteration: Group 1 could still match \1, but not \1b,
# (!!!) so it matched and captured just b
___
a a a a a b b b
↑
# 5th iteration: Group 1 matched \1b and captured bb
#
# No more a, + "loop" terminates
Aha! W naszej czwartej iteracji nadal mogliśmy dopasować \1
, ale nie mogliśmy dopasować \1b
! Ponieważ pozwalamy, aby dopasowywanie odniesień było opcjonalne \1?
, silnik cofa się i wybrał opcję „nie, dziękuję”, która pozwala nam dopasować i przechwycić tylkob
!
Zwróć jednak uwagę, że z wyjątkiem pierwszej iteracji, zawsze możesz dopasować tylko samo odniesienie \1
. Jest to oczywiście oczywiste, ponieważ właśnie to uchwyciliśmy w naszej poprzedniej iteracji, aw naszej konfiguracji zawsze możemy to ponownie dopasować (np. Jeśli przechwyciliśmy bbb
ostatnim razem, mamy gwarancję, że nadal będzie bbb
, ale może lub może nie być bbbb
tym razem).
Lekcja : Uważaj na wycofywanie się. Silnik wyrażeń regularnych wykona tyle operacji cofania, ile pozwolisz, aż do dopasowania danego wzorca. Może to wpłynąć na wydajność (tj. Katastroficzne wycofywanie się ) i / lub poprawność.
Krok 5: Samoposiadanie na ratunek!
„Poprawka” powinna być teraz oczywista: połącz opcjonalne powtórzenia z kwantyfikatorem zaborczym . To znaczy, zamiast po prostu ?
, użyj ?+
zamiast tego (pamiętaj, że powtórzenie, które jest określone ilościowo jako zaborcze, nie cofa się, nawet jeśli taka „współpraca” może skutkować dopasowaniem ogólnego wzorca).
W warunkach bardzo nieformalne, to co ?+
, ?
i ??
mówi:
?+
- (opcjonalnie) „Nie musi tam być”,
- (zaborczy) "ale jeśli tam jest, musisz to wziąć i nie puścić!"
?
- (opcjonalnie) „Nie musi tam być”,
- (chciwy) „ale jeśli tak, możesz to na razie wziąć”
- (cofam się) „ale możesz zostać poproszony o puszczenie tego później!”
??
- (opcjonalnie) „Nie musi tam być”,
- (niechętnie) „a nawet jeśli tak jest, nie musisz jeszcze tego brać”
- (cofam się) „ale możesz zostać poproszony o zrobienie tego później!”
W naszej konfiguracji \1
nie będzie go za pierwszym razem, ale zawsze będzie dostępny w dowolnym momencie później i zawsze chcemy to dopasować. W ten \1?+
sposób osiągnęlibyśmy dokładnie to, czego chcemy.
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Teraz wynik to ( jak widać na ideone.com ):
aaa 0
aaab 1 a|b # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb # Hurrahh!!!
Voilà !!! Problem rozwiązany!!! Teraz liczymy poprawnie, dokładnie tak, jak chcemy!
Lekcja : poznaj różnicę między chciwym, niechętnym i zaborczym powtarzaniem. Opcjonalnie-zaborczy może być potężną kombinacją.
Krok 6: Ostatnie poprawki
Więc to, co teraz mamy, to wzorzec, który pasuje a
wielokrotnie, a dla każdego, a
który został dopasowany, znajduje się odpowiedni b
przechwycony w grupie 1. +
Kończy się, gdy nie ma więcej a
, lub jeśli asercja nie powiodła się, ponieważ nie ma odpowiednika b
dla an a
.
Aby zakończyć pracę, wystarczy dołączyć do naszego wzoru \1 $
. To jest teraz odniesienie wsteczne do dopasowanej grupy 1, po którym następuje koniec zakotwiczenia linii. Kotwica zapewnia, że w ciągu nie ma żadnych dodatkowych b
znaków; innymi słowy, w rzeczywistości mamy a n b n .
Oto sfinalizowany wzorzec z dodatkowymi przypadkami testowymi, w tym jeden o długości 10 000 znaków:
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
'', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
str_repeat('a', 5000).str_repeat('b', 5000)
);
$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Okazuje 4 mecze: ab
, aabb
, aaabbb
, a do 5000 b 5000 . Uruchomienie w witrynie ideone.com zajmuje tylko 0,06 sekundy .
Krok 7: Test Java
Tak więc wzorzec działa w PHP, ale ostatecznym celem jest napisanie wzorca działającego w Javie.
public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"", // false
"ab", // true
"abb", // false
"aab", // false
"aabb", // true
"abab", // false
"abc", // false
repeat('a', 5000) + repeat('b', 4999), // false
repeat('a', 5000) + repeat('b', 5000), // true
repeat('a', 5000) + repeat('b', 5001), // false
};
for (String test : tests) {
System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN));
}
}
static String repeat(char ch, int n) {
return new String(new char[n]).replace('\0', ch);
}
Wzór działa zgodnie z oczekiwaniami ( jak widać na ideone.com ).
A teraz dochodzimy do wniosku ...
Trzeba powiedzieć, że a*
antycypacja, a nawet „główna +
pętla”, pozwalają na cofanie. Czytelników zachęca się do potwierdzenia, dlaczego nie stanowi to problemu pod względem poprawności i dlaczego jednocześnie sprawiłoby, że oba zaborcze również zadziałałyby (chociaż być może mieszanie obowiązkowego i nieobowiązkowego kwantyfikatora zaborczego w tym samym wzorze może prowadzić do nieporozumień).
Należy również powiedzieć, że chociaż fajnie jest, że istnieje wzorzec wyrażenia regularnego, który będzie pasował do a n b n , nie zawsze jest to „najlepsze” rozwiązanie w praktyce. Dużo lepszym rozwiązaniem jest po prostu dopasowanie^(a+)(b+)$
, a następnie porównanie długości ciągów przechwyconych przez grupy 1 i 2 w języku programowania hostującego.
W PHP może to wyglądać mniej więcej tak ( jak widać na ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
Celem tego artykułu NIE jest przekonanie czytelników, że regex może zrobić prawie wszystko; najwyraźniej nie może, a nawet jeśli chodzi o rzeczy, które może zrobić, należy rozważyć przynajmniej częściowe delegowanie do języka hostującego, jeśli prowadzi to do prostszego rozwiązania.
Jak wspomniano na górze, podczas gdy ten artykuł jest koniecznie oznaczony tagami [regex]
dla przepełnienia stosu, być może chodzi o coś więcej. Chociaż z pewnością nauka o twierdzeniach, zagnieżdżonych odniesieniach, kwantyfikatorze zaborczym itp. Ma wartość, być może większą lekcją jest proces twórczy, za pomocą którego można spróbować rozwiązać problemy, determinację i ciężką pracę, których często wymaga, gdy jesteś poddawany różne ograniczenia, systematyczny skład z różnych części, aby zbudować działające rozwiązanie itp.
Dodatkowy materiał! Wzorzec rekurencyjny PCRE!
Ponieważ wprowadziliśmy PHP, trzeba powiedzieć, że PCRE obsługuje wzorce rekurencyjne i podprogramy. Tak więc następujący wzorzec działa dla preg_match
( jak widać na ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
Obecnie regex Javy nie obsługuje wzorca rekurencyjnego.
Jeszcze więcej materiału bonusowego! Dopasowywanie a n b n c n !!
Tak więc widzieliśmy jak dopasować do n b n , która jest nieregularna, ale nadal bezkontekstowych, ale możemy również dopasować do n b n c n , która nie jest jeszcze wolny od kontekstu?
Odpowiedź oczywiście brzmi TAK! Zachęcamy czytelników do samodzielnego rozwiązania tego problemu, ale rozwiązanie jest podane poniżej (z implementacją w Javie na ideone.com ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $