Autor: Paweł Rajewski

Wyrażenia regularne to sposób ogólnego opisu sekwencji znaków spełniających określone kryteria. Często korzystamy z takiej notacji, być może nawet o tym nie wiedząc. Część II artykułu.

Wyszukiwanie wg ilości wystąpień

W naszym przykładzie występują po sobie trzy litery a (w wyrazie kotaaa). To oczywisty błąd. Jak znaleźć takie miejsce? Można zastosować wyrażenie „aaa”, ale gdyby liter było pięć, albo dwie, albo nie była to litera a, lecz jakaś inna? Jak znaleźć kilka takich samych znaków występujących po sobie?

Na początek, dla ułatwienia, poszukajmy kilku wystąpień litery a:

"(a){2,5}"

– taki zapis znajdzie ciągi składające się z dwóch do pięciu liter a. Poszukiwane wyrażenie powinno być umieszczone w nawiasie, choć w przypadku pojedynczego znaku można go pominąć. Zaraz za wyrażeniem występuje nawias klamrowy z dwoma liczbami rozdzielonymi przecinkiem określającymi od ilu do ilu razy powinno być powtórzone poprzednie wyrażenie. Pominięcie drugiej liczby oznacza nieskończoność, a więc:

"(a){2,}"

wyszuka ciągi składające się z dwóch lub więcej liter a (a to jest już bliskie naszym potrzebom).

Tu także istnieją pewne skróty, które można stosować:

+ – oznacza wystąpienie poprzedzającego znak wyrażenia raz lub więcej. Znak + odpowiada więc zapisowi {1,}.
* – oznacza wystąpienie poprzedzającego znak wyrażenia zero razy lub więcej. Znak * odpowiada więc zapisowi {0,}.
? – oznacza wystąpienie poprzedzającego znak wyrażenia co najwyżej raz. Znak ? odpowiada więc zapisowi {0,1}.

Aby odnaleźć w przeszukiwanym łańcuchu zwykłe znaki +, *, ?, należy zdjąć z nich znaczenie specjalne przy pomocy znaku backslash.

Odwołania wsteczne

Wyrażenia regularne posiadają „pamięć”, a nawet… 99 pamięci. Wartości wyrażeń objętych nawiasami okrągłymi są zapamiętywane w tymczasowych „komórkach pamięci” o adresach (w kolejności wystąpienia nawiasów otwierających) od 1 do 99. Można się do nich odwołać stosując w wyrażeniu znaki od \1 do \99 w zależności od komórki, do której się odwołujemy. W naszym ostatnim przykładzie w komórce o numerze 1 znajduje się litera a – bo jest to zawartość pierwszego nawiasu okrągłego w całym wyrażeniu.

Ta pamięć przyda się do odnajdywania dwóch takich samych, występujących po sobie znaków, w sytuacji, gdy nie wiemy, jakie to mają być znaki. Zapis:

"(.)\1"

oznacza: pobierz dowolny znak (tj. pierwszy z badanego łańcucha, ale bez konieczności spełnienia jakichkolwiek konkretnych warunków) i umieść go w pamięci nr 1 (nawias), a następnie dopisz do niego zawartość pamięci nr 1. W ten sposób otrzymujemy wyrażenie oznaczające ciąg dwóch identycznych znaków, bez określania jakie to mają być znaki! W miarę przeszukiwania łańcucha, do komórki nr 1 podstawiana jest wciąż nowa wartość (kolejny pobierany znak łańcucha), dopisywany jest do niego ten sam znak i następuje sprawdzenie, czy ciąg „pasuje”. W efekcie wyszukane zostają wszystkie ciągi składające się z dwóch takich samych znaków jeden po drugim.

A jeśli znaków jest więcej? Można zastosować wyrażenie:

"(.)(\1){1,}"

poszukujące ciągów o dowolnym pierwszym znaku, a następnie dowolnej ilości powtórzeń tego samego znaku. Znalezione zostaną ciągi aa, aaaaa, bbb, nnnnn, 333 itp.

Zastępowanie z pamięci

Skoro wyrażenia regularne mają pamięć, czy można wykorzystać ją do podstawiania znaków pod znalezione ciągi? Oczywiście! W naszym przykładzie wszystkie ciągi powtarzających się znaków zamieniane są na gwiazdki. Jak zamienić je na te same znaki, tylko występujące jeden raz? Na przykład tak (modyfikacja skryptu nr 1):

objRExp.Pattern="(.)(\1){1,}" 
objRExp.Global=True 
...dalszy ciąg skryptu... 
strWynik=objRExp.Replace(strLancuch,"$1") 

Zamiast znaku gwiazdki (lub innego łańcucha do podmieniania), w metodzie Replace() podajemy jako drugi parametr łańcuch „$1” pełniący tu specjalną rolę. To odwołanie do „pamięci nr 1” wyrażenia regularnego. W ten sposób, gdy ciąg identycznych znaków zostanie znaleziony, zamieniony zostanie nie na wciąż ten sam łańcuch (np. gwiazdkę), ale na aktualną zawartość pamięci nr 1 przechowującej, w naszym przypadku, właśnie pojedynczy znak, który się powtarza. Tym sposobem wszystkie wielokrotne wystąpienia znaków zostaną zredukowane do jednego. Podstawmy pod zmienną strLancuch np.: „alaaa mmma kkkotta iii kanarrrrrrka 000023” i zobaczymy efekt.

Czy to znaczy, że nie można podstawić pod znaleziony ciąg zwykłego łańcucha $1 (np. coś kosztuje dolara)? Można, trzeba tylko znak dolara poprzedzić… znakiem dolara. $1 oznacza „pamięć nr 1”, ale $$1 to po prostu zwykły łańcuch o treści $1.

Odwołanie do komórki pamięci nie nastąpi także wtedy, gdy komórka o danym numerze nie została aktywowana. Jeśli wyrażenie regularne nie zawiera nawiasów, sekwencja $1 będzie traktowana jak zwykły łańcuch, a nie jak odwołanie. Podobnie, jeśli wykorzystano pięć komórek, to łańcuchy od $6 będą traktowane jak łańcuchy, a nie jak odwołania do nieistniejących pamięci. Ale uwaga! Jeśli komórka nr 1 istnieje i zawiera np. ciąg ala, a komórka nr 15 nie istnieje, to zapis $15 spowoduje pobranie zawartości z istniejącej komórki $1 i dodanie do niej znaku 5. W efekcie otrzymamy ciąg ala5 co raczej nie jest tym, czego oczekujemy. Stosując znak dolara i nawiasy należy przewidzieć podobne sytuacje.

Czasami chcemy zastosować w wyrażeniu nawias, ale nie chcemy, aby jego zawartość była zapamiętywana. W takiej sytuacji na początku nawiasu podajemy znaki ?: (znak zapytania, dwukropek). Np. w ostatnim przykładzie zupełnie niepotrzebne jest zapamiętywanie wartości drugiego nawiasu. Możemy zaoszczędzić odrobinę pamięci pisząc:

"(.)(?:\1){1,}"

Drugi nawias będzie pełnił rolę grupowania znaków, ale nie będzie zapamiętywany (gwoli ścisłości ten nawias można w ogóle pominąć – „grupowanie” jednego znaku nie jest konieczne – niemniej stosuję go dla przejrzystości przykładu). Przed zapamiętywaniem nawiasu chronią też znaki ?= oraz ?! określające dodatkowo konieczne albo wykluczone „dalsze ciągi” (opisane w poprzednim odcinku).

Pamięć wyrażeń możemy wykorzystać do podświetlania znalezionych ciągów, co da efekt zbliżony do wyszukiwarki Google (skrypt nr 2):

<SCRIPT TYPE="text/vbscript" LANGUAGE="VBScript"
ID="skrypt2">
Option Explicit
Dim strLancuch, strWynik, objRExp
strLancuch="Tu jest pies Ali, a tam dwa koty Oli. Pies Ali nie lubi kotów!"
Set objRExp=New RegExp
objRExp.Pattern="(kot|pies)"
objRExp.Global=True
objRExp.IgnoreCase=True
strWynik=objRExp.Replace(strLancuch,"<SPAN STYLE=""background:#ffff00"">$1</SPAN>") window.document.write(strLancuch&"<BR>"&strWynik)
</SCRIPT>

W tym przypadku znaleziony ciąg zamieniany jest na ten sam ciąg, ale objęty tagiem SPAN z odpowiednim stylem. Uzupełniając wyrażenie o znaki brzegu wyrazu, możemy ograniczyć wyniki jedynie do pełnych wyrazów (niestety, pozostanie problem polskich liter):

"\b(kot|pies)\b"

Ten sam efekt podświetlenia uzyskamy bez angażowania pamięci stosując w metodzie Replace() znak $& zwracający aktualnie odnaleziony ciąg. W powyższym przykładzie znajdowane są po kolei ciągi: pies, kot, Pies, kot. $& zwraca właśnie te łańcuchy. Zmiana wyrażenia na „kot|pies” (bez nawiasu!) i (w metodzie Replace()) $1 na $& spowoduje, że skrypt będzie działał… tak samo, ale bez korzystania z pamięci. Uwaga: jeśli chcemy pozostawić znaki brzegu wyrazu, musimy pozostawić także nawias pełniący w tym wypadku rolę grupowania wyrażenia (w przeciwnym razie będzie ono rozumiane błędnie jako szukanie ciągu kot na początku wyrazu ALBO ciągu pies na końcu wyrazu). Nawias nie musi być zapamiętywany, więc wyrażenie mogłoby wyglądać następująco:

"\b(?:kot|pies)\b"

Dwa inne, rzadziej stosowane symbole ze znakiem dolara to:

$` (dolar, apostrof – nad klawiszem tabulatora) – zwraca fragment przeszukiwanego łańcucha od początku do miejsca, w którym zaczyna się aktualnie znaleziony ciąg.

$’ (dolar, znak minuty – obok klawisza Enter) – zwraca fragment przeszukiwanego łańcucha od miejsca, w którym kończy się aktualnie znaleziony ciąg, do końca łańcucha.

Ważną cechą „pamięci” wyrażeń regularnych $.. jest ich tymczasowość – istnieją tylko w czasie przeszukiwania łańcucha. Próba odwołania się do nich poza metodą Replace() wywoła błąd. Jak pokonać ten problem i odzyskać zawartość „pamięci”, napiszę w dalszej części artykułu.

Metoda Test()

Metoda Replace() zastępuje znalezione ciągi, ale nie zawsze zastępowanie jest potrzebne. Czasami wystarczy sama informacja, czy żądana sekwencja została odnaleziona. Przykładem może być sprawdzenie, czy w łańcuchu występuje znak ;@ oraz kropka, których brak mógłby sugerować nieprawidłowo wpisany adres e-mail.

Załóżmy, że przyjęliśmy poszukiwanie następującego ciągu: jeden lub więcej znaków literowo-cyfrowych, znak ;@, jeden lub więcej znaków literowo-cyfrowych, kropka, jeden lub więcej znaków literowo-cyfrowych. Nie interesuje nas przy tym nic poza faktem, czy taki ciąg zostanie znaleziony. W tej sytuacji wygodnie jest zastosować metodę Test(strA) testującą łańcuch strA podany jej jako parametr (skrypt nr 3):

<SCRIPT TYPE="text/vbscript" LANGUAGE="VBScript" 
ID="skrypt3"> 
Option Explicit 
Dim strLancuch, blnWynik, objRExp 
strLancuch="adres @serwer.pl" 
Set objRExp=New RegExp 
objRExp.Pattern="\w{1,}@\w{1,}\.\w{1,}" 
blnWynik=objRExp.Test(strLancuch) 
window.document.write(strLancuch&"<BR>"&blnWynik) 
</SCRIPT> 

Metoda zwraca wartość logiczną True lub False (wyświetlaną w polskiej wersji Windows jako Prawda lub Fałsz) w zależności od tego, czy ciąg opisany wyrażeniem regularnym został odnaleziony w łańcuchu wejściowym, czy nie. Jeśli blnWynik równa się False, to sygnał, że coś jest nie tak z podanym adresem – w tym przypadku chodzi o spację przed znakiem ;@ (zwrot wartości True nie świadczy jeszcze o poprawności adresu, bo użyte wyrażenie nie uwzględnia wielu możliwych błędów).

Przy korzystaniu z metody Test() nie ma znaczenia ustawienie właściwości Global. Aby otrzymać wartość True, wystarczy znalezienie jednego wyniku. Przeszukiwanie dalszej części łańcucha jest więc niepotrzebne.

Pajączek.pl - twórz poprawiaj publikuj

W podobny sposób możemy sprawdzić kod pocztowy, który powinien składać się z dwóch cyfr, minusa i trzech cyfr. Wyrażenie mogłoby wyglądać następująco:

"^[0-9]{2}-[0-9]{3}$"

Podałem tu dokładną liczbę wystąpień cyfr – dwa i trzy razy, a nie przedział (choć można też napisać {2,2} oraz {3,3}, co oznacza to samo). Dodałem także znaki ^ i $ oznaczające w tym przypadku początek i koniec całego łańcucha – a zatem nie można już dopisać czegoś przed i za kodem, co było możliwe w poprzednim przykładzie z adresem e-mail.

Metoda Execute()

To najtrudniejsza w użyciu metoda obiektu RegExp. O ile metoda Replace() zwracała zmieniony łańcuch, a metoda Test() – informację o odnalezieniu poszukiwanego ciągu, to metoda Execute(strA) zwraca kolekcję obiektów z danymi o wszystkich znalezionych wystąpieniach szukanej sekwencji. Parametrem metody jest łańcuch strA, który należy przeszukać. Na przykład (skrypt nr 4):

<SCRIPT TYPE="text/vbscript" LANGUAGE="VBScript"
ID="skrypt4">
Option Explicit
Dim strLancuch, objWyniki, objRExp
strLancuch="Ala ma kota i Ola ma kota. To ile jest kotów?"
Set objRExp=New RegExp
objRExp.Pattern="kot[a-ząćęłńóśżź]{0,}"
objRExp.Global=True
objRExp.IgnoreCase=True
Set objWyniki=objRExp.Execute(strLancuch)
window.document.write(strLancuch&"<BR>"&"Count: "&objWyniki.Count)
' Tu dopiszemy ciąg dalszy...
</SCRIPT>

Efektem działania metody Execute() jest kolekcja Matches przypisana do zmiennej objWyniki (ponieważ kolekcja jest obiektem, przypisanie musi zawierać instrukcję Set). Kolekcja Matches posiada właściwość Count zawierającą informację o ilości znalezionych wystąpień ciągu – w tym przypadku będzie to 3 (wypisane przez skrypt pod przeszukiwanym łańcuchem).

Kolekcja Matches i obiekty Match

Kolekcja Matches jest zbiorem obiektów Match, z których każdy zawiera dane o kolejnym wystąpieniu poszukiwanego ciągu. Do obiektów Match docieramy przy pomocy właściwości Item(intNr) Np.:

objWyniki.Item(0)

…doprowadzi nas do pierwszego obiektu Match (numeracja zaczyna się od zera!). Ponieważ Item to właściwość domyślna, identyczny efekt da skrótowy zapis:

objWyniki(0)

Każdy z obiektów Match zawartych w kolekcji Matches posiada cztery właściwości:

FirstIndex – pozycja danego wyniku w przeszukiwanym ciągu.
Length – długość znalezionego ciągu.
Value – treść znalezionego ciągu.
SubMatches – chwilowa zawartość pamięci wyrażenia regularnego (opis w dalszej części artykułu).

Poniższy kod dopisany na końcu skryptu nr 4 wyświetli komplet informacji o wszystkich znalezionych wynikach. Wykorzystałem tu pętlę For Each… Next „przeglądającą” wszystkie pozycje kolekcji (w tym przypadku kolekcji Matches).

' ...Tu poprzednia część skryptu nr 4
Dim objWynik, strWyswietl
For Each objWynik In objWyniki
  strWyswietl=strWyswietl&"<BR><BR>FirstIndex: "&objWynik.FirstIndex&"<BR>Length: "&objWynik.Length&"<BR>Value: "&objWynik.Value
Next
window.document.write(strWyswietl)

Kolekcja SubMatches (IE5.5 i nowsze)

Jak wspomniałem wcześniej, pamięci wyrażenia regularnego ($1-$99) nie są dostępne po zakończeniu przeszukiwania. Co więcej, zapis $.. można stosować tylko w ramach metody Replace(). Dodatkowo, zawartość pamięci ulega ciągłym zmianom. Jeśli np. poszukujemy ciągu „(kot|pies)”, to pamięć $1 będzie zawierała raz ciąg kot (jeśli akurat zostanie znaleziony ciąg „kot”), a raz ciąg pies (gdy akurat zostanie znaleziony ciąg „pies”). W miarę trwania przeszukiwania i znajdowania kolejnych pasujących sekwencji, zawartość pamięci $1 będzie zmieniana.

Zapis chwilowej zawartości pamięci oferuje właściwość SubMatches obiektów Match. Właściwość ta też jest obiektem – kolekcją zawierającą tyle pozycji, ile nawiasów okrągłych posiada wyrażenie regularne. Np. w przypadku wyrażenia „(kot|pies)” będzie tylko jedna aktywna komórka pamięci i jedna pozycja w kolekcji SubMatches, a w przypadku wyrażenia „(kot)|(pies)|(kanarek)” – będą trzy komórki i trzy pozycje kolekcji. Każda z pozycji kolekcji SubMatches jest zwykłym łańcuchem, w którym zapamiętana została zawartość odpowiedniej komórki pamięci wyrażenia – w momencie odnalezienia danego wystąpienia ciągu. Uwaga: numeracja w kolekcji zaczyna się od zera, a zatem SubMatches.Item(0) odpowiada pamięci $1.

Kolekcja posiada też jedną stałą właściwość: Count określającą ilość pozycji.

Reasumując: metoda Execute() zwraca kolekcję Matches. Kolekcja Matches jest zbiorem obiektów Match, z których każdy przechowuje dane o jednym wystąpieniu poszukiwanego ciągu. Obiekty Match mają kilka właściwości, z których jedna jest kolekcją SubMatches. Kolekcje SubMatches posiadają tyle pozycji, ile komórek pamięci wyrażenia regularnego zostało aktywowanych. W kolejnych pozycjach zapisana jest chwilowa zawartość kolejnych komórek.

Oto przykład dostępu do kolekcji SubMatches (skrypt nr 5):

<SCRIPT TYPE="text/vbscript" LANGUAGE="VBScript"
ID="skrypt5">
Option Explicit
Dim strLancuch, objRExp, objWyniki, objWynik, strWyswietl
strLancuch="Ala, ala.kwiatek@serwer.pl, www.mojastrona.serwer.pl; Ola, ola_2@onet.pl, brak; Szkoła, sp12@szkoly.edu, www.szkoly.edu/szkolapodstawowa12/; Minister, vip1@rzad.gov, brak;"
Set objRExp=New RegExp
objRExp.Pattern="[\w\.]{1,}@([\w\.]{1,})"
objRExp.Global=True
objRExp.IgnoreCase=True
Set objWyniki=objRExp.Execute(strLancuch)
For Each objWynik In objWyniki
  strWyswietl=strWyswietl&"<BR><BR>Value: "&objWynik.Value&"<BR>$1: "&objWynik.SubMatches.Item(0)
Next
window.document.write(strLancuch&strWyswietl)
</SCRIPT>

Skrypt wybiera adresy e-mail z fikcyjnej bazy danych. Dodatkowo, w pamięci $1 zapisuje nazwę serwera pocztowego (nawias w wyrażeniu). Jak widać, chwilowe wartości pamięci $1 zostały zapisane w kolekcji SubMatches i mogą być przetwarzane w dalszej części skryptu.

Mniej zachłanne wyrażenia (IE5.5 i nowsze)

Załóżmy, że przeszukujemy ciąg (można podstawić do ostatniego skryptu):

"Ala (10 pkt), Ola (8 pkt), Ania (7 pkt)"

przy pomocy wyrażenia:

"\((.{1,})\)"

Chcemy wydobyć teksty znajdujące się w nawiasach. Zadanie brzmi więc: znajdź ciąg: otwarcie nawiasu, następnie jeden lub więcej dowolnych znaków, zamknięcie nawiasu. Znaki pomiędzy otwarciem i zamknięciem nawiasu zapamiętaj w pamięci nr 1 (zwracam uwagę, że pierwszy i ostatni znak nawiasu mają zdjęte znaczenie specjalne, oznaczają więc zwykłe znaki do wyszukania. Natomiast nawias wewnętrzny służy zapamiętaniu ciągu „jeden lub więcej znaków”).

Jakiego wyniku oczekujemy? Zapewne trzech ciągów z osiągniętą punktacją. Tymczasem otrzymujemy jeden wynik: (10 pkt), Ola (8 pkt), Ania (7 pkt). I to jest rozwiązanie prawidłowe – wszak ciąg spełnia podane kryterium – zaczyna się otwarciem nawiasu, w środku ma jeden lub więcej znaków, i kończy się zamknięciem nawiasu.

Wyrażenia regularne działają właśnie w ten sposób – „zachłannie”, odnajdując maksymalne ciągi pasujące do podanego wzorca. Jeśli wewnątrz wyniku znajdują się wyniki cząstkowe (mniejsze ciągi także spełniające warunek) są ignorowane. Od IE5.5 można temu przeciwdziałać.

Jeśli bezpośrednio po wyrażeniu określającym ilość powtórzeń podamy znak zapytania, wyrażenie to zacznie zwracać nie maksymalny, ale minimalny ciąg spełniający dany warunek. W przykładzie jw. podajemy więc wyrażenie:

"\((.{1,}?)\)"

i… otrzymujemy trzy wyniki: (10 pkt), oraz (8 pkt), oraz (7 pkt). Odnalezione zostały najkrótsze możliwe ciągi spełniające warunek „jednego lub więcej znaków” pomiędzy nawiasami. Pamięć nr 1 przechowuje teraz teksty wewnątrz nawiasów, czyli dokładnie to, czego szukamy.

Przykład z nawiasami wydaje się teoretyczny, ale takie „minimalistyczne” wyszukiwanie ma ogromne znaczenie gdy szukamy wielu ciągów zaczynających się i kończących w określony sposób np. tagów HTML. Oto przykład (skrypt nr 6):

<SCRIPT TYPE="text/vbscript" LANGUAGE="VBScript" 
ID="skrypt6"> 
Option Explicit 
Sub funUsunTagi() 
  Dim objBody, strBIH, objRExp 
  window.event.returnValue=False 
  Set objBody=window.document.body 
  strBIH=objBody.innerHTML 
  Set objRExp=New RegExp 
  objRExp.Pattern="<.{1,}?>" 
  objRExp.Global=True 
  objRExp.IgnoreCase=True 
  objRExp.MultiLine=True 
  strBIH=objRExp.Replace(strBIH,"") 
  objBody.innerHTML=strBIH 
End Sub 
Set window.document.oncontextmenu=GetRef("funUsunTagi") 
</SCRIPT> 

Skrypt należy umieścić w sekcji HEAD dowolnej strony HTML. Po kliknięciu prawym klawiszem myszki usunie ze strony (z elementu BODY) wszystkie tagi HTML pozostawiając sam tekst (lepiej, aby strona testowa nie zawierała innych skryptów, ponieważ może dojść do konfliktów).

Wyrażenie regularne poszukuje ciągów: nawias trójkątny otwierający, jeden lub więcej dowolnych znaków, nawias trójkątny zamykający. Znak zapytania za wyrażeniem określającym ilość znaków sygnalizuje, że należy szukać minimalnych ciągów spełniających ten warunek, czyli pojedynczych tagów.

Usuńmy z wyrażenia znak zapytania i zobaczmy co się dzieje – po kliknięciu znika niemal cała strona! Znaleziony został maksymalny ciąg pasujący do wyrażenia – zaczynający się otwarciem pierwszego tagu, a kończący zamknięciem ostatniego. Cała strona została uznana za jeden wielki tag.

Zakończenie

Wyrażenia regularne to ogromne możliwości w zwięzłej i prostej formie. Ta prostota jest jednak zwodnicza, zwłaszcza dla początkujących. W skondensowanym zapisie łatwo o pomyłkę. Nietrudno przeoczyć zjawiska, które mogą zachodzić podczas wyszukiwania. Nie stosujmy więc (przynajmniej na początku) wszelkich możliwych skrótów – twórzmy wyrażenia czytelne. Przy testowaniu pomoże też opcja Internet Explorera „Wyświetl powiadomienie o każdym błędzie skryptu” (w wersji 5.5: Narzędzia – Opcje internetowe – Zaawansowane – Przeglądanie). W razie problemów dowiemy się, gdzie leży przyczyna.

Powodzenia.

Paweł Rajewski

Skasowane dane to nie zawsze tragedia - ściągnij program i odzyskaj dane