Professional Documents
Culture Documents
Regular Ne
Regular Ne
Regular Ne
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.
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)
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ć:
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():
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.')
if wynik:
else:
2
wyszukiwania w ciągu znaków. Przy okazji przypiszmy łańcuch znaków do zmiennej, co zwiększy
przejrzystość kodu.
# Kompilacja wzorca
wzorzec = re.compile(r'ni')
# Wyszukiwanie
wynik = wzorzec.search(tekst)
if wynik:
else:
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)
if len(wynik) > 0:
print(f'Dopasowano: {wynik}')
else:
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:
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:
for w in lista_wynikow:
else:
print('Brak wyników.')
Uzyskano 2 wyniki/ów.
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:
wynik = wzorzec.split(tekst)
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
print(zmieniony)
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:
5
print(re.findall(r'ni', tekst)) # Zwracana lista
['ni', 'ni']
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:
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.
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+')
wzorzec = re.compile(r'X*')
7
Łańcuch: ACGGGATTAGGGGGGACCCGGT
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.
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', '']
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'.+')
wzorzec = re.compile(r'.+?')
Łańcuch: ACGGGATTAGGGGGGACCCGGT
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']
print(f'Łańcuch: {tekst}')
wzorzec = re.compile(r'G+')
8
print(f'Dopasowanie zachłanne: {wzorzec.findall(tekst)}')
wzorzec = re.compile(r'G+?')
Łańcuch: ACGGGATTAGGGGGGACCCGGT
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', '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ń.
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']
print(f'Łańcuch: {tekst}')
wzorzec = re.compile(r'G+(?!A)')
print(f'{wzorzec.findall(tekst)}')
Łańcuch: ACGGGATTAGGGGGGACCCGGT
print(f'Łańcuch: {tekst}')
wzorzec = re.compile(r'(?<=C)G+')
print(f'{wzorzec.findall(tekst)}')
Łańcuch: ACGGGATTAGGGGGGACCCGGT
['GGG', 'GG']
print(f'Łańcuch: {tekst}')
wzorzec = re.compile(r'(?<!C)G+')
print(f'{wzorzec.findall(tekst)}')
Łańcuch: ACGGGATTAGGGGGGACCCGGT
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.
import re
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
wzorzec = re.compile(r'Jesteśmy|którzy|Ni!')
print(f'{wzorzec.findall(tekst)}')
11
['Jesteśmy', 'którzy', 'Ni!']
import re
tekst = "AAAGCCTGCTTAGCC"
wzorzec = re.compile(r'[AT]GC')
print(f'{wzorzec.findall(tekst)}')
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']
Wzorzec Znaczenie
\s biały znak
\S nie-biały znak
\d cyfra
\D nie-cyfra
[0-9] cyfry
12
Wypróbujmy je na kilku przykładach:
import re
print(tekst)
print(f'{opis}: {wzorzec}')
wz = re.compile(wzorzec)
print(f'\t{wz.findall(tekst)}')
['12345', '1234']
13
Jeden lub więcej cyfr: [0-9]+
['12345', '1234']
['aggt', 'acctga']
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ą
print(f'{wzorzec.findall(tekst)}')
[]
Flaga re.DOTALL pozwala na dopasowanie przez znak kropki . wszystkich znaków, także znaku nowej
linii:
print(tekst)
# Bez flagi
wzorzec = re.compile(r'rycerzami,.którzy')
print(f'{wzorzec.findall(tekst)}')
# z flagą
14
print(f'{wzorzec.findall(tekst)}')
Jesteśmy rycerzami,
[]
['rycerzami,\nktórzy']
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
"""
print(f'{wzorzec.findall(tekst)}')
15
T - litera ,,T''
Przeanalizujmy to na przykładach:
import re
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
"""
16
print(wzorzec.sub(r'\1 - \2', tekst))
Rumex acetosa - 91
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):
, - przecinek
(\w+ \w+) - dwa ciągi znaków alfanumerycznych (słowa) przedzielone spacją - przyporządkowane do
odwołania \1
, - przecinek
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:
17
wszystkie spacje zostały zmienione na podkreślniki (_)
>KU180469.1 Orobanche alba subsp. alba clone 50 ATPase subunit 6 (atp6) gene, partial cds;
mitochondrial
>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:
Zadanie 2
import re
plik_odczyt.close()
plik_zapis.close()
18