Poniższy artykuł pochodzi z serii Przygotowań do egzaminu 70-536.
Wyrażenia regularne pozwalają sprawdzić, czy ciąg znaków pasuje do pewnego wzorca, co przydatne jest chociażby przy sprawdzaniu danych wejściowych, wyciąganiu informacji z tekstu, czy do ich podmiany. Zapisywany wzorzec jest ciągiem znaków, który musi spełniać pewne wymagania, a użyte symbole i operatory mają określone znaczenie, często zależne od kontekstu.
Najczęściej wykorzystywane to:
- “^” - oznacza początek napisu,
- “$” oznaczający koniec napisu,
- “?”, który sprawia, że poprzedzający go znak jest opcjonalny,
- “.”, który dopasowuje dowolny znak,
- “*”, który zajmuje się powtórzeniami (poprzedzający go znak/blok może wystąpić zero, bądź wiele razy),
- “+” – poprzedzający blok musi wystąpić minimum jeden raz.
Jako przykład posłuży wzorzec “^a(mo)+t.*z$”. Co on oznacza? Otóż do wzorca tego pasują wszystkie ciągi znaków, które zaczynają się (symbol “^”) znakiem a, następnie występuje grupa “mo”, która musi wystąpić chociaż raz (“+” sprawia, że może powtarzać się wiele razy), następnie występuje znak t, potem ciąg dowolnych znaków (w tym możliwe, że żaden) i na koniec (symbol “$”) musi wystąpić z.
Sprawdźmy, które z ciągów pasują do wzorca:
1: string[] opt = {"amotz", "amomtrewz", "amotmoz", "atrewz", "amomomottothez"};
2: foreach (var s in opt)
3: {
4: if (Regex.IsMatch(s, @"^a(mo)+t.*z$"))
5: {
6: Console.Write("{0} ", s); // amotz amotmoz amomomottothez
7: }
8: }
Ciąg “amomtrewz” nie pasuje do wyrażenia, ponieważ czwarty znak nie pasuje (jest m a powinno być t), “atrewz” nie pasuje ponieważ po pierwszym a powinniem pojawić się podciąg “mo”.
Następne zadanie to wybranie wyrażenia, do którego będą pasowały ciągi “zoot” i “zot”.
1: string[] reg = { @"z(oo)+t", @"zo*t$", @"$zo*t", @"^(zo)+t"};
2: foreach (var r in reg)
3: {
4: if (Regex.IsMatch("zoot", r) && Regex.IsMatch("zot", r))
5: {
6: Console.Write("{0} ", r); // zo*t$
7: }
8: }
Pasuje tylko jedno. Pierwsze wyrażenie będzie pasowało do ciągu “zoot”, ale nie do “zot” – ciąg “oo” musi wystąpić nie mniej niż jeden raz, trzecie wyrażenie nie pasuje do żadnego z ciągów, ponieważ zaczyna się symbolem “$”, który dopasowuje koniec ciągu znaków. Do ostatniego wyrażenia będzie pasował ciąg “zot”, ale nie “zoot” – zapis wyrażenia wskazuje, że ciąg pasujący ma zaczynać się od “zo”, które może się powtarzać, po czym ma nastąpić znak t.
Często podawanym przykładem jest sprawdzanie poprawności adresu e-mail, chciałbym jednak ostrzec czytelników, gdyż takie wyrażenie nie istnieje. Możemy odrzucić ciągi bez znaku @, jednak reszta nie jest już taka prosta.
Uwaga: Aby korzystać z metody Regex,IsMatch należy dodać odpowiednią dyrektywę using.
1: using System.Text.RegularExpressions;
Tryb wielowierszowy
Zakładamy, że mamy trzy wiersze tekstu i sprawdzamy dopasowanie:
1: string s = "abc\n" +
2: "def\n" +
3: "ghi";
4: Console.WriteLine(Regex.IsMatch(s, "^def$")); // False
Wynikiem będzie “False”, ponieważ “def” nie jest jednocześnie początkiem i końcem całego napisu. Jeżeli chcemy zmienić zachowanie, aby “^” i “$” wykonywały dopasowanie na początku i końcu wiersza, musimy skorzystać z opcji RegexOptions.Multiline.
1: Console.WriteLine(Regex.IsMatch(s, "^def$", RegexOptions.Multiline)); // True
Zamienianie podciągów
Metoda String.Replace daje nam możliwość zamiany podciągów, jeżeli jednak okaże się niewystarczająca warto skorzystać z potężniejszej broni – wyrażeń regularnych. Pomocna tu okaże się statyczna metoda Regex.Replace.
1: static string Hej(string strIn)
2: {
3: // zamień nieporządany podciąg...
4: return Regex.Replace(strIn, @"(ma)+.{2}", "hej");
5: }
Wykorzystany tu został nowy zapis – nawiasy trójkątne, które wymuszają dopasowanie dokładnie n razy. Wyrażenie zamieni podciąg złożony z powtórzeń “ma”, po którym występują dwa dowolne znaki (łącznie z nimi) na podciąg “hej”.
1: Console.WriteLine(Hej("mamamika;)")); // hejka;)
Inny przykład, który można wykorzystać w formularzach, to usuwanie niedozwolonych znaków.
1: static string CleanInput(string strIn)
2: {
3: return Regex.Replace(strIn, @"[^\w\.@-]", "");
4: }
Tu znowu pojawia się kilka nowości: operator wyliczenia “[]” – zbiór znaków, dopasowuje jakikolwiek z wymienionych w nim znaków, “\w” – dopasowuje każdy znak alfanumeryczny, włącznie ze znakiem podkreślenia (jest to to samo, co “[A-Za-z0-9_]”, gdzie zapis typu “A-Z” oznacza zakres znaków (tutaj wszystkie znaki z zakresu “A” do “Z”). Warto też wiedzieć, że symbol “^” znajdując się w nawiasach kwadratowych ma inne znaczenie, niż poprzednio – tu oznacza negację!
1: Console.WriteLine(CleanInput("_123abcZAQ-.@`~!#$%^&*()+={}[];:<>")); // _123abcZAQ-.@
Zamieniliśmy wszystkie znaki, które nie są znakami alfanumerycznymi, nie są kropką, @, czy myślnikiem na ciąg pusty – usunęliśmy je.
Ostatni, raczej prosty przykład, to zamiana adresu typu “http://” na “https://”.
1: string s = @"<a href='http://eastgroup.pl'>Eastgroup</a>";
2: s = Regex.Replace(s, "http://", "https://", RegexOptions.IgnoreCase);
3: Console.WriteLine(s); // <a href='https://eastgroup.pl'>Eastgroup</a>
Kod poprawnie zamieni podciąg, dodatkowo nie jest wrażliwy na wielkość liter i poradzi sobie bez problemu z adresami typu “HTTPS://”.
Wydobywanie pasujących danych
Poza stwierdzaniem, czy ciąg pasuje do wzorca, czy jego podmianą możemy wyciągać dane z ciągu znaków. Przykładowo zakładamy, że dane osobiste mamy w wielowierszowym formacie:
First Name: Jan
Last Name: Kowalski
Chcemy wyciągnąć same dane:
1: string s = "First Name: Jan\nLast Name: Kowalski";
2: Match m = Regex.Match(s, @"First Name: (.*$)\nLast Name: (.*$)", RegexOptions.Multiline);
3: Console.WriteLine("{0} {1}", m.Groups[1], m.Groups[2]); // Jan Kowalski
Nawiasy posłużyły tutaj do dopasowania grupy znaków. Wywołanie metody Match zwróciło dopasowania jako tablicę Match.Groups. Pierwszy element grupy, o indeksie 0, zawiera cały łańcuch znaków, dopasowane elementy znajdują się pod indeksami zaczynającymi się od 1.
Wykorzystanie indeksów może okazać się niewygodne, dlatego istnieje także opcja nadawania nazw grupom, aby potem odwoływać się do dopasowanych danych. Aby nadać grupie nazwę wykorzystuje się zapis “(?<nazwa>wzorzec)”.
1: string s = "First Name: Jan\nLast Name: Kowalski";
2: Match m = Regex.Match(s, @"First Name: (?<firstName>.*$)\nLast Name: (?<lastName>.*$)", RegexOptions.Multiline);
3: Console.WriteLine("{0} {1}", m.Groups["firstName"], m.Groups["lastName"]); // Jan Kowalski
Podsumowanie
Wyrażenia regularne, to bardzo potężne narzędzie. Artykuł porusza jedynie wierzchołek góry lodowej, dobrze jednak poznać chociażby podstawy zapisu. Na całe szczęście do egzaminu potrzebne są najbardziej popularne przypadki, które zostały tu umówione.
Linki
Kolejny artykuł z serii to 70-536: Encoding and Decoding