[JS] Problem w wyrażeniu regularnym

0

Witam,

Napisałem następujący kawałek kodu:

<html>
	<head>
		<script type="text/javascript">
var myRe = new RegExp("expander\[(.+)\]");
var str = "expander[menu$1]";
var myArray;
for (var i = 0; (myArray = myRe.exec(str)) != null; i++)
{
  var msg = "Found " + myArray[0] + ".  ";
  msg += "Next match starts at " + myRe.lastIndex;
  alert(msg);
  
  if (i == 4) {
    break;
  }
}
		</script>
	</head>
	
	
	<body>
	</body>
</html>

Problem w tym, że nie znajduje mi danych. Wyrażenie testowałem także w Designerze i tutaj nie miałem z nim problemów.

Pozdrawiam,
Grzegorz

0

Problem leży w zachłanności operatorów w wyrażeniach regularnych. Co to jest ta zachłanność? Ano to, że każde podwyrażenie (po kolei) próbuje dopasować możliwie największą ilość tekstu.

Przykładowo u Ciebie kropka będzie dopasowywała (zżerała, jak to się mówi w profesjonalnych kręgach ;-)) każdy znak tekstu. Zeżre po prostu całą resztę tekstu, łącznie z nawiasem zamykającym ]. Bo takie podwyrażenie:

(.+)

mówi: zżeraj od tego miejsca dowolne znaki, przy czym musisz zeżreć co najmniej jeden znak, by nastąpiło dopasowanie. Nie ma natomiast żadnego ograniczenia co do górnej liczby znaków. Ani tego, że ma nie zżerać nawiasów zamykających (]). Czyli zeżre to nie tylko znaki:

m
e
n
u
$
1

ale również zeżre znak:

]

Bo niby czemu nie? Nie powiedziałeś, żeby oszczędził ten znak, tylko że ma zżerać wszystkie. Tak więc zeżre cały tekst, po czym uzna, że ma dosyć i jest zadowolony (byłby niezadowolony tylko wówczas, gdyby nie zeżarł żadnej litery). Potem jednak do akcji wkracza kolejna część wyrażenia regularnego, czyli po prostu ]. Takie coś znaczy tylko tyle, że "chcę zeżreć znak ]!". Jak nie będzie takiego znaku, to źle -- całe wyrażenie nie zostanie dopasowane. Jak będzie ten znak, to dobrze. A jak po nim jeszcze coś będzie, to niech sobie będzie -- ta część wyrażenia regularnego zżera jeden znak ] i tylko to.

Problem w tym, że poprzednia część wyrażenia już zeżarła całą resztę stringu, łącznie ze znakiem ]. Więc teraz już nie ma znaku ], który mógłby być dopasowany (już został zjedzony!).

I przez to Ci nie działa.

Rozwiązań jest kilka:

var myRe = /expander\[([^]]+)\]/; // (a) zzeraj wszystko, tylko nie ]
var myRe = /expander\[(.+?)\]/; // (b) dodatkowy znak ? wylacza zachlannosc

Na dobrą sprawę wystarczy opcja (b), czyli dopisanie jednego znaku (znaku ?) za operatorem +. Oznacza on, że operator + zeżre tylko tyle, ile musi. Czyli na początku spróbuje zeżreć tylko jeden znak. Jeśli reszta wyrażenia nie zostanie dopasowana, to zeżre drugi i tak dalej. Zeżre w końcu tyle znaków, by reszta wyrażenia -- czyli ] -- mogła zostać dopasowana, czyli zeżre wszystkie znaki aż do nawiasu, ale bez nawiasu.

Inna sprawa, że prawdopodobnie w Twoim kodzie są błędy escape'owania znaków. Otóż napisałeś:

new RegExp("expander\[(.+)\]");

Wszystko byłoby dobrze w składni wyrażeń regularnych, ale niestety użyłeś tego nieszczęsnego konstruktora RegExp, który za argument przyjmuje zwykły string. A w stringu w JavaScripcie znaki \ (tzw. ukośniki wsteczne) mają specjalne znaczenie. W stringu dwa znaki: [ zostaną zastąpione znakiem [. Dopiero to zostanie przekazane wyrażeniu regularnemu.

Musiałbyś to napisać tak:

new RegExp("expander\\[(.+)\\]");

Znaki \ są w stringu zastępowane znakiem \ (jeden ukośnik wsteczny). I to trafi do wyrażenia regularnego. Czyli po prostu:

\[

zostanie zamienione (ze względu na zasady obowiązujące w stringach) na:

[

Czyli to, o co Ci chodziło. Bierze się to stąd, że w stringach ukośniki wsteczne mają specjalne znaczenie. Np. kombinacja \n oznacza znak nowej linii. Jeśli chcesz to specjalne znaczenie zniwelować i po prostu użyć ukośnika wstecznego, musisz go napisać dwa razy (czyli jeśli chcesz po prostu ten ukośnik i literę n, a nie znak nowej linii, to musisz napisać \n).

Ale najlepiej skorzystać z wygodnego literału wyrażenia regularnego, który nie ma tych ograniczeń, a w tym wypadku jest najzupełniej wystarczający:

/expander\[(.+)\]/;

Zauważ, że u góry nie ma żadnego stringu. Stringi oznaczamy cudzysłowami lub apostrofami. Powyżej zaś tego nie ma, treść wyrażenia regularnego umieszczamy pomiędzy dwoma ukośnikami. I tu już nie obowiązują te głupie zasady, że jeśli chcemy użyć ukośnika wstecznego, to musimy go napisać dwa razy.

0

Wow... dzięki za bardzo szczegółową odpowiedź. Jest na prawdę wyczerpująca temat.

Zmieniłem mój kod na:

var myRe = /expander\[(.+?)\]/;

tyle, że w tym przypadku w rezultacie zwraca mi cały ciąg znaków "expander[menu$1]", a ja chciałem w wyniku jedynie "menu$1".

Kolejny problem w tym, że pętla jakoś nie ma ochoty się skończyć i iteruje mi nieskończoność :/

0

Chciałem Ci wytłumaczyć o co chodzi, a nie podać rozwiązanie, bo to bardzo powszechny problem w wyrażeniach regularnych, z tą zachłannością. Jak go zrozumiesz, to potem zawsze będziesz potrafił stwierdzić, czy przypadkiem znowu się na niego nie nadziałeś. Problem z ukośnikami jest zaś równie powszechny w językach nie posiadających literałów wyrażeń regularnych, np. w PHP. JavaScript posiada te literały, ale posiada też konstruktor RegExp -- przydatny w niektórych przypadkach, ale zwykle tylko sprawiający problemy.

Szczerze mówiąc nie do końca ogarniam, co chcesz zrobić w tej pętli. Doprawdy, jest to pętla nieskończona :). exec nie działa tu tak, jak myślisz, tj. nie startuje od re.lastIndex. Zawsze startuje od 0, więc pętla kręci się w kółko. Ale możesz to obejść.

Po prostu na końcu pętli zmień str tak, by zaczynał się od odpowiedniego miejsca. Możesz użyć lastIndex, ale lepiej będzie użyć rightContext -- we własności tej przechowywany jest tekst, który jeszcze nie został przemielony przez wyrażenie regularne (czyli tekst, który znajduje się "po prawej" od "wskaźnika", stąd nazwa rightContext). Czyli możesz na końcu pętli zrobić tak:

str = myRe.rightContext;

Inny sposób, to zmuszenie exec, by jednak korzystało z lastIndex i startowało zawsze od niego. To odrobinę niebezpieczne, jeśli się nie uważa (np. czasem trzeba ręcznie ustawić lastIndex = 0 żeby zresetować wyrażenie regularne). Można włączyć tę funkcjonalność, włączając w wyrażeniu regularnym flagę g. Flaga ta wpływa różnie na różne metody i to też może powodować pewne problemy, jeśli się o tym zapomni. Ale na exec wpływa tak, że dopasowanie rozpoczyna się od lastIndex.

Flagę g ustawia się tak:

new RegExp("tresc_wyrazenia", "g")

Po prostu drugi parametr konstuktora RegExp to flagi. Jakbyś chciał postawić jeszcze flagę i (ang. case-insensitive -- jeśli jest włączona, wielkość znaków nie jest brana pod uwagę), to musiałbyś zrobić tak:

new RegExp("tresc_wyrazenia", "gi")

Lub korzystając z literału wyrażenia regularnego (polecam i opisuję dalej):

/tresc_wyrazenia/gi

(flagi wypisuje się zaraz za zamykającym ukośnikiem; nie ma tu żadnych cudzysłowów etc.).

Co do tego, że chcesz wyciągnąć jedynie fragment wyrażenia... To też bardzo, bardzo częsty problem, więc czytaj uważnie ;).

exec zwraca, jak zauważyłeś, tablicę. W pierwszym elemencie tablicy (czyli o indeksie [0], bo w tablicach liczymy od 0) znajduje się całe dopasowane wyrażenie. Natomiast w kolejnych znajdują się fragmenty tekstu dopasowane do kolejnych tzw. grup dopasowania (ang. capturing group). Takie grupy oznaczane są w wyrażeniu regularnym nawiasami okrągłymi. Ty tez masz taką grupę, choć może jeszcze o tym nie wiesz.

Przypuśćmy, że masz takie wyrażenie regularne:

var re = /a(bc)[0-9]+(.+)/

Wyrażenie to oznacza: najpierw litera a, potem b i c, następnie przynajmniej jedna cyfra, a potem przynajmniej jeden dowolny znak. W wyrażeniu tym masz dwie grupy dopasowania. Pierwsza to (bc), a druga to (.+). To tak jakby fragmenty wyrażenia regularnego.

Teraz wyobraź sobie, że robisz:

var str = 'abc123lalala5';
var m = re.exec(str);

Tekst w zmiennej str pasuje do wyrażenia regularnego. exec zwraca zaś tablicę z dopasowaniami, którą podstawiamy do zmiennej m (nazwa m jak matches, czyli dopasowania). m[0] to cały dopasowany tekst, czyli "abc123lalala5". m[1] to tekst dopasowany przez grupę pierwszą, a m[2] -- drugą. Grupa pierwsza to było (bc) i dopasowała pogrubiony fragment tekstu:

abc123lalala5

Czyli m[1] będzie równe "bc". Grupa druga to było (.+) i dopasowała ten fragment tekstu:

abc123lalala5

Więc m[2] == "lalala5"

Teraz zauważ, że i Ty masz w swoim wyrażeniu jedną grupę. Ilekroć użyjesz nawiasów, tworzona jest grupa. Dopasowuje Ci ona dokładnie to, o co Ci chodzi: ten fragment pomiędzy nawiasami [ i ]. To grupa pierwsza, użyj więc myArray[1] by dobrać się do dopasowanego przez nią fragmentu tekstu!

PS. Jeszcze o numeracji grup. Bywa, że masz zagnieżdżone grupy, czyli "nawias w nawiasie", coś jak takie wyrażenie regularne:

blabla(abc([0-9]+)xxx(.+))

Tutaj numeracja jest taka, że zaczyna się od najbardziej zewnętrznych nawiasów. Czyli:
-grupa [1] to ten duuży nawias: (abc([0-9]+)xxx(.+))
-grupa [2] to pierwszy z tych wewnętrznych: ([0-9]+)
-grupa [3] to drugi z wewnętrznych (.+)

1 użytkowników online, w tym zalogowanych: 0, gości: 1