Większość ludzi których znam, nie lubi regexa. Wolą żmudnie wpisywać string.IndexOf niż nauczyć się przepotężnego narzędzia. Z tymi indexOf to w ogóle kupa problemów jest. Ostatnio okazało się, że jak delikatnie zmieni się struktura HTML’a to IndexOf przestaje działać. Ja, czuły na to, poniżej chciałbym pokazać jak rozwiązaliśmy pewien problem i użyć “power of regex”.

Mamy narzędzie, które generuje z aspx’a HTML. Nie używamy razora czy innych ciekawych rzeczy. Nie używamy też żadnego sensownego rozwiązania które kompiluje aspx. Ktoś kiedyś stwierdził, że zrobi to sam. No i zrobił…

W każdym razie problemem jest to, że <style></style> jest rozrzucony po całym pliku HTML, a tak naprawdę potrzebny jest w nagłówku (czyli tam gdzie powinien być, choć jeszcze lepiej jakby był w osobnym pliku). Jak wspomniałem powyżej – standardowo (widziałem wiele rozwiązań takich) byłoby string.IndexOf(), string.Copy() etc. Poniżej kod zbierający style w całym pliku z pomocą regex:

Powyższy kod powoduje, że pobieramy wszystkie znaki które znajdują się pomiędzy znacznikami <style> i </style>. Potem ten zapisany regex porównujemy z tekstem, w tym wypadku zawartością pliku HTML. W wyniku otrzymujemy grupy, ponieważ nasze zapytanie zawarliśmy w nawiasach ( i ). W grupie pierwszej (index 0) otrzymujemy całe wyrażenie, a w grupie drugiej (index 1) wyrażenie zapisane w nawiasach. Znacznik “.*” oznacza:

  • “.”- weź dowolny znak,
  • “*” – weź dowolną ilość tego co przede mną.

Czyli łącząc – weź dowolną ilość dowolnych znaków przed znacznikiem </style>.

Jest z nim jednakoż pewien problem. A właściwie dwa. Pierwszy to to, że kod ten nie jest w stanie zebrać stylu, który jest zapisany w wielu linijkach:

Jest to spowodowane tym, że znak “.” pobiera wszystkie znaki oprócz znaku końca linii.

Drugi problem jest taki, że kod tej funkcji zmienia jakąś zmienną globalną, a nie zwraca wartości. To spory błąd, który zmniejsza czytelność kodu, bo tak naprawdę nie wiemy, gdzie wynik tej funkcji będzie użyty. Wykonanie funkcji jest oddzielone od skorzystania z wyniku jej działania. Pierwsza próba refactoringu wyglądała tak:

Przede wszystkim zażegnaliśmy całkowicie drugi problem. Od tej pory funkcja zwraca swą wartość i od razu możemy jej użyć w miejscu wywołania, co zrobiliśmy. Natomiast regex został zmieniony na [\s\S]* czyli:

  • “[]” – weź dowolny ze znaków zapisanych w tym nawiasie,
  • “\s” – weź dowolny znak biały, łącznie ze znakiem nowej linii,
  • “\S” – weź dowolny znak nie biały,
  • “*” weź wiele tego co jest przede mną.

Czyli weź wszystkie znaki białe i nie białe, które są pomiędzy znacznikami <style> i </style>.

Wszystko wyglądało pięknie w kodzie, ale niestety, nie w jego egzekucji. Po wykonaniu tego kodu, regex łapał wszystkie znaki pomiędzy pierwszym znacznikiem <style>, a ostatnim znacznikiem </style> włącznie z całym kodem HTML znajdującym się pomiędzy nimi. No, a nie do końca o to nam chodziło.

Po szybkiej naradzie i rozmowie z “mądrzejszymi” kod zmodyfikowaliśmy na taki:

Oznacza on nic innego jak:

  • “[]” weź dowolny ze znaków zapisanych w tym nawiasie,
  • “^” nie bierz tego co po prawej (zaprzeczenie),
  • “<” weź znak mniejszości,
  • “*” – weź wiele tego co jest przede mną.

Czyli jednym słowem weź wszystkie znaki pomiędzy <style> i </style> które nie są znakiem “<“. Co całkowicie nas satysfakcjonowało… Do czasu, aż okazało się, że można tak:

No więc należało wziąć pod uwagę wszystkie znaczniki, które zaczynają się od “<style” potem mają dowolny tekst i kończą na “>”. Można to osiągnąć tak:

Za pomoca regexów można osiągnąć wiele. Widziałem wielo-linijkowe programy, które wyglądały na ciężkie i skomplikowane w porównaniu do prostego wyrażenia regularnego zawartego w 1 linijce i wykonującego to samo. Polecam zresztą poćwiczyć – fajnym miejscem na to jest CodeWars. Zaraz po rozwiązaniu problemu widzimy tam jak inni go zrobili i traficie tam na pewno na piękne regexy. W ogóle wyrażenia regularne to moim zdaniem siła numer 2 po programowaniu funkcyjnym. Ktoś ma inne zdanie?

 

9 Replies to “Power of regex

  1. Pingback: dotnetomaniak.pl
    1. Zgadzam się z Tobą całkowicie! Ale jeśli regex 1 jest trudny, to może za pomocą “dziel i rządź” warto rozbić go na 2/4/8/16/[…] łatwiejszych problemów, a jak już je rozwiążemy to ewentualnie scalić?
      Właściwie tak było z tym przykładem. Najpierw prosty

  2. Moim zdaniem to jest idealny przykład na wykorzystanie F#, Ten problem aż prosi się o funkcyjne rozwiązanie:

    Pattern matching (mamy trafienie albo nie)
    – jak nie to nic nie rob
    – jak tak to klasynczy Fold (czy Reduce jak inni to zwą)

    Wszystko ładnie bez ifów, zmiennych ani pętli.

    1. No nie no, za mało jeszcze F# w C# w tym projekcie. Że dało się to lepiej zrobić – wierzę. Co chwila bym coś poprawiał, ale na dłuższą metę – chodziło o regex, a nie okolice 🙂

      1. Wydaje mi się że prawidłowym i 100% pewnym rozwiązaniem tego problemu jest sparsowanie HTML do AST i praca na tym. Każda inna forma jest ułomna pod jakimś względem, bo zawsze możesz trafić na zapis jakiego Twój regex nie obsługuje.
        HTML to nie są logi, które zawsze wyglądają tak samo 🙂

        1. Jeżeli chodzi o parsowanie całego HTML – zgadzam się. Jeśli chodzi o parsowanie jego pewnego, ustalonego fragmentu, to nie widzę problemu by użyć regexa. Poza tym regex przydaje się w wielu miejscach choćby w poleceniu sed czy przy pracy bieżące w “find & replace”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *