Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Krótkie ale bardzo użyteczne wyrażenie regularne - lookbehind, lazy, group i backreference

Ostatnio chciałem wyciągnąć z logów komunikaty wysyłane do zewnętrznego systemu i przeprowadzić kilka operacji LINQ to XML na pozyskanych danych. Oto przykładowa linia logu (uproszczona, rzeczywisty log był o wiele bardziej skomplikowany ale nie ma to znaczenia w tym poscie):

Call:<getName seqNo="56789"><id>123</id></getName> Result:<getName seqNo="56789">John Smith</getName>

Interesowały mnie takie dane XML ("call"):

<getName seqNo="56789">
  <id>123</id>
</getName>

Przy okazji: najprostszym sposobem na pozyskanie ładnie sformatowanego XMLa w .NET 3.5 lub nowszym jest wywołanie metody ToString na obiekcie XElement:

var xml = System.Xml.Linq.XElement.Parse(someUglyXmlString);     
Console.WriteLine(xml.ToString());

Jeśli chodzi o log, kilka rzeczy było pewnych:

  • XML z callem znajduje się po tekście „Call:” z początku linii
  • nazwa głównego elementu (roota) komunikatu będzie skądać się wyłącznie ze znaków alfanumerycznych lub podkreślenia
  • w komunikacie nie będzie znaków podziału linii
  • głowy element calla może wystąpić w logu także w sekcji „Result”
Otrzymanie prawidłowych danych było dosyć proste dzięki klasie Regex:
Regex regex = new Regex(@"(?<=^Call:)<(\w+).*?</\1>");
string call = regex.Match(logLine).Value;

To krótkie wyrażenie regularne ma kilka interesujących części. Nie jest może doskonałe ale okazało się bardzo przydatne podczas analizy logów. Jeśli powyższy regex nie jest dla Ciebie zupełnie jasny – czytaj dalej, prędzej czy później będziesz musiał użyć czegoś podobnego.

Poniżej jest to samo wyrażenie ale z komentarzami (ustawienie opcji RegexOptions.IgnorePatternWhitespace jest konieczne do przetworzenia wyrażenia zapisanego w ten sposób):

string pattern = @"(?<=^Call:) # Positive lookbehind dla znacznika wywołania
                   <(\w+)      # Capturing group dla nazwy tagu otwierającego
                   .*?         # Lazy wildcard (wszystko w środku)
                   </\1>       # Backreference do nazwy tagu otwierającego";   
Regex regex = new Regex(pattern, RegexOptions.IgnorePatternWhitespace);
string call = regex.Match(logLine).Value;

Positive lookbehind

(?<=Call:) to tzw. lookaround a dokładniej positive lookbehind. Jest to asercja zerowej szerokości, która pozwala na sprawdzanie czy tekst poprzedzony jest przez dany ciąg znaków. Tutaj „Call:” to tekst poprzedzający, którego szukamy. Zapis (?<=something) określa positive lookbehind. Istnieje również negative lookbehind zapisywany za pomocą (?<!something). Dzięki niemu można zweryfikować czy jakiś tekst nie posiada określonych znaków przed sobą. Lookaround sprawdza fragment tekstu ale nie stanowi on części dopasowanej wartości. Tak więc rezultatem tego:

Regex.Match("X123", @"(?<=X)\d*").Value

będzie „123” a nie „X123”

Silnik wyrażeń regularnych w .NET obsługuje także mechanizm lookaheads. Odwiedź  świetną stronę jeśli chcesz dowiedzieć się więcej o lookarounds.

Uwaga: w niektórych przypadkach (np. w naszym badaniu logu) zamiast positive lookaround można użyć grup nieprzechwytujących...

Capturing group

<(\w+) dopasowuje znak mniejszości, po którym następuje jeden lub więcej znaków z klasy \w (litery, cyfry lub znaki podkreślenia). Fragment \w+ jest otoczony nawiasami w celu utworzenia grupy zawierającej nazwę korzenia XML (getName dla przykładowej linii logu). Grupa ta jest później użyta do znalezienia tagu zamykającego przy użyciu odwołania wstecznego. (\w+) to grupa przechwytujaca (capturing group), co oznacza, że rezultat istnienia tej grupy jest dodawany do kolekcji Groups obiektu Match. Jeśli chcesz umieścić część wyrażenia w grupie ale nie chcesz wstawiać rezultatu do kolekcji Groups możesz skorzystać z grupy nieprzechwytującej. Grupę taką tworzy się poprzez dodanie pytajnika i dwukropka przed nawiasem otwierającym: (?:something) 

Lazy wildcard

.*? dopasowuje wszystkie znaki z wyjątkiem nowe linii (ponieważ nie używamy opcji RegexOptions.Singleline) w trybie leniwym (lazy lub non-gready) dzięki pytajnikowi umieszczonemu za gwiazdką. Domyślnie kwantyfikator * działa w trybie zachłannym (greedy) co oznacza, że silnik wyrażeń regularnych próbuje dopasować tak dużo tekstu jak to możliwe. W naszym przypadku, domyślny tryb spowodowałby zwrócenie zbyt długiego tekstu:

<getName seqNo="56789"><id>123</id></getName> Result:<getName seqNo="56789">John Smith</getName>

Backreference

</\1> dopasowuje zamykający tag XML, którego nazwa dostarczona jest przez \1 backreference. Wspomniana wcześniej grupa (\w+) ma numer 1, przez użycie składni \1 odwołujemy się do tekstu dopasowanego przez tą grupę. Więc dla naszego przykładowego logu </\1> zwraca </getName>. Jeśli wyrażenie regularne jest skomplikowane, dobrym pomysłem jest porzucenie numerowanych referencji na rzecz nazwanych. Grupę można nazwać poprzez składnię <name> lub <’name’> a odwołać się do niej można dzięki k<name> lub k’name’. Wyrażenie może więc wyglądać tak:

@"(?<=^Call:)<(?<tag>\w+).*?</\k<tag>>"

lub tak:

@"(?<=^Call:)<(?'tag'\w+).*?</\k'tag'>"

W naszym przypadku wersja druga jest lepsza. Użycie znaków < > w przypadku dopasowywania XML jest mylące. Silnik regex poradzi sobie z zapisem używającym < > ale pamiętaj, że kod źródłowy piszę się dla ludzi...

Wyrażenia regularne wyglądają strasznie ale warto poświęcić kilka godzin na ich przećwiczenie, regexy to niesamowicie przydatne narzędzie (nie tylko w czasie analizy logów)!

Komentarze (1) -

  • kaziel

    2019-06-14 10:48:25 | Odpowiedź

    Dziękuję, w końcu znalazłem coś co rozwiązało w elegancki sposób mój problem.

Dodaj komentarz

Loading