Przykładowe wyrażenia regularne

Kontynuujemy dwuczęściową serię wprowadzającą w wyrażenia regularne. Na kilku przykładach opiszemy sposób ich działania i zobaczymy efekty.

Dwa wyrazy różniące się jedną literą

Zacznijmy od czegoś prostego. Niektóre pary wyrazów różnią się tylko jednym znakiem. Dzięki wyrażeniom regularnym, ich odnalezienie jest banalnie proste.

Czasem jeden wyraz zawiera literę, której drugi jest pozbawiony. Taką parę tworzą wyrazy „żelazko” i „żelazo”. Znak opcjonalny w danej frazie (tutaj — literę „k”) możemy oznaczyć przy pomocy pytajnika:

żelazk?o

Innym razem zamiast konkretnej litery występuje inna. Tutaj dobrym przykładem są „kopać” i „kąpać”. Do ich odnalezienia służą zakresy:

k[oą]pać

Dwa wyrazy różniące się kilkoma literami

Inne pary wyrazów są podobne, ale różnią się więcej niż jednym znakiem. W takim przypadku konieczne jest wykorzystanie grupowania.

W parze „lek” i „lekarski” jeden wyraz zawiera kilka liter, których drugi jest pozbawiony. Do znalezienia obu służy wyrażenie:

lek(arski)?

Warto przy tym zauważyć, iż zapis taki nie jest równoważny wyrażeniu:

leka?r?s?k?i?

To drugie znajdzie również wyraz „lekarki”, którego nie poszukujemy.

Istnieją również pary wyrazów dzielące kilka liter, ale różniące się pozostałymi. Tak jest w przypadku nazw miast „Kutno” i „Krosno”. W takiej sytuacji należy skorzystać ze znaku alternatywy w obrębie zgrupowania:

K(ut|ros)no

Wyszukiwanie całych słów z jednym rdzeniem

Ponieważ język polski należy do grupy języków fleksyjnych, istnieją w nim całe grupy wyrazów o wspólnym rdzeniu. Dla przykładu, wszystkie wyrazy utworzone od czasownika „biegać” dzielą rdzeń „bieg”: „bieg-nący”, „bieg-ający”, „bieg-owy”, „bieg-acze” czy „bieg-nij”.

Do znalezienia wszystkich wyrazów o wspólnym rdzeniu moglibyśmy wykorzystać alternatywę w ramach zgrupowania. Jednak wypisanie wszystkich możliwych końcówek byłoby bardzo pracochłonne, łatwo również przy tym o pomyłkę. Dlatego lepiej skorzystać z symbolu dopasowania \w i odpowiedniego modyfikatora liczby dopasowań:

bieg\w+

Należy przy tym pamiętać, że niektóre głoski mogą przechodzić w inne. W przypadku rdzenia „bieg”, „g” przechodzi w „ż” (np. „bież-ący”, „bież-nia”). Niedopatrzenie to możemy uzupełnić przy pomocy zakresów:

bie[gż]\w+

Dodatkowo rdzeń może utracić głoskę. Tak jest w przypadku rdzenia „zrobi” („zrobi-łem”, „zrobi-sz” itd.), w którego niektórych formach pochodnych brakuje „i” (np. „zróbże”; dodatkowo widać, jak „o” przechodzi w „ó”). Sytuacje takie możemy uwzględnić przy pomocy pytajnika:

zr[oó]bi?\w+

Okno programu Writer z podświetlonymi wyrazami o tym samym rdzeniu („bieg”)

Znaki przestankowe, przed którymi jest spacja

Powyższe przykłady dobrze pokazywały podstawowe zasady konstruowania wyrażeń regularnych, ale były raczej oderwane od rzeczywistości. Przejdźmy więc do bardziej praktycznych sposobów ich wykorzystania.

W języku polskim standardem de facto jest umieszczanie odstępu po znaku przestankowym, ale nie przed nim. Przy pomocy poniższego wyrażenia regularnego znajdziemy wszystkie znaki interpunkcyjne, przed którymi znajduje się zbędny odstęp:

[[:space:]][.,:;?!)\x{2026}\x{201D}-]

Wyrażenie to składa się z dwóch zakresów. Pierwszy — nazwany — dopasowuje się do różnych rodzajów odstępów. Drugi zawiera listę znaków interpunkcyjnych, przed którymi spacja nie powinna się znaleźć.

Ten drugi zakres prowokuje do kilku uwag.

Po pierwsze, znajdują się w nim znaki, które w wyrażeniach regularnych pełnią również funkcje symboli dopasowania (kropka) lub modyfikatora liczebności (pytajnik). W poprzednim artykule zaznaczałem, iż znaki takie należy zacytować, gdy chcemy je odnaleźć w tekście. Dlaczego więc tutaj nie zostały poprzedzone odwróconym ukośnikiem? Ponieważ w obrębie zakresu tracą one swoje specjalne znaczenie. Tak więc kropka ujęta w nawiasy kwadratowe to po prostu kropka, a nie zakres zawierający dowolny znak.

Po drugie, dywiz (-) w ramach zakresu posiada specyficzne znaczenie. Aby go odnaleźć, musi zostać zacytowany lub umieszczony na początku albo końcu listy znaków w zakresie.

Ponadto wielokropek () oraz cudzysłów zamykający () identyfikujemy na podstawie ich kodu w standardzie Unicode. Oczywiście mogą one się znaleźć w obrębie zakresu, ale wymagałoby to uprzedniego wprowadzenia ich w inny sposób i skopiowania do schowka.

Wreszcie odpowiedzieć należy na pytanie, dlaczego w drugim zakresie ręcznie wpisaliśmy listę znaków (narażając się na pominięcie któregoś z nich), zamiast skorzystać z zakresu nazwanego [[:punct:]]. Przyczyną jest fakt, iż zakres ten zawiera również symbole, przed którymi spacja jest pożądana — np. otwarcie nawiasu (() czy półpauzę ().

Podczas pracy z wyrażeniami regularnymi łatwo o drobne błędy, w wyniku których odnajdują one niechciane fragmenty. Osobiście preferuję szybkość pisania nad precyzję, w związku z czym liczę się z pewną liczbą „fałszywych alarmów”. Dlatego nigdy nie klikam przycisku Zamień wszyst..

Z mojej perspektywy zakres nazwany [[:punct:]] jest całkiem niezłym przybliżeniem, ale nie wszystkim musi on odpowiadać.

Okno programu Writer z podświetlonymi znakami interpunkcyjnymi, przed którymi wstawiono spację

Dotychczas ograniczyliśmy się wyłącznie do znalezienia miejsc wymagających poprawek. Jednak przy pomocy wyrażeń regularnych możemy błędy tego typu automatycznie poprawić!

W tym celu grupujemy drugi zakres (ujmujemy go w nawiasy), zaś w polu Zamień na wpisujemy $1. Odpowiednie ustawienia przedstawia również obrazek poniżej.

Okno „Znajdź i zamień” z wyrażeniem regularnym znajdującym znaki interpunkcyjne z niepotrzebnymi spacjami

Znaki przestankowe, po których nie ma spacji

Jak wspomniałem, spacja nie powinna znajdować się przed znakami interpunkcyjnymi, ale powinna być umieszczona za nimi. Ten drugi typ błędu również możemy łatwo zidentyfikować i usunąć przy pomocy wyrażeń regularnych:

[.,:;?!)\x{2014}\x{2013}\x{2026}\x{201D}][[:alpha:]]

Dokonane zmiany nie są zbyt duże. W zakresie znaków interpunkcyjnych znalazły się pauza i półpauza, które powinny być oddzielone odstępem z obu stron; usunięty został dywiz, wokół którego odstępów nie stawiamy. Zakres nazwany nie określa już odstępów, a dowolną literę. Ponadto zmieniła się kolejność zakresów; wynika ona oczywiście z faktu, że teraz interesuje nas to, co jest po znaku przestankowym.

Jeżeli chcemy automatycznie poprawić wszystkie błędy, musimy zgrupować oba zakresy, zaś w polu Zamień na wpisać: $1 $2 (dwa odwołania wsteczne oddzielone spacją). Poprawne ustawienia przedstawia również ilustracja poniżej po prawej stronie.

Przy okazji warto dodać, co naprawdę robi LibreOffice w trakcie takiej poprawki.

Przede wszystkim znajduje i zaznacza poszukiwany tekst. W przypadku naszego wyrażenia regularnego są to dwa symbole — znak interpunkcyjny oraz litera lub cyfra bezpośrednio za nim.

Następnie usuwa zaznaczenie.

Wreszcie na pozycji kursora umieszcza nowy tekst. W naszym przypadku jest to odwołanie wsteczne do pierwszej grupy (znaku interpunkcyjnego), spacja oraz odwołanie wsteczne do drugiej grupy (litery lub cyfry).

Tak więc program nie tyle wstawia odstęp pomiędzy znalezionymi symbolami, co raczej zastępuje jeden ciąg znaków innym. Uwaga ta jest ważna z dwóch powodów. Po pierwsze, jeżeli chcemy zachować jakiś fragment poszukiwanego tekstu, musimy pamiętać o grupowaniu i odwołaniu wstecznym. Po drugie, w wyniku takiej operacji tekst utraci wszelkie formatowanie.

Przypadkowe powtórzenie słowa

W wyniku różnych pomyłek, przerw w pisaniu oraz poprawek redakcyjnych, w dokumencie mogą pojawić się powtórzone wyrazy. Odnalezienie ich przy pomocy wyrażeń regularnych jest dziecinnie proste:

\b(\w+)\W+\1

O znajdujących się w tym zapisie symbolach dopasowania i modyfikatorach liczebności była już mowa. Nowością jest symbol pozycji granicy słowa (ang. word boundary). Zapewnia on, że znalezione zgrupowanie będzie stanowiło cały wyraz, a nie tylko jego fragment (różnicę ukazują ilustracje poniżej). Obecne odwołanie wsteczne oznacza nie „(\w+)”, ale tekst znaleziony w pierwszym zgrupowaniu, w jego dokładnym brzmieniu. Cała fraza znajdzie więc dwa identyczne wyrazy oddzielone przynajmniej jedną spacją lub znakiem interpunkcyjnym.

Aby automatycznie usunąć takie powtórzenia, należy w polu Zamień na umieścić $1. Zwróćmy przy tym uwagę, iż odwołania wsteczne do zgrupowanych fraz inaczej oznaczamy w zależności od tego, w którym polu okna Znajdź i zamień je umieszczamy.

Powtórzenia w zdaniu

Powtórzenia są zazwyczaj uznawane za uchybienia stylistyczne. Chociaż wyrażenia regularne nie są w stanie znaleźć ich wszystkich, mogą nam pomóc zidentyfikować przynajmniej niektóre. Służy do tego następujące, dość skomplikowane zapytanie:

\b(\w{3,5})\w+\W+(\w+\W+){1,8}\1

Rozpoczyna się ono od symbolu pozycji granicy słowa, zapewniającego że dopasowane zostaną tylko całe wyrazy. Po nim następują zgrupowane litery (od trzech do pięciu) — w zamyśle ma to być rdzeń słowa. Następnie końcówka fleksyjna oraz odstępy i znaki interpunkcyjne. Dalsza część to ciąg wyrazów i przerw między nimi, powtórzony od jednego do ośmiu razy. Na samym końcu odwołanie wsteczne do wcześniejszej grupy (rdzenia).

Zapis ten posiada wiele ograniczeń skutecznie zmniejszających jego przydatność. Po pierwsze, pozostaje bezsilne wobec rdzeni choćby odrobinę odmiennych w zależności od kontekstu (w których „o” przechodzi w „ó” czy „g” w „ż”). Po drugie, pracuje wyłącznie na słowach liczących przynajmniej cztery znaki (nie znajdzie więc nadużywanych „że”, „czy”, „lub”, „ale” itp.). Wreszcie powtórzeń poszukuje tylko w obrębie następnych dziewięciu wyrazów, co jest wyborem raczej arbitralnym.

Oczywiście niektóre z tych barier da się usunąć tworząc bardziej skomplikowane wyrażenie regularne. Przykład ten jest jednak pouczający o tyle, o ile ukazuje, że wyrażenia regularne nie są w stanie rozwiązać wszystkich naszych problemów.

Podobał Ci się ten artykuł? Zapisz się na listę subskrybentów i otrzymuj informacje o następnych

komentarze 4

  • Leon Miklosik pisze:

    Podziwiam Pana wiedzę i pracę. Pozdrawiam.

  • andy pisze:

    Niestety, formuła \b(\w+)\W+\1 jest nieprawidłowa a przykład tendencyjny gdyz znajduje takie kwiatki jak: (do d)omu, czy tak, tak – co jest oczywiście w drugim przypadku tekstem prawidłowym. Aby pozbyć się pierwszego przypadku można dodać \b(\w+)(\W+\1\s) – zrobiłem dwie grupy żeby widać było lepiej wyniki :).Wolę wyraz ze spacją „za” niż jego kawałek. Drugi przypadek potrzebuje warunku istnienia TYLKO spacji lub nieistnienia czegokolwiek: taktak, tylkotylko.

    • W artykule jest wyraźnie napisane, co robi podane wyrażenie. Tak, dopasuje się do zwrotów „do domu”, „i Ikar”, „jak jakikolwiek”, „z Zenonem” i podobnych. Można to rozwiązać dodają znacznik granicy słowa za odwołaniem wstecznym. Albo symbol odstępu, tak jak proponujesz. Kilka akapitów ponad fragmentem, o którym rozmawiamy, napisałem, że podczas pracy z wyrażeniami regularnymi łatwo o drobne pomyłki i ślepe klikanie „Zamień wszystkie” nie jest dobrym pomysłem.

      Ogólnie rzecz ujmując, Twoja uwaga jest słuszna, ale forma jej przedstawienia wyolbrzymia problem.

      „Tak, tak” jest przypadkiem brzegowym, pokazującym projektową słabość wyrażeń regularnych. Ten mechanizm nie jest w stanie odróżnić powtórzeń poprawnych („i tak, i tak”, „trochę tego, trochę tego” itp.) od powtórzeń niepoprawnych („tylko tylko”, „błąd błąd” itp.). Aby pominąć poprawne powtórzenia, to już potrzebny jest algorytm do przetwarzania języka naturalnego.
      W wyrażeniach regularnych można co najwyżej stworzyć listę wyjątków. Ale raz, że niekoniecznie będzie ona wyczerpująca, a dwa, że znacznie komplikuje wyrażenie.

  • andy pisze:

    Hm, no nie wiem, które podejście jest lepsze 😀 Osobiście wolę posiedzieć tydzień nad (może i rozudowanym) zbliżonym do dobrego (nie piszę doskonałym 😀 ) wyrażeniem regex i mieć je uzyteczne do wiekszej ilości publikacji na 700 czy więcej stronach tekstu niż nie rozbudowane i mało skomplikowane ale przydatne tylko do 1 czy 2 stron. Na małej ilości tekstu można zamiast regex użyć po prostu słowa, np.: tylko i patrzeć czy jedno wystąpienie będzie zaraz za drugim, przewijając tekst i sprawdzając zaznaczenia. Po co mi wtedy regex?
    Proponuję pokusić się o regex do zaznaczenia wszystkich wyrazów (typu: ósemka, nić, kąt, itd… ) , w których występują polskie znaki diaktryczne ale tak, by zaznaczał się cały wyraz plus oczywiście wyrazy z dywizem (czerwono-złoty) bez otaczających spacji czy innych znaków (.,: itd).