Regular Ne

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 18

Moduł `re` i wyrażenia regularne

Podczas jednej z lekcji poświęconej łańcuchom znaków poznaliśmy sposoby wyszukiwania i zamiany
jednych łańcuchów w innych. Używaliśmy w tym celu metod obiektów typu String:

Metoda Działanie
startswith(str) Wyszukiwanie sekwencji znaków na początku
łańcucha
endswith(str) Wyszukiwanie sekwencji znaków na końcu
łańcucha
count(str [,START [, KONIEC]]) Liczba wystąpień sekwencji znaków
find(str [,START [, KONIEC]]) Wyszukiwanie sekwencji znaków od początku
łańcucha. Zwracany jest indeks pierwszego
wystąpienia. W przypadku nieznalezienia
sekwencji zwracana jest wartość -1.
rfind(str [,START [, KONIEC]]) Wyszukiwanie sekwencji znaków od końca
łańcucha. Zwracany jest indeks pierwszego
wystąpienia od końca. W przypadku
nieznalezienia sekwencji zwracana jest wartość -1.

index(str [,START [, KONIEC]]) Podobnie jak find(), ale w przypadku


nieznalezienia sekwencji zwracany jest błąd.
replace(str1, str2 [,N]) Zmienia wystąpienia ciągu str1 na str2 w łańcuchu
znaków. Jeśli poda się liczbę (N) to dokona się
maksymalnie N zmian, w przypadku braku tego
parametru, zmienią się wszystkie znalezione
sekwencje znaków.
Metody te są użyteczne, ale mają pewne ograniczenia, na przykład pozwalają jedynie na znalezienie
dokładnie takich sekwencji znaków, jakie zostały wskazane. Znacznie większe możliwości oferują
wyrażenia regularne.

Moduł re
Do pracy z wyrażeniami regularnymi będziemy używać modułu re wchodzącego w skład Biblioteki
Standardowej Pythona. Jego dokumentację można znaleźć pod adresem
https://docs.python.org/3.8/library/re.html.

Zacznijmy od bardzo prostych przykładów pokazujących niektóre (!) dostępne w module funkcje, na
razie bez wyrażeń regularnych. Zauważ, że przy tworzeniu ciągów znaków, które będziemy
dopasowywać, będziemy używać surowych łańcuchów (ang. raw strings), które oznaczamy literą r lub
R, dzięki czemu znak \ nie tworzy sekwencji ucieczkowej, czyli jest, wraz z następnym znakiem
odczytywany literalnie. Nie zawsze jest to konieczne, ale jest uważane za dobrą praktykę, więc
wprowadzimy ją od początku.

# Import modułu re

import re

1
wynik = re.match(r'Python', 'Python to nie tylko wąż, ale też język programowania.')

print(wynik)

<re.Match object; span=(0, 6), match='Python'>

Funkcja match() zwróciła obiekt typu Match, który zawiera informację o znalezionym ciągu znaków
dopasowanym do szukanego, oraz jego lokalizacji. Oczywiście możemy łatwo te informacje uzyskać:

print(f"Dopasowano: '{wynik.group()}', początek: {wynik.span()[0]}, koniec: {wynik.span()[1]}")

Dopasowano: 'Python', początek: 0, koniec: 6

Metoda group() zwraca wynik dopasowania, z kolei span() zwraca krotkę zawierającą indeksy
pierwszego i ostatniego znaku dopasowania w szukanym łańcuchu.

Wartości te można także uzyskać wywołując na obiekcie typu Match metody start() i end():

print(f"Dopasowano: '{wynik.group()}', początek: {wynik.start()}, koniec: {wynik.end()}")

Dopasowano: 'Python', początek: 0, koniec: 6

Poszukajmy teraz ciągu ni.

wynik = re.match(r'ni', 'Python to nie tylko wąż, ale też język programowania.')

print(wynik)

None

Tym razem zamiast obiektu typu Match otrzymaliśmy None, tak jest, gdy nie znaleziono dopasowania.
Stało się tak, ponieważ metoda match() dopasowuje szukany ciąg znaków od pierwszego znaku
łańcucha, w którym szukamy. Zatem jeśli dopasowanie nie znajduje się na początku, ale gdzieś dalej
(lub w ogóle go nie ma), otrzymujemy None.

Jeśli chcemy wyszukać ciąg znaków, niekoniecznie na początku łańcucha to użyjemy metody search():

wynik = re.search(r'ni', 'Python to nie tylko wąż, ale też język programowania.')

# Jeżeli zwrócony zostanie wynik

if wynik:

print(f"Dopasowano: '{wynik.group()}', początek: {wynik.start()}, koniec: {wynik.end()}")

# Jeżeli zwrócone zostanie None

else:

print('Nie znaleziono szukanego ciągu znaków.')

Dopasowano: 'ni', początek: 10, koniec: 12

Zauważ, że zwrócone zostało tylko pierwsze dopasowanie do łańcucha ni.

Powyżej wykorzystywaliśmy metody match() i search() przekazując im bezpośrednio dwa argumenty:


łańcuch szukany (czyli wzorzec) i łańcuch, w którym szukamy. Zwykle postępuje się jednak inaczej.
Najpierw kompiluje się wzorzec, a potem wykorzystuje się otrzymany obiekt (typu Pattern) do

2
wyszukiwania w ciągu znaków. Przy okazji przypiszmy łańcuch znaków do zmiennej, co zwiększy
przejrzystość kodu.

# Łańcuch znaków, w którym wyszukujemy wzorca

tekst = 'Python to nie tylko wąż, ale też język programowania.'

# Kompilacja wzorca

wzorzec = re.compile(r'ni')

# Wyszukiwanie

wynik = wzorzec.search(tekst)

# Jeżeli zwrócony zostanie wynik

if wynik:

print(f"Dopasowano: '{wynik.group()}', początek: {wynik.start()}, koniec: {wynik.end()}")

# Jeżeli zwrócone zostanie None

else:

print('Nie znaleziono szukanego ciągu znaków.')

Dopasowano: 'ni', początek: 10, koniec: 12

Początkowo może się wydawać, że jest to niepotrzebna komplikacja, ale dzięki kompilowaniu wzorca
wyszukiwanie przebiega szybciej, zatem dalej będziemy używać tego sposobu.

Aby znaleźć wszystkie wystąpienie wzorca, użyjemy funkcji findall(). Jeśli wzorzec zostanie znaleziony,
zostanie zwrócona lista z dopasowanymi, niezachodzącymi na siebie, ciągami znaków. Jeśli nie,
zostanie zwrócona pusta lista.

wzorzec = re.compile(r'ni')

# Wyszukiwanie

wynik = wzorzec.findall(tekst)

# Jeżeli zwrócony zostanie wynik

if len(wynik) > 0:

print(f'Dopasowano: {wynik}')

# Jeżeli zwrócone zostanie None

else:

print(f'{wynik} Nie znaleziono szukanego ciągu znaków.')

Dopasowano: ['ni', 'ni']

Metoda findall() pozwala uzyskać listę dopasowań, ale nie otrzymujemy informacji na temat ich
położenia. Jeśli potrzebujemy takich danych, możemy użyć metody finditer(), która zwraca iterator -
obiekt, który w tym przypadku zawiera obiekty typu Match i umożliwia uzyskanie ich np. w pętli for.

3
wzorzec = re.compile(r'ni')

# Wyszukiwanie

wynik = wzorzec.finditer(tekst)

for w in wynik:

print(f'Dopasowano: "{w.group()}", początek: {w.start()}, koniec: {w.end()}')

Dopasowano: "ni", początek: 10, koniec: 12

Dopasowano: "ni", początek: 49, koniec: 51

Uwaga: pobierając z iteratora elementy, usuwamy je, nie można zatem ponownie się do nich odwołać.
Rozwiązaniem tego problemu może być uzyskanie z niego listy elementów, co przy okazji umożliwia
np. użycie funkcji len() aby pobrać liczbę wyników:

wzorzec = re.compile(r'ni')

# Wyszukiwanie

wynik = wzorzec.finditer(tekst)

lista_wynikow = list(wynik)

if len(lista_wynikow) > 0:

print(f'Uzyskano {len(lista_wynikow)} wyniki/ów.')

for w in lista_wynikow:

print(f'Dopasowano: "{w.group()}", początek: {w.start()}, koniec: {w.end()}')

else:

print('Brak wyników.')

Uzyskano 2 wyniki/ów.

Dopasowano: "ni", początek: 10, koniec: 12

Dopasowano: "ni", początek: 49, koniec: 51

Wzorca możemy też użyć do ,,pocięcia’’ ciągu znaków i uzyskania listy elementów, występujących
między dopasowaniami wzorca. Służy do tego funkcja split(). Podzielmy tekst na poszczególne słowa:

wzorzec = re.compile(r' ')

# Cięcie łańcucha znaków

wynik = wzorzec.split(tekst)

print(f'Uzyskano {len(wynik)} wyniki/ów.')

for w in wynik:

print(w)

Uzyskano 9 wyniki/ów.

4
Python

to

nie

tylko

wąż,

ale

też

język

programowania.

Kolejną użyteczną metodą jest sub(), która pozwala na zmianę wzorca na inny ciąg znaków:

wzorzec = re.compile(r'ni')

# Zamiana

zmieniony = wzorzec.sub('Ni!', tekst)

print(zmieniony)

Python to Ni!e tylko wąż, ale też język programowaNi!a.

Podsumujmy poznane metody:

Metoda; Działanie; Zwracany obiekt;


match(); Dopasowanie wzorca na Match
początku łańcucha.
search(); Dopasowanie w dowolnym Match
miejscu łańcucha
findall(); Dopasowanie wszystkich, lista
niezachodzących dopasowań.
finditer(); Dopasowanie wszystkich, iterator
niezachodzących dopasowań.
split(); Pocięcie łańcucha w miejscach lista
dopasowań.
sub(); Zamiana dopasowania wzorca łańcuch znaków
na inny łańcuch.
Więcej metod można znaleźć w dokumentacji

W większości powyższych przykładów używaliśmy metod wywołując je na obiektach typu Pattern, ale
możemy też ich użyć jako funkcji modułu re, choć należy pamiętać, że w takich przypadkach trzeba
dodać dodatkowy argument określający wzorzec:

tekst = 'Python to nie tylko wąż, ale też język programowania.'

print(re.match(r'Python', tekst)) # Zwracany obiekt typu Match

print(re.search(r'ni', tekst)) # Zwracany obiekt typu Match

5
print(re.findall(r'ni', tekst)) # Zwracana lista

for w in re.finditer(r'ni', tekst): # Zwracany iterator

# Zwracany obiekt typu Match

print(f'Dopasowano: "{w.group()}", początek: {w.start()}, koniec: {w.end()}')

print(re.split(r' ', tekst)) # Zwracana lista

print(re.sub(r'ni', "Ni!", tekst)) # Zwracany łańcuch znaków

<re.Match object; span=(0, 6), match='Python'>

<re.Match object; span=(10, 12), match='ni'>

['ni', 'ni']

Dopasowano: "ni", początek: 10, koniec: 12

Dopasowano: "ni", początek: 49, koniec: 51

['Python', 'to', 'nie', 'tylko', 'wąż,', 'ale', 'też', 'język', 'programowania.']

Python to Ni!e tylko wąż, ale też język programowaNi!a.

Wyrażenia regularne
Wyrażenia regularne (ang. regular expressions, w skrócie regex lub regexp) pozwalają na dopasowanie
nie tylko dokładnych ciągów znaków, ale także opisać z różnym stopniem ogólności, pewne grupy
ciągów znaków, czy ich kategorie, a następnie je wyszukać, znaleźć ich położenie, usunąć, czy zamienić
na inne.

Niektóre znaki w wyrażeniach regularnych odczytywanie są dosłownie (np. litery, cyfry), inne mają
znaczenie specjalne. Na przykład . (kropka) oznacza dowolny znak a + wskazuje, że poprzednia litera,
lub wyrażenie może wystąpić raz, lub więcej razy. Ponadto znaczenie niektórych znaków, np. ^ może
się zmieniać zależnie od kontekstu. Szczególnym przypadkiem jest znak lewego ukośnika (ang.
backslash), czyli , który nadaje specjalne znaczenie kolejnemu znakowi. Poznaliśmy np. \n oznaczający
nową linię, czy \t oznaczający tabulator, ale jest takich oznaczeń znacznie więcej, i \ może także
,,przywracać’’ dosłowne znaczenie znakom, które zwykle mają znaczenie specjalne. Na przykład, jak
wspomniałem wyżej, w wyrażeniach regularnych . oznacza dowolny znak, ale \. oznacza po prostu znak
kropki.

Początkowo wyrażenia regularne, zwłaszcza złożone, mogą się wydawać niezrozumiałym szyfrem, ale
z czasem stają się coraz bardziej czytelne. Warto się z nimi zapoznać, ponieważ są potężnym
narzędziem, niezwykle przydatnym w pracy z tekstem, danymi czy sekwencjami nukleotydów. Co
więcej, składnia i zasady dotyczące wyrażeń regularnych, które tu przedstawię, są podobne w wielu
innych językach programowania, programach przeznaczonych do pracy z tekstem (np. grep, sed) czy
edytorach tekstu. Zatem opanowanie ich może być przydatne także poza programowaniem w
Pythonie.

Zacznijmy zatem przygodę z wyrażeniami regularnymi. Jak wspomniałem kropka (.) oznacza dowolny
znak. Sprawdźmy, jak to działa:

tekst = 'Python to nie tylko wąż, ale też język programowania.'

6
wzorzec = re.compile(r'.')

print(wzorzec.findall(tekst))

['P', 'y', 't', 'h', 'o', 'n', ' ', 't', 'o', ' ', 'n', 'i', 'e', ' ', 't', 'y', 'l', 'k', 'o', ' ', 'w', 'ą', 'ż', ',', ' ', 'a', 'l', 'e', ' ', 't', 'e',
'ż', ' ', 'j', 'ę', 'z', 'y', 'k', ' ', 'p', 'r', 'o', 'g', 'r', 'a', 'm', 'o', 'w', 'a', 'n', 'i', 'a', '.']

Metoda findall() zwróciła listę wszystkich znaków, ponieważ zgodnie z opisem zwraca listę,
niezachodzących na siebie dopasowań. W tym przypadku każdy znak w teście został dopasowany do
wzorca.

Kwantyfikatory

Kwantyfikatory pozwalają na określenie ile wystąpień znaków, czy wzorców jest konieczne, aby
doszło do dopasowania.

Uzupełnijmy wyrażenie o znak +:

import re

tekst = "ACGGGATTAGGGGGGACCCGGT"

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'.+')

print(wzorzec.findall(tekst))

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['ACGGGATTAGGGGGGACCCGGT']

Znak + oznacza, jeden lub więcej dopasowań wzorca znajdującego się przed znakiem. Powyższy
wzorzec wyglądał tak: .+, zatem oznaczał ,,jeden, lub więcej dowolnych znaków’’ (bez \n), zatem
dopasowane zostały wszystkie znaki w łańcuchu.

Teraz zamieńmy + na *:

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'.*')

print(wzorzec.findall(tekst))

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['ACGGGATTAGGGGGGACCCGGT', '']

Znak * oznacza zero lub więcej dopasowań, zatem otrzymaliśmy dodatkowo wynik z pustym
dopasowaniem. Zamieńmy w powyższych przykładach wzorców . na X, który nie występuje w tekście:

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'X+')

print(f'Jeden lub więcej X: {wzorzec.findall(tekst)}')

wzorzec = re.compile(r'X*')

print(f'Zero lub więcej X: {wzorzec.findall(tekst)}')

7
Łańcuch: ACGGGATTAGGGGGGACCCGGT

Jeden lub więcej X: []

Zero lub więcej X: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']

Skoro w tekście nie było znaku X, w pierwszym przypadku nie uzyskaliśmy wyniku, w drugim
otrzymaliśmy listę dopasowań ,,zera lub więcej X’’, w takim przypadku uzyskaliśmy listę pustych
dopasowań dla każdego znaku.

Użyjmy teraz znaku ?, który oznacza: ,,zero, lub jeden’':

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'.?')

print(wzorzec.findall(tekst))

wzorzec = re.compile(r'.?T')

print(wzorzec.findall(tekst))

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['A', 'C', 'G', 'G', 'G', 'A', 'T', 'T', 'A', 'G', 'G', 'G', 'G', 'G', 'G', 'A', 'C', 'C', 'C', 'G', 'G', 'T', '']

['AT', 'T', 'GT']

Powyższe dopasowania z użyciem znaków * oraz + były zachłanne, co oznacza, że starały się dopasować
tak wiele znaków, jak to tylko możliwe (zastanów się, czemu drugie dopasowanie dla wzorca .?T
zwróciło pojedynczą literę T). Dodanie znaku ?, zmienia to zachowanie, na leniwe - dopasowane jest
tak mało znaków, jak to możliwe. Przy okazji zwróć uwagę, także w dalszej części lekcji, jak zmienia się
znaczenie znaku ? w zależności od kontekstu.

print(f'Łańcuch: {tekst}')

print(tekst)

wzorzec = re.compile(r'.+')

print(f'Dopasowanie zachłanne: {wzorzec.findall(tekst)}')

wzorzec = re.compile(r'.+?')

print(f'Dopasowanie leniwe: {wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

ACGGGATTAGGGGGGACCCGGT

Dopasowanie zachłanne: ['ACGGGATTAGGGGGGACCCGGT']

Dopasowanie leniwe: ['A', 'C', 'G', 'G', 'G', 'A', 'T', 'T', 'A', 'G', 'G', 'G', 'G', 'G', 'G', 'A', 'C', 'C', 'C', 'G', 'G',
'T']

Jeszcze jeden przykład:

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'G+')

8
print(f'Dopasowanie zachłanne: {wzorzec.findall(tekst)}')

wzorzec = re.compile(r'G+?')

print(f'Dopasowanie leniwe: {wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

Dopasowanie zachłanne: ['GGG', 'GGGGGG', 'GG']

Dopasowanie leniwe: ['G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G']

W pierwszym przypadku, dopasowane zostało tak wiele liter G jak to możliwe, w drugim, tak mało jak
to możliwe.

Możemy także określić ile dokładnie powinno być poprzedzających kwantyfikator wzorców. W takim
przypadku umieszczamy odpowiednie liczby w parze nawiasów klamrowych: wzorzec{n} - dokładnie n
dopasowań, wzorzec{n,m} - miedzy n a m dopasowań, wzorzec{n,} - n lub więcej dopasowań,
wzorzec{,m} - m lub mniej dopasowań, z 0 włącznie:

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'G{3}')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'G{2,3}')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'G{2,}')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'G{,3}')

print(f'{wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['GGG', 'GGG', 'GGG']

['GGG', 'GGG', 'GGG', 'GG']

['GGG', 'GGGGGG', 'GG'] ['', '', 'GGG', '', '', '', '', 'GGG', 'GGG', '', '', '', '', 'GG', '', '']

Podsumujmy:

Kwantyfikator Znaczenie
+ Jedno lub więcej dopasowań.
* Zero lub więcej dopasowań.
? Zero lub jedno dopasowanie.
{n} dokładnie n dopasowań.
{n,m} miedzy n a m dopasowań.
{n,} n lub więcej, dopasowań.
{,m} m lub mniej dopasowań, z 0 włącznie.

9
UWAGA: jeśli znaki +, * czy ? mają w wyrażeniu oznaczać konkretnie te znaki, to zależy je poprzedzić
znakiem ucieczki \. Np. \+ oznacza znak plusa, a nie jedno lub więcej dopasowań.

Wzorce poprzedzające i następujące po szukanym wzorcu


Możliwe jest też dopasowanie wzorca w zależności od tego, czy jest on (lub nie jest) poprzedzony lub
poprzedza dany wzorzec.

wzorzec(?=X) oznacza, że po wzorcu powinien występować X:

import re

tekst = "ACGGGATTAGGGGGGACCCGGT"

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'G+(?=A)')

print(f'{wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['GGG', 'GGGGGG']

Zwróć uwagę, że X nie jest częścią zwracanego dopasowania.

wzorzec(?!X) oznacza, że po wzorcu nie powinien występować X:

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'G+(?!A)')

print(f'{wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['GG', 'GGGGG', 'GG']

(?<=X)wyrażenie - X poprzedza wyrażenie.

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'(?<=C)G+')

print(f'{wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['GGG', 'GG']

(?<!X)wyrażenie - X nie poprzedza wyrażenia.

print(f'Łańcuch: {tekst}')

wzorzec = re.compile(r'(?<!C)G+')

print(f'{wzorzec.findall(tekst)}')

Łańcuch: ACGGGATTAGGGGGGACCCGGT

['GG', 'GGGGGG', 'G']

10
UWAGA: omawiane tu wzorce poprzedzające i następujące po szukanym wzorcu, muszą mieć
okresloną długość, nie stosujemy zatem takich znaków jak +, czy *.

Podsumujmy:

Wzorzec Znaczenie
wzorzec(?=X) po wzorcu powinien występować X
wzorzec(?!X) po wzorcu nie powinien występować X
(?<=X)wyrażenie X poprzedza wyrażenie.
(?<!X)wyrażenie X nie poprzedza wyrażenia.

Początek i koniec łańcucha


Istnieją także dwa znaki, pozwalające ,,zakotwiczyć’’ wzorzec przy początku (^) i końcu ($) łańcucha
znaków:

import re

tekst = "Jesteśmy rycerzami, którzy mówią: Ni!"

wzorzec = re.compile(r'^Jesteśmy')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'^rycerzami')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'Jesteśmy$')

print(f'{wzorzec.findall(tekst)}')

wzorzec = re.compile(r'Ni!$')

print(f'{wzorzec.findall(tekst)}')

['Jesteśmy']

[]

[]

['Ni!']

…lub…
Można także tak zdefiniować wyrażenie, aby uwzględniały kilka możliwości, używając znaku |, który
można odczytać jako “albo” czy “lub”:

import re

tekst = "Jesteśmy rycerzami, którzy mówią: Ni!"

wzorzec = re.compile(r'Jesteśmy|którzy|Ni!')

print(f'{wzorzec.findall(tekst)}')

11
['Jesteśmy', 'którzy', 'Ni!']

Wykorzystując parę nawiasów [] można na utworzyć wyrażenie, które pozwala na dopasowanie


któregokolwiek ze znajdujących się w nim znaków (znak | jest tu zbędny). Na przykład stwórzmy
wzorzec dopasowujący sekwencje GC poprzedzone A lub C. Zauważ, że zwracane są całe dopasowania:

import re

tekst = "AAAGCCTGCTTAGCC"

wzorzec = re.compile(r'[AT]GC')

print(f'{wzorzec.findall(tekst)}')

['AGC', 'TGC', 'AGC']

Jeśli dodamy w parze nawiasów kwadratowych [], znak ^, będzie to oznaczało, że następne znaki nie
mogą znaleźć się w dopasowaniu (zwróć uwagę na inne znaczenie znaku ^, niż w przykładach powyżej):

tekst = "AAAGCCTGCTTAGCC"

wzorzec = re.compile(r'[^AC]GC')

print(f'{wzorzec.findall(tekst)}')

['TGC']

Znaki specjalne i zakresy znaków


Wyrażenia regularne pozwalają także na określenie pewnych typów i zakresów znaków. Zauważ, że
wyrażenie z wielką litera oznacza przeciwieństwo wyrażenia z małą literą (np. \s i `\S’):

Wzorzec Znaczenie
\s biały znak

\S nie-biały znak

\d cyfra

\D nie-cyfra

\w znaki alfanumeryczne (litery i cyfry) oraz _

\W znaki nie-alfanumeryczne i nie _

\b początek lub koniec ,,słowa’’ - formalnie, \b


oznacza granicę między \w i \W

\B nie początek lub koniec ,,słowa''

[a-z] małe litery

[A-Z] wielkie litery

[0-9] cyfry

12
Wypróbujmy je na kilku przykładach:

import re

tekst = "AA-AGC*Caggt$12345_CGCT TAGacctgaCC 1234\n"

print(tekst)

def szukaj(opis, wzorzec):

print(f'{opis}: {wzorzec}')

wz = re.compile(wzorzec)

print(f'\t{wz.findall(tekst)}')

szukaj('Jeden lub więcej nie-białych znaków', r'\S+')

szukaj('Jeden lub więcej cyfr', r'\d+')

szukaj('Jeden lub więcej nie-cyfr',r'\D+')

szukaj('Jeden lub więcej znaków alfanumerycznych + _', r'\w+')

szukaj('Jeden lub więcej znaków nie-alfanumerycznych + _', r'\W+')

szukaj('Trzy znaki od granicy słowa', r'\b.{3}')

szukaj('Jeden lub więcej cyfr', r'[0-9]+')

szukaj('Jeden lub więcej małych liter',r'[a-z]+')

szukaj('Jeden lub więcej wielkich liter', r'[A-Z]+')

AA-AGC*Caggt$12345_CGCT TAGacctgaCC 1234

Jeden lub więcej nie-białych znaków: \S+

['AA-AGC*Caggt$12345_CGCT', 'TAGacctgaCC', '1234']

Jeden lub więcej cyfr: \d+

['12345', '1234']

Jeden lub więcej nie-cyfr: \D+

['AA-AGC*Caggt$', '_CGCT TAGacctgaCC ', '\n']

Jeden lub więcej znaków alfanumerycznych + _: \w+

['AA', 'AGC', 'Caggt', '12345_CGCT', 'TAGacctgaCC', '1234']

Jeden lub więcej znaków nie-alfanumerycznych + _: \W+

['-', '*', '$', ' ', ' ', '\n']

Trzy znaki od granicy słowa: \b.{3}

['AA-', 'AGC', '*Ca', '$12', ' TA', ' 12']

13
Jeden lub więcej cyfr: [0-9]+

['12345', '1234']

Jeden lub więcej małych liter: [a-z]+

['aggt', 'acctga']

Jeden lub więcej wielkich liter: [A-Z]+

['AA', 'AGC', 'C', 'CGCT', 'TAG', 'CC']

Flagi
Czytając dokumentację modułu re można zauważyć, że przy opisie wielu funkcji pojawia się argument
flags, np: re.compile(pattern, flags=0). Ustawiając opdowiednie flagi, można zmienić zachowanie tych
funkcji. Przyjrzyjmy się dwu z nich.

Flaga re.IGNORECASE, jak wskazuje nazwa, pozwala na ignorowanie wielkości znaków przy
dopasowywaniu:

import re

tekst = "AAGCGCCAAATGCGCGCTTAAAAGCC"

# Bez flagi

wzorzec = re.compile(r'gc')

print(f'{wzorzec.findall(tekst)}')

# z flagą

wzorzec = re.compile(r'gc', flags=re.IGNORECASE)

print(f'{wzorzec.findall(tekst)}')

[]

['GC', 'GC', 'GC', 'GC', 'GC', 'GC']

Flaga re.DOTALL pozwala na dopasowanie przez znak kropki . wszystkich znaków, także znaku nowej
linii:

tekst = 'Jesteśmy rycerzami,\nktórzy mówią: Ni!'

print(tekst)

# Bez flagi

wzorzec = re.compile(r'rycerzami,.którzy')

print(f'{wzorzec.findall(tekst)}')

# z flagą

wzorzec = re.compile(r'rycerzami,.którzy', flags=re.DOTALL)

14
print(f'{wzorzec.findall(tekst)}')

Jesteśmy rycerzami,

którzy mówią: Ni!

[]

['rycerzami,\nktórzy']

Bardziej złożone wyrażenia


Dotychczasowe wzorce były dość proste, możemy jednak budować bardziej złożone, pozwalające na
dopasowanie bardziej skomplikowanych ciągów znaków.

Na przykład z podanego tekstu, który może być odczytanym plikiem w formacie csv, wybierzmy
rzędy/linie, które zawierają dane dla Rumex acetosa o wartości pomiaru przynajmniej trzycyfrowej,
zawierające wartość T w polu ,,żywy’':

tekst = """

nr.,gatunek,próbka,pomiar,żywy

1,Rumex acetosa,A,243,T

2,Rumex acetosa,B,91,T

3,Zea mays,A,2432,T

4,Rumex thyrsiflorus,A,347,T

5,Rumex acetosa,C,3324,F

6,Rumex acetosella,A,347,T

7,Rumex thyrsiflorus,B,445,F

8,Oryza sativa,A,237,T

9,Rumex acetosa,C,1224,T

"""

wzorzec = re.compile(r'.+Rumex acetosa.+\d{3,}.+T')

print(f'{wzorzec.findall(tekst)}')

['1,Rumex acetosa,A,243,T', '9,Rumex acetosa,C,1224,T']

Rozbierzmy na podstawowe elementy wzorzec: .+Rumex acetosa.+\d{3,}.+T:

.+ -jeden lub więcej dowolnych znaków

Rumex acetosa - wiadomo

.+ - jeden lub więcej dowolnych znaków

\d{3,} - trzy lub więcej cyfr

.+- jeden lub więcej dowolnych znaków

15
T - litera ,,T''

Grupy przechwytywania i odwołania wsteczne


Kolejnym narzędziem, które często przydaje się przy pracy z wyrażeniami regularnymi, są grupy
przechwytywania i odwołania wsteczne (ang. backreferences). Pozwalają one np. zmieniać fragmenty
wyrażeń, czy ich kolejność. Jeśli obejmiemy parą nawiasów (``) fragment wyrażenia, to do dopasowania
będzie można się odwołać za pomocą odwołań wstecznych, które działają podobnie do zmiennych.
Oznaczamy je kolejno \1, \2 … \99.

Przeanalizujmy to na przykładach:

import re

tekst = 'Co nam dali Rzymianie?'

wzorzec = re.compile(r'\w+ (\w+) (\w+) (\w+)\?')

print(wzorzec.sub(r'\3 \1 \2:', tekst))

Rzymianie nam dali:

Wzorzec \w+ (\w+) (\w+) (\w+)\? możemy odczytać tak: słowo (jeden lub więcej znaków
alfanumerycznych), spacja, słowo, spacja, słowo, spacja, słowo, znak ?. Zauważ jednak, że wyrażenia
oznaczające drugie, trzecie i czwarte słowo są objęte nawiasami. Zostają one przyporządkowane
kolejno do odwołań \1, \2, \3. Kiedy użyjemy metody sub(), powyższe dopasowanie zamieniamy na
łańcuch składający się kolejno z wyrażeń przypisanych do odwołań \3, \1 i \2 (tak jakby to były nazwy
zmiennych) poprzedzielanych spacjami, po których umieszczamy dwukropek.

W drugim przykładzie użyjemy używanego wcześniej fragmentu hipotetycznego pliku .csv z danymi,
dla uproszczenia z usuniętą linią z nagłówkami kolumn. Pobierzmy z każdej linii nazwę organizmu oraz
wartość pomiaru i wydrukujmy je:

tekst = """

1,Rumex acetosa,A,243,T

2,Rumex acetosa,B,91,T

3,Zea mays,A,2432,T

4,Rumex thyrsiflorus,A,347,T

5,Rumex acetosa,C,3324,F

6,Rumex acetosella,A,347,T

7,Rumex thyrsiflorus,B,445,F

8,Oryza sativa,A,237,T

9,Rumex acetosa,C,1224,T

"""

wzorzec = re.compile(r'\d+,(\w+ \w+).+,(\d+).+')

16
print(wzorzec.sub(r'\1 - \2', tekst))

Rumex acetosa - 243

Rumex acetosa - 91

Zea mays - 2432

Rumex thyrsiflorus - 347

Rumex acetosa - 3324

Rumex acetosella - 347

Rumex thyrsiflorus - 445

Oryza sativa - 237

Rumex acetosa - 1224

Zastosowane wyrażenie \d+,(\w+ \w+).+,(\d+).+ można odczytać tak (w nawiasach części wyrażenia,
do których przyporządkowujemy odwołania wsteczne):

\d+ - jedna lub więcej cyfr

, - przecinek

(\w+ \w+) - dwa ciągi znaków alfanumerycznych (słowa) przedzielone spacją - przyporządkowane do
odwołania \1

.+ - jeden lub więcej dowolnych znaków

, - przecinek

(\d+) - jedna lub więcej cyfr - przyporządkowane do odwołania \2

.+ - jeden lub więcej dowolnych znaków

Zadania
Zadanie 1

Wg. standardu IUPAC, poza oznaczeniami literami G, A, C, T (w DNA), U (w RNA) zasad, w zapisie
sekwencji kwasów nukleinowych można także używać liter oznaczających możliwe występowanie w
danym miejscu więcej niż jednej zasady, np. Y oznacza T lub C (,,pYrimidine’’).

Napisz kod, który w podanej sekwencji (np. DAARYCGGGTANTTM) znajdzie wszystkie znaki poza tymi,
które oznaczają konkretne zasady (G, A, T, C) oraz poda miejsce w sekwencji ich występowania,
począwszy od 1.

Zadanie 2

Pobierz plik sequence.fasta. Zawiera on zestaw sekwencji genu atp6 pobranych z bazy GenBank.
Odczytaj plik i przepisz jego zawartość do innego pliku, o nazwie sekwencje-atp6.fasta, ale ze
zmianami linii opisującej sekwencje tak, aby:

pozostały w nich wyłącznie numery GenBank, ale z usuniętym numerem wersji,

dwuczłonowa nazwa rodzajowa i gatunkowa, bez nazw podgatunku etc.

17
wszystkie spacje zostały zmienione na podkreślniki (_)

Czyli np. linia:

>KU180469.1 Orobanche alba subsp. alba clone 50 ATPase subunit 6 (atp6) gene, partial cds;
mitochondrial

Po zmianach powinna wyglądać tak:

>KU180469_Orobanche_alba

Przykładowe rozwiązania

Zadanie 1

import re

tekst = 'DAARYCGGGTANTTM'

wzorzec = re.compile(r'[^AGCTU]')

wynik = wzorzec.finditer(tekst)

for w in wynik:

print(f'Znak: "{w.group()}", miejsce: {w.start()+1}')

Znak: "D", miejsce: 1

Znak: "R", miejsce: 4

Znak: "Y", miejsce: 5

Znak: "N", miejsce: 12

Znak: "M", miejsce: 15

Zadanie 2

import re

plik_odczyt = open('sequence.fasta', 'rt')

plik_zapis = open('sekwencje-atp6.fasta', 'wt')

for linia in plik_odczyt:

wzorzec = re.compile(r'(>\w+).\d (\w+) (\w+).+')

print(wzorzec.sub(r'\1_\2_\3', linia), end='', file=plik_zapis)

plik_odczyt.close()

plik_zapis.close()

18

You might also like