BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Wyrażenia regularne na przykładzie prostego klienta FTP

20-12-2003 22:57 | DRECH

Podstawową funkcjonalność niezbędną do wykonywania operacji na tekście (wykładowcy mówią w takich przypadkach o wydobywaniu informacji z danych) dostarcza klasa System.String wspomagana czasem typem System.Text.StringBuilder. Jak się wkrótce przekonamy to właśnie pierwszą z tych klas wykorzystywać będziemy najczęściej. W poważniejszych jednakże zastosowaniach, gdzie interesująca nas treść może przyjąć dużo lub nieskończenie wiele wartość, proste porównania przestają wystarczać. Z pomocą przychodzi bardzo zaawansowany i skomplikowany wewnętrznie podsystem wyrażeń regularnych, znany doskonale użytkownikom wielu edytorów Unixowych, czy programistom Perla. Interfejsem tego podsystemu jest zbiór klas przestrzeni System.Text.RegularExpressions.

Omówienie klasy String nieco wykracza poza ramy tematu tego artykułu, jednak ze względu na jej wszechobecność warto wspomnieć o kilku jej metodach. Klasa ta pozwala między innymi na sprawdzenie początku lub końca wartości obiektu: StartsWith i EndsWith. Z kolei IndexOf i IndexOfAny pozwalają na wyszukanie położenia konkretnego ciągu lub jednego z ciągów znaków w zawartości obiektu. Metoda Substring pozwala natomiast na sprawdzenie zawartości określonego fragmentu.

Poniższy fragment kodu wykorzystuje przedstawioną funkcjonalność w celu zalogowania się do serwera FTP:

/// <summary>
     /// Połączenie i zalogowanie się do serwera ftp
     /// </summary>
private static bool Connect(string ServerName, string User, string Pass)
     {
          //Połączenie z serwerem
          (...)
          //Logowanie do serwera
          response = commandReader.ReadLine();//Powitanie z serwera
          if(response!=null && response.StartsWith("220"))
          {
               commandWriter.WriteLine("user "+User);
               commandWriter.WriteLine("pass "+Pass);
               do
               {
                    response = commandReader.ReadLine();
} while(response.Substring(1,2) != "30");//230 lub 530
               //230 - login OK, 530 - login się nie udał
               if(response.StartsWith("230"))
                    return true;
               (...)

Odpowiedź serwera FTP zaczyna się one zawsze 3 – cyfrowym kodem. Pierwsza z tych cyfr informuje o statusie przesłanej komendy: 1 – będą przesyłane dane, 2 – komenda zakończona pomyślnie, 3 – potrzeba dalszych informacji, 4 i 5 – zaistniał błąd, komenda się nie wykonała. Dwie kolejne cyfry zależą od przesłanej serwerowi komendy, oraz ewentualnie od rodzaju zaistniałego błędu.

Niestety nie zawsze powyższa funkcjonalność jest wystarczająca. Załóżmy na przykład, że z linii tekstu musimy wydobyć wartość tagu xml postaci:

     ... <wartość_tagu> ...

Nie znamy struktury ani początku, ani końca tekstu. Rozwiązanie problemu znajdziemy w głównym temacie tego artykułu – w przestrzeni nazw System.Text.RegularExpression. Wyrażenia regularne stanowią potężne narzędzie. Pozwalają dopasować tekst do konkretnego wzorca i na tej podstawie wyciągać informacje, czy też dokonywać zmian. Wspomniane wzorce i ich składnia stanowią podstawowy i jednocześnie krytyczny element umożliwiający efektywne wykorzystanie wyrażeń regularnych. Na ich budowie skupimy się też poniżej, nie zapominając następnie o zbiorze kilku klas stanowiących obiektowy interfejs udostępniający całą funkcjonalność podsystemu omawianych wyrażeń.

Wyrażenia regularne w wydaniu Microsoftu są kompatybilne z implementacją w języku Perl, a tym samym osoby piszące w tym języku powinny poczuć się całkiem swojo. Również podobnie jak w przypadku Perla (ale też Pythona, Emacsa, czy Tcl) użyto silnika (ang. engine) NFA (Niedeterministyczny Automat Skończony), który będąc wolniejszym od konkurencyjnych rozwiązań daje najszersze możliwości. Jak to zwykle bywa, wydanie z Redmond zostało jeszcze dodatkowo rozszerzone.

Przejdźmy tymczasem do budowy wzorców dla wyrażeń. Wzorzec jest obiektem tekstowym ideologicznie przypominającym znaki ‘*’, czy ‘?’ stosowane na przykład przy kopiowaniu wielu plików w pamiętnych czasach MS-DOSa:

     

     copy  b?g*.txt  a:\texty

Tutaj ze wzorcem zgodne będą takie pliki jak: big.txt, bigger.txt itd. Wzorce wyrażeń regularnych, poza różnicami składniowymi, posiadają również znacznie szersze możliwości.

Wzorzec jest obiektem klasy String. Wewnątrz tekstu tego obiektu znajdują się natomiast oprócz zwykłych znaków alfanumerycznych również znaki specjalne, kwalifikatory, czy odwołania wsteczne.

Znaki specjalne przedstawia poniższa tabela:

 

Symbol

Opis

.

Dowolny znak, oprócz ‘\n’ (chyba, że wyrażenie regularne utworzono z opcją RegexOption.Singleline)

[]

Dowolny znak ze zbioru znajdującego się wewnątrz nawiasów. Przedziały znaków oznacza się ze znakiem ‘–‘ np. [a-zA-Z]. Wewnątrz nawiasów ‘\b’ wyjątkowo oznacza backspace

[^ ]

Dowolny znak nie należący do podanego zbioru np. [^0-9] – nie cyfra

^

Dopasowanie musi zaczynać się na początku wiersza

$

Dopasowanie musi kończyć się z końcem wiersza

()

Grupowanie wyrażeń. Ciąg dopasowany do wyrażenia wewnątrz nawiasów zostanie zapamiętany i będzie potem dostępny.

(?<nazwa>)

Grupa nazwana. Umożliwia dostęp do grupy bez podawania jej numeru.

|

Lub – alternatywa znaków, lub zbiorów.

 

Kwalifikatory oznaczające krotność wystąpienia znaku (lub zbioru):

 

Symbol

Opis

*

Zero lub więcej razy

+

Jeden lub więcej razy

?

Zero lub jeden raz

{n}

Dokładnie n razy

{n,m}

Od n do m razy

{n,}

N lub więcej

*?

Leniwy kwalifikator *, analogicznie +?, ??, ...

 

Kwalifikator nieleniwy (zachłanny) próbuje dopasować jak najwięcej elementów. Leniwy jak najmniej.

Istotne są również sekwencje specjalne znaków:

 

Symbol

Opis

\t, \r, \n, \v

Jak zawsze: tab, powrót karetki, nowa linia, pionowy tab

\znak_specjalny

Po prostu ten znak np. \$ - oznacz $; \^ - oznacza ^ itd.

\b

Teoretyczna granica pomiędzy ciągiem alfanumerycznym, a białym znakiem

\w

Skrót dla znaku ze zbioru znaków alfanumerycznych

\W

Zaprzeczenie \w

\s

Dowolny biały znak

\S

Zaprzeczenie \s

\d

Cyfra

\D

Zaprzeczenie \d

\liczba np \1

Odwołanie wsteczne. Oznacza dokładnie ten sam ciąg, jaki został dopasowany do grupy o numerze ‘liczba’.

\k<nazwa>

Odwołanie wsteczne do grupy nazwanej

 

Uwaga!!! Kompilator C# uznaje ‘\’ jako znak specjalny stałej tekstowej, nie wzorca. Zaprotestuje, jeśli po backslashu znajdzie się nieprawidłowy znak np. ‘\$’. Prawidłowy (dla tekstu) znak natomiast zmieni znaczenie wzorca np. ‘\r’. Należy koniecznie pamiętać, że aby uzyskać znak ‘\’ należy wpisać ‘\\’:

     string wzorzec = "cośtam \\d";

Powyższy wzorzec oznacza dopasowanie do tekstu zawierającego napis „cośtam”, spację i dowolną cyfrę. C# umożliwia też uproszczenie zapisów wymagających znaku ‘\’:

     string wzorzec = @"cośtam \d";

Znak ‘@’ wyłącza interpretowanie tekstu przez kompilator. Ułatwia to śledzenia zwłaszcza skomplikowanych wzorców.

Aby lepiej przyswoić sobie konstrukcje wzorców wykorzystujące informacje z powyższych tabel przyjrzyjmy się kilku przykładom:

string wzorzec = @"a+b";//Pasować będą takie ciągi jak: “ab”, “aab”, //“aaab”, itd.

string wzorzec = @"^[0-9]{3,5}?";//Pasować będą ciągi rozpoczynające //się od 3 – 5 cyfr, przy czym dopasowanych zostanie jak najmniej //cyfr

string wzorzec = @"\([^0-9]\)$";//Pasować będzie dowolny znak nie //należący do zbioru cyfr, ujęty w nawiasy okrągłe. Ciąg musi //znajdować się na końcu linii np. “(d)”, “(R)”, itd. Nawiasy nie //poprzedzone znakiem ‘\’ wyznaczałyby grupę

string wzorzec = @"b.g(ger|gest)";//Pasować będą np. “bigger”, //“biggest”, “b5gger”, itd.

string wzorzec = @"(?<first>\w+), \k<first>er, \k<first>est"; //Pasować będą ciągi z powtarzającą się grupą znaków. Wyraz, który //dopasowany zostanie do grupy <first> (wewnątrz nawiasów //okrągłych), musi pojawić się jeszcze 2 razy (odpowiednio z końcówką //“er” i “est”. Przykłady: “small, smaller, smallest”, “chip, chiper, //chipest”, itd.

Jak widać w ostatnim przykładzie z wartości dopasowanej do grupy można korzystać jeszcze wewnątrz wzorca. Oczywiście podstawowym zadaniem grup jest umożliwienie wyciągnięcia dopasowanej do nich wartości na zewnątrz. W dalszej części poznamy klasy udostępniające pełną funkcjonalność wyrażeń regularnych.

Podstawowym typem przetwarzającym wyrażenia regularne jest Regex. Obiekt tego typu w najprostszym przypadku tworzy się podając wzorzec jako jedyny parametr konstruktora. Druga publiczna wersja konstruktora umożliwia natomiast ustawienie opcji wyrażenia. Opcje te, podawane jako drugi parametr, są wartościami typu wyliczeniowego RegexOptions. Najczęściej wykorzystywane przedstawia poniższa tabelka:

 

Nazwa wartości

Opis

Compiled

Nakazuje kompilację wyrażenia kodu MSIL

IgnoreCase

Bez rozróżniania wielkości liter

RightToLeft

Dopasowywanie rozpocznie się od prawej strony

Multiline

Znaki specjalne ‘^’ i ‘$’ oznaczać będą odpowiednio początek i koniec każdego wiersza tekstu

Singleline

Znak ‘.’ oznaczać będzie dowolny znak (również ‘\n’)

 

W przykładowym kliencie FTP niezbędne będą dwa wyrażenia:

//Kompilacja wyrażeń regularnych - dla portu i daty
Regex pasivPort = new Regex(@"\((\d+),(\d+),(\d+),(\d+),(?<port1>\d+),(?<port2>\d+)\)", RegexOptions.Compiled);
     
Regex fileDate = new Regex(@"^(\d{3}).+(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})(?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2})", RegexOptions.Compiled);

Pierwsze z wyrażeń – passivPort – pozwala uzyskać z odpowiedzi serwera numer portu niezbędny dla uruchomienia trybu pasywnego.

Połączenie nawiązywane w celu rozpoczęcia komunikacji z serwerem FTP wykorzystywane jest jedynie do przesyłania poleceń i odbierania odpowiedzi z serwera. W celu przesyłu danych utworzone musi zostać drugie. Połączenie danych może zostać nawiązane w trybie aktywnym bądź pasywnym. W pierwszym z nich klient informuje na jakim porcie rozpoczyna nasłuch, natomiast połączenie nawiązuje serwer. Powoduje to problemy w przypadku stosowania firewalli. Właśnie dlatego skorzystamy tu z trybu pasywnego. Wszystko dzieje się w nim odwrotnie. Serwer podaje numer portu, a klient nawiązuje połączenie.

Odpowiedź serwera zawierająca niezbędny numer portu wygląda następująco:

     227 Entering Passive Mode (127,0,0,1,5,79)

Tekst odpowiedzi może być dowolny, jednakże zawsze występuje tu ciąg sześciu liczb rozdzielonych przecinkami i ujętych w nawias okrągły, stąd w wyrażeniu sześć grup liczb. Numer portu zakodowany jest w dwóch ostatnich liczbach w postaci PIĄTA_LICZBA*256+SZÓSTA_LICZBA, dlatego dwie ostatnie grupy zostały nazwane.

Drugie z wyrażeń pozwala na ekstrakcję daty ostatniej modyfikacji pliku (ponieważ chcemy pobierać z serwera FTP jedynie nowsze pliki) zwracanej z serwera po przesłaniu komendy:

     MDTM nazwa_pliku

W odpowiedzi otrzymujemy interesujący nas czas w następującej postaci:

     213 20031204121536

Na początku otrzymujemy 3 – cyfrowy kod, stąd też wzorzec rozpoczyna się od @”^(\d{3}). W dalszej części znajduje się dowolny ciąg znaków – stąd .+ , po czym mamy 14 cyfr daty w postaci czterech cyfry dla roku i po dwie odpowiednio dla miesiąca, dnia, godziny, minuty i sekundy. W naszym wzorcu zapisane zostało to jako (?<year>\d{4})(?<month>\d{2})(?<day>\d{2})(?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2}).

Oba wyrażenia tworzone są z opcją RegexOptions.Compiled. Każde wyrażenie regularne jest w .NET stałe (ale wielokrotnego użytku) i standardowo przekształcane do pewnej postaci pośredniej. Jeśli zamierzamy natomiast dane wyrażenie wykorzystywać wielokrotnie przez cały czas działania aplikacji możemy nakazać kompilatorowi utworzenie szybszego kodu MSIL (oczywiście poniesiemy dodatkowy koszty przy pierwszym użyciu). Należy być jednak ostrożnym. Skompilowane wyrażenie dołączane jest do programu (do assembly) i egzystuje w nim cały czas, nawet po „przejeździe śmieciarki” (Garbage Collector). Jeśli więc mamy dużo wyrażeń, zwłaszcza używanych tylko w krótkich okresach czasu należy oszczędnie stosować wspomnianą opcję.

Mając gotowy obiekt wyrażenia regularnego możemy już przystąpić do operacji na tekście. (Tak naprawdę to nie potrzebujemy tworzyć obiektu klasy Regex. Klasa ta dysponuje dla każdej metody również statyczny odpowiednik. Oczywiście ogranicza to wyrażenia do jednokrotnego użycia) Na początek możemy sprawdzić, czy można dopasować pożądany ciąg tekstowy metodą IsMatch:

     Regex reg = new Regex(/*wzorzec*/);
     string tekst = "Jakiś tam tekst";
     if( reg.IsMatch(tekst) )
     {
          //Uda się dopasować
     }

Następnie zajmujemy się samym dopasowaniem. Mamy tu do wyboru dwie metody: Match i Matches. Pierwsza z nich zwraca obiekt typu Match reprezentujący pierwsze możliwe dopasowanie wzorca do tekstu. Wykonując drugą otrzymamy natomiast kolekcję obiektów MatchMatchCollection, czyli wszystkie możliwe dopasowania. Przydatne bywają też metody GetGroupNames i GetGroupNumbers informujące o nazwach i numerach grup, oraz GroupNameFromNumber i GroupNumberFromName zamieniające nazwę na numer i odwrotnie. W naszej przykładowej aplikacji proces dopasowania wygląda następująco:

     Match matchRegEx;//dopasowanie wyrażenia regularnego do stringu
     //Uruchomienie trybu passiv dla odczytu zawartości katalogu
     (...)
     //Odczyt odpowiedzi ze strumienia
     response = commandReader.ReadLine();
     //Dopasowanie odpowiedzi do wyrażenia "wyciągającego" numer portu
     matchRegEx = pasivPort.Match(response);
     if(!matchRegEx.Success) return;

Ponieważ wcześniej nie sprawdzaliśmy, czy dopasowanie jest możliwe, korzystamy z funkcjonalności klasy Match umożliwiającej uzyskanie podobnej informacji. Właściwość Success informuje, czy dopasowanie zakończyło się pomyślnie.

Posiadając obiekt dopasowania możemy wykonać już najbardziej interesujące nas zadania. Po pierwsze właściwość Groups klasy Match umożliwia pobranie kolekcji obiektów Group (w postaci obiektu GroupCollection), reprezentujących tekst dopasowany do zadanych wzorcem grup. Klasa Group umożliwia z kolei dostęp do dopasowanego tekstu – właściwość Value, czy jego długości – właściwość Length:

     Match m = (...);//Uzyskanie obiektu dopasowania
     //Wypisanie grupy na konsolę
     GroupCollection grCol = m.Groups;//Pobranie grup
     Console.WriteLine(grCol[/*Nazwa lub numer grupy*/].Value);

Indekser klasy GroupCollection pozwala na dostęp do konkretnego obiektu Group na podstawie numeru, bądź nazwy grupy.

Zarówno klasa Group, jak i Match umożliwiają też dostęp do obiektów Capture (poprzez kolekcję CaptureCollection) za pomocą właściwości Captures. Obiekt Capture reprezentuje poszczególne dopasowania w obrębie grupy stworzonej z użyciem kwalifikatorów (powtarzającego się kawałka tekstu) i jest raczej rzadko stosowany.

W naszym prostym kliencie FTP wykorzystaliśmy jeszcze jedną metodę klasy Match do wyciągnięcia interesującej nas zawartości grup. Metoda Result zwraca przekazany jej ciąg tekstowy, dokonując przy tym podstawień w miejsce odwołań wstecznych (czyli kiedy informujemy, że w danym miejscu chcemy uzyskać zawartość dopasowaną do danej grupy). Po pierwsze potrzebujemy „dobrać się” do numeru portu:

     //Odczyt rezultatów dopasowania

     port = int.Parse(matchRegEx.Result("${port1}"))*256;
     port += int.Parse(matchRegEx.Result("${port2}"));

Innym razem tworzymy obiekt DateTime reprezentujący czas ostatniej modyfikacji pliku na serwerze FTP:

     //Dopasowanie odpowiedzi do wzorca "wyciągającego" datę
matchRegEx = fileDate.Match(response);
     if(!matchRegEx.Success) continue;
     //Odczyt rezultatu i utworzenie obiektu DateTime
     //z czasem modyfikacji pliku na serwerze ftp
     ftpFile = new DateTime(int.Parse(matchRegEx.Result("${year}")),
int.Parse(matchRegEx.Result("${month}")), int.Parse(matchRegEx.Result("${day}")),
int.Parse(matchRegEx.Result("${hour}")), int.Parse(matchRegEx.Result("${min}")),
     int.Parse(matchRegEx.Result("${sec}")) );

Odwołanie wsteczne tworzymy zwykle podając nazwę grupy w nawiasach klamrowych. Nazwa w nawiasach musi być poprzedzona znakiem dolara ‘$’. Na przykład odwołanie wsteczne „${month}” odnosi się do tekstu dopasowanego do grupy „(?<month> )”. Innym sposobem jest odwołanie się do numery grupy, automatycznie nadawanego wszystkim grupom przez podsystem wyrażeń regularnych (również tym nazwanym – numerowanie grup nazwanych rozpoczyna się po nadaniu numerów wszystkim grupom nienazwanym) w kolejności występowania (od lewej) we wzorcu. Do grupy o zadanym numerze odwołujemy się poprzez ten numer poprzedzony znakiem dolara ‘$’. Na przykład do grupy 5 odwołamy się przez „$5”. W ten sposób bazując na powyższym przykładzie możemy wyświetlić datę i godziną w formacie „YYYY-MM-DD hh:mm:ss” w następujący sposób:

     Console.WriteLine(matchRegEx.Result(

"${year}-${month}-${day} ${hour}:${min}:${sec}"

));

Jak widać wyrażenia regularne dostępne w .NET poprzez interfejs przestrzeni nazw System.Text.RegularExpressions stanowią potężną podstawę do skomplikowanych operacji na tekście. Przydadzą się nie tylko w komunikacji z niektórymi serwerami. Przykładowo wyszukiwanie emotikonów w wiadomościach przesyłanych przez GaduGadu staje się banalne. W podobny sposób możemy przeprowadzić parsowanie dokumentów XML, czy HTML.

Do artykułu załączony jest prosty, konsolowy klient FTP – MirrorFTP. Wykonuje on kopię określonego katalogu z serwera na lokalnym dysku. Ponieważ wymaga on biblioteki PasswordConsole.dll, od niej musimy zacząć kompilację, a następnie pamiętać o dołączeniu referencji do biblioteki do właściwej aplikacji:

     

     csc /t:library PasswordConsole.cs

     csc /r:PasswordConsole.dll MirrorFTP.cs

Załączniki:

Podobne artykuły

Komentarze 1

kmccoy1329
kmccoy1329
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bezsens........
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.