Odcinek 12/15 – Wyrażenia regularne (regex) w Pythonie bez strachu

foto kurs pythona odcinek 12

Cel: po tym rozdziale:

  • rozpoznasz wzorce w tekście (np. e-mail, telefon, datę),
  • wyciągniesz potrzebne fragmenty,
  • zrobisz sprytne zamiany (sprzątanie tekstu),
  • poznasz flagi i lookaroundy (bez strachu).

1) Start: moduł re i „surowe” łańcuchy

Regex to wzorzec zapisany jako tekst. W Pythonie piszemy go jako surowy łańcuch (z r"..."), żeby backslash \ nie robił niespodzianek.

import re

wzorzec = r"\d\d-\d\d"          # np. 12-34
tekst   = "Kod: 12-34 i 99-01"

m = re.search(wzorzec, tekst)   # pierwsze dopasowanie
print(m.group())                # 12-34
  • re.search – znajduje pierwsze dopasowanie.
  • Chcesz wszystkie? Użyj findall albo finditer:
print(re.findall(wzorzec, tekst))  # ['12-34', '99-01']

for dop in re.finditer(wzorzec, tekst):
    print(dop.span(), dop.group())

2) Najważniejsze klocki składni (po ludzku)

  • . – dowolny znak (poza nową linią)
  • \d – cyfra, \w – „znak słowa” (litera/cyfra/_), \s – biały znak
  • +1 lub więcej, *0 lub więcej, ?0 lub 1
  • {m,n} – od m do n razy, np. \d{2,4}
  • [] – zestaw znaków, np. [A-ZĄĆŁÓŚŻŹ]
  • () – grupowanie i łapanie fragmentu
  • | – „albo”
  • ^ / $ – początek / koniec linii (lub całego tekstu – zależy od flag)

Przykład – polski kod pocztowy DD-DDD:

kod = r"^\d{2}-\d{3}$"
print(bool(re.fullmatch(kod, "80-241")))  # True
print(bool(re.fullmatch(kod, "80241")))   # False

re.fullmatch wymaga dopasowania całości.


3) Grupy (w tym nazwane) – wyciągasz tylko to, co ważne

email = r"([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[A-Za-z]{2,})"
m = re.search(email, "Kontakt: ala.kowalska@example.com")
if m:
    nazwa, domena = m.group(1), m.group(2)
    print(nazwa, domena)

Nazwane grupy są czytelniejsze:

email_named = r"(?P<user>[a-zA-Z0-9._%+-]+)@(?P<host>[a-zA-Z0-9.-]+\.[A-Za-z]{2,})"
m = re.search(email_named, "Napisz: test+info@firma.pl")
print(m.group("user"), m.group("host"))

4) Zamiana w tekście – re.sub (tak sprzątasz tekst)

Proste podmiany:

tekst = "Ala    ma   kota"
czysto = re.sub(r"\s+", " ", tekst)  # wiele spacji → jedna
print(czysto)  # Ala ma kota

Funkcyjna” zamiana – pełna kontrola:

import re

email_named = r"(?P<user>[a-zA-Z0-9._%+-]+)@(?P<host>[a-zA-Z0-9.-]+\.[A-Za-z]{2,})"

def maskuj_email(m):
    user = m.group("user")
    host = m.group("host")
    return f"{user[:2]}***@{host}"

mask = re.sub(email_named, maskuj_email, "Kontakt: jan.kowalski@firma.pl")
print(mask)  # Kontakt: ja***@firma.pl

5) Flagi – kiedy chcesz inaczej

  • re.IGNORECASE / re.I – ignoruj wielkość liter
  • re.MULTILINE / re.M^ i $ działają dla każdej linii
  • re.DOTALL / re.S – kropka . obejmuje też nową linię
  • re.VERBOSE / re.X – dziel wzorzec na linie i komentuj
wzorzec = re.compile(r"""
    ^\s*                 # opcjonalne spacje na początku
    (?P<key>\w+)\s*:\s*  # klucz:
    (?P<val>.+?)         # wartość (najkrótsze dopasowanie)
    \s*$                 # opcjonalne spacje na końcu
""", re.X | re.M)

txt = "imie: Ala\nmiasto:  Gdańsk"
for m in wzorzec.finditer(txt):
    print(m.group("key"), "->", m.group("val"))

re.compile(...) opłaca się, gdy wzorca używasz wielokrotnie.


6) Lookaroundy – spójrz obok, ale nie „zjadaj”

  • (?=...)pozytywny lookahead: dalej musi być…
  • (?!...)negatywny lookahead: dalej nie może być…
  • (?<=...)pozytywny lookbehind: przed dopasowaniem musi być…
  • (?<!...)negatywny lookbehind: przed dopasowaniem nie może być…

Przykład – tylko liczby przed „PLN”:

ceny = "25 PLN, 9.99 USD, 100 PLN"
print(re.findall(r"\d+(?:\.\d+)?(?=\s*PLN)", ceny))  # ['25', '100']

7) Bezpieczeństwo i zdrowy rozsądek

  • Użytkownik może wpisać . + ( – w regexie to specjalne znaki.
    Chcesz dopasować dosłownie? Użyj re.escape: szukane = input("Czego szukasz? ") wzorzec = re.compile(re.escape(szukane), re.I) print(bool(wzorzec.search("To jest bezpieczne wyszukiwanie.")))
  • Regex nie zawsze jest potrzebny. Czasem split, in, startswith są prostsze i szybsze.
  • Bardzo skomplikowane wzorce – trudne w utrzymaniu. Używaj re.VERBOSE i komentarzy.

8) Małe, życiowe przykłady

A) „Na oko” walidacja telefonu PL (do nauki, nie perfekcyjna produkcja):

tel = r"^\+?48?\s?[- ]?\d{3}[- ]?\d{3}[- ]?\d{3}$"
for t in ["+48 600-700-800", "600700800", "48 600 700 800", "abc"]:
    print(t, "->", bool(re.fullmatch(tel, t)))

B) Wyciąganie wszystkich e-maili z tekstu

emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,}", """
Kontakt: ala@firma.pl, bok@example.com, test+help@dom.pl
""")
print(emails)

C) Normalizacja spacji i tabów

tekst = "Ala\t  ma    \t kota"
print(re.sub(r"\s+", " ", tekst).strip())  # Ala ma kota

D) Zamiana dat „DD/MM/RRRR” → „RRRR-MM-DD”

def repl(m):
    d, mth, y = m.group("d"), m.group("m"), m.group("y")
    return f"{y}-{mth.zfill(2)}-{d.zfill(2)}"

wz = re.compile(r"(?P<d>\d{1,2})/(?P<m>\d{1,2})/(?P<y>\d{4})")
print(re.sub(wz, repl, "Spotkanie: 1/9/2025, backup: 12/10/2025"))

9) Typowe pułapki (i jak ich uniknąć)

PułapkaCo się dziejeJak żyć
. nie łapie nowej liniidopasowania „urywają się”użyj flagi re.S (DOTALL)
Niechciane ogromne dopasowanie„zachłanne” .+ zjada za dużoużyj wersji „leniwych” .+?
Literówki w wzorcuwynik „niby działa” ale źletestuj na kilku przykładach
Zbyt skomplikowany regextrudny w utrzymaniurozbij na części, użyj re.X i komentarzy

10) Ćwiczenia (koniecznie spróbuj)

  1. Kropki do jednej – zamień ... lub .. na .
  2. Adresy IP – wyciągnij z tekstu wzorce 123.45.67.89
  3. Kod pocztowy – sprawdź DD-DDD (pełne dopasowanie)
  4. Ceny w PLN – wyciągnij liczby (całe i z częścią dziesiętną) przed PLN
  5. Wielka litera – znajdź słowa zaczynające się od dużej litery (np. imiona)

Podsumowanie

Umiesz już:

  • szukać (search, findall, finditer) i dopasowywać całość (fullmatch),
  • używać grup (w tym nazwanych) i robić zamiany (sub, także funkcją),
  • stosować flagi, lookaroundy i kompilować wzorce,
  • wybierać, kiedy regex ma sens, a kiedy lepiej użyć prostszych metod.