Przetwarzanie napisów i wyrażenia regularne¶
Język Python jest bardzo często wykorzystywany do przetwarzania tekstu. Nic więc dziwnego, że udostępniono w nim szeroki wachlarz funkcjonalności służących do tego celu.
Napis jako obiekt¶
Zmienna napisowa (string) jest obiektem. Oznacza to że można na niej wykonywać operacje, wywołując jej metody, stosując składnię z którą omawiana już była podczas wykładu o strukturach danych.
Note
Napis jest immutable, podobnie jak krotka; wszystkie metody klasy string zwracają zmodyfikowaną kopię napisu a oryginał pozostaje niezmieniony
Poniżej zebrałem najważniejsze metody klasy string:
lstrip(),rstrip()orazstrip()usuwają białe znaki 1 z końców napisu, odpowiednio z lewego, prawego i z obydwu; metody te mogą przyjąć jako argument napis składający się ze znaków które mają być usunięte; poniższy przykład pokazuje wykorzystanie tych metod, znaki>oraz<dodano aby pokazać, które spacje zostały usunięte
napis.startswith(prefix)zwraca prawdę, jeżeli napis zaczyna się podanym prefixem; analogicznienapis.endswith(suffix)zwraca prawdę, jeżeli napis kończy się podanym fragmentem; przydaje się to m.in do rozpoznawania typu pliku:
isupper()orazislower()zwracają prawdę, jeżeli wszystkie litery napisu są odpowiednio wielkie lub małe
upper()zamienia wszystkie małe litery napisu na wielkie, wielkie litery pozostają niezmienione; analogicznielower()zamienia wielkie litery na małe
napis.split(sep)dzieli napis na kawałki i zwraca listę napisów,septo napis definiujący separatory - znaki oddzielające wyrazy; domyślnym separatorem są białe znaki
napis.find(fragment)sprawdza, czy napis zawiera podany fragment
Ćwiczenie 1
Co się stanie, jeżeli w przykładzie ilustrującym metodę
split()zabraknie","?Co się stanie, jeżeli w przykładzie ilustrującym metodę
split()zabrakniefloat(), czyli sumowanie w linii 10 będzie wyglądać następująco:suma += l?
Pełen spis metod klasy string znajduje się na tej stronie.
Wyszukiwanie podnapisów¶
Aby wyszukać zadany ciąg znaków w napisie, korzystamy z metody find(). Należy tu znaczyć, że metoda ta zwraca
index pod którym znalazł się podciąg, np: "Ala ma kota".find("kot") zwróci wartość 7. Poszukiwania
podciągu "Ala" dadzą wynik 0. Jeżeli poszukiwania zakończą się porażką, to metoda ta zwraca -1.
Dlatego też wykorzystając w tym kontekście instrukcję warunkową if należy sprawdzić, czy find() zwróciła wartość nieujemną.
Przeanalizuj poniższy program, zastanów się gdzie jest błąd.
Wyszukiwanie podnapisu metodą find() znajduje pierwsze wystąpienie żądanego fragmentu. A jak możemy znaleźć wszystkie?
Okazuje się, że metoda ta ma dodatkowy argument start, którego domyślną wartością jest 0. Argument ten określa początek poszukiwań.
Umożliwia to wyszukiwanie metodą find() w pętli while aż do końca napisu:
Wyrażenia regularne¶
W skrócie
Wyrażenie regularne to wzorzec, definiujący ciąg znaków (tekst).
Metody klasy string pozwalają rozwiązać wiele codziennych zadań, jednak w bardziej skomplikowanych przypadkach mogą się okazać niewystarczające. Receptą na te problemy są wyrażenia regularne, choć jest to podejście trudniejsze niż wykorzystanie metod klasy string. Cóż zatem można osiągnąć, stosując wyrażenia regularne?
Wyrażenie regularne to wzorzec, którymi można opisać ciąg znaków. A dokładniej: dowolny ciąg znaków może pasować do zadanego wyrażenia regularnego bądź nie - operacja
match()można też sprawdzić, czy dany napis zawiera w sobie fragment zdefiniowany jako wyrażenia regularne - operacja
search()użyć wyrażenia regularnego do podzielenia napisu na części - operacja
split()
Niestety istnieje wiele notacji, które służą zapisywaniu wyrażeń regularnych; w zasadzie każdy język programowania ma własny dialekt. Notacja Pythona to tylko jedna z możliwych. Aby skorzystać z wyrażeń regularnych, należy zaimportować moduł re. Zacznijmy od przykładu, który opisuje kod pocztowy. Jak wiemy, w Polsce kody pocztowe są pięciocyfrowe: dwie cyfry - kreska - trzy cyfry.
W przypadku poprawnego kodu pocztowego funkcja match() zwraca jakiś obiekt (<_sre.SRE_Match object>); w przeciwnym przypadku nic nie zwraca … a dokładniej zwraca None. Ponieważ None jest interpretowane przez Pythona jako wartość logiczna fałsz, wynik funkcji match() można wykorzystać w instrukcji warunkowej.
Ćwiczenie 2
Zmodyfikuj powyższy program tak, aby wypisywał w konsoli Poprawny kod pocztowy jeżeli faktycznie jest on poprawny oraz Kod błędny w przeciwnym przypadku
Język wyrażeń regularnych¶
Klasy znaków
W powyższym wzorcu \d zastępuje dokładnie jedną, dowolną cyfrę. Znak - wymieniony jest wprost, pomiędzy grupą dwóch a grupą trzech cyfr. Generalnie znak \ oznacza, że zamiast określonego znaku (np cyfry 0) ma nastąpić dokładnie jeden znak z pewnej klasy (grupy znaków). Oto kilka grup znaków, zdefiniowanych w Pythonowym module re:
\d- dowolna cyfra
\D- wszystko inne niż\d(dopełnienie zbioru znaków)
\w- dowolna litera lub cyfra
\W- wszystko inne niż\w
\s- dowolny biały znak
\S- dowolny nie-biały znak
.- (kropka) zastępuje dowolny znak
Dodatkowo można samemu definiować własne klasy znaków, wpisując listę dozwolonych możliwości w nawiasy kwadratowe. Następujące wyrażenie regularne pasuje do kodów pocztowych zaczynających się na 0 lub na 1: "[01]\d-\d\d\d". Zauważ, że [01] a nawet [01234] dopasowuje dokładnie jeden znak.
Dopasowywanie wielu znaków jednocześnie
Podobnie możemy stworzyć wzorzec pasujący do numeru telefonu: "\d\d\d\d\d\d\d\d\d" (dla uproszczenia nie pozwólmy na razie na spacje i kreski w numerze). Wpisywanie po wielokroć tego samego wzorca wydaje się mało sensowne … i faktycznie można to skrócić do "\d{9}". Można też podać zakres liczby powtórzeń: "\d{7,9}"
Przeszukiwanie leniwe i zachłanne
Znak * (gwiazdka) oznacza “tak wiele razy, jak tylko się da”. Uwaga - znak * pasuje nawet 0 razy! Nieco inaczej działa znak +, który oznacza “ile się tylko da, ale co najmniej raz”. Zatem wzorce "\d*" oraz "\d+" pasują do dowolnie długiego nieprzerwanego ciągu cyfr. Numery telefonów mogłyby być nawet i dwudziestocyfrowe, o ile nie zapisano by ich ze spacjami. Przeszukiwanie takie nazywamy zachłannym. Jest ono szczególnie goźne w połączeniu z kropką: wzorzec .* znajdzie po prostu całą linijkę tekstu 2 !
Dodanie znaku ? zmienia przeszukiwanie z zachłannego na leniwe - dopasowuje najmniej ile potrzeba, aby wzorzec był spełniony. Uwaga! pojedynczy znak ? dopasowuje zero lub jeden raz, również zachłannie.
Zauważ, że zarówno wzorce "\d{2}-\d{3}", "\d{2}-\d*" "\d*-\d+" oraz "\d{3}-\d+?" pasują do tekstu "02-093", ale tylko "\d{2}-\d{3}" poprawnie rozpoznaje kody pocztowe. Pozostałe wzorce pasują również do np. "02-123456789". Żaden jednak nie pasuje do "02-09a".
Ćwiczenie 2
Wpisz do przykładu z kodem pocztowym podane powyżej wyrażenia regularne i sprawdź, na jakich napisach działają
Dopasowanie vs. wyszukiwanie¶
Powyżej sprawdzaliśmy jedynie, czy podany napis pasuje do wzorca jako całość. Za pomocą wyrażeń regularnych można również wyszukikać fragmentów tekstu. Wynikiem operacji search() jest obiekt Match, który zawiera informację o wynikach poszukuwań.
Instrukcja wynik.group(0) zwraca wynik wyszukuwania, który jest jednocześnie zerową grupą wyszukiwaną. Grup takich może być więcej, o czym poniżej. Nie należy mylić grupy wyszukiwania z wielokrotnymi wynikami! W powyższym przykładzie faktycznie są dwa kody pocztowe, a znaleźliśmy tylko jeden (pierwszy z nich). Jak dostać kolejne? Służy do tego funkcja findall() , która zwraca listę napisów (obiektów klasy string), które są wynikami poszukiwań, jak w linii 7 i 8 powyższego przykładu. Zauważ, że wzorzec kodu pocztowego nie sprawdza, czy wynik jest oddzielnym wyrazem! Wyniki wyszukiwania będą takie same, jeżeli adres zmienimy na "00-927Warszawa" a nawet na "00-92712345awa"!
Ćwiczenie 3
Zmodyfikuj wyrażenie regularne w powyższym przykładzie tak, aby wyszukiwało kody pocztowe jako oddzielne wyrazy
Grupy zdefiniowane¶
Rozwiązanie ćwiczenia 3 polega na stworzeniu wzorca, który będzie wyszukiwał też spacji, które okalają kod pocztowy. Poprawnym wynikiem wyszukiwania będzie wtedy napis " 00-927 " ale nie "00-927". Definiowanie grup umożliwia wyłuskiwanie fragmentów tekstu, które są krótsze niż poszukiwany wzorzec. W jednym wzorcu możemy zdefiniować więcej niż jedną grupę.
Przeanalizujmy teraz całkiem realny przykład. Poniżej podano rzeczywiste nagłówki sekwencji białkowych, tak jak je zdeponowano w bazie danych. Każda sekwencja ma nazwę oraz identyfikator a także nazwę gatunku, z którego została wyizolowana.
W powyższym programie wyszukujemy nazw gatunków zauważywszy, że to jedyny napis, który pojawia się w nawiasach kwadratowych. Ponieważ nawiasy kwadratowe są już wykorzystane w wyrażeniach regularnych do definiowania grup, to kiedy na prawdę poszukujemy znaku ']', musimy poprzedzić go znakiem \. Powyższy program nie tworzy jednak grup; ta pojawia się dopiero w poniższym kodzie:
W skrócie
Grupy definiujemy, zawierając fragment wzorca w nawiasach okrągłych.
Po zdefiniowaniu jednej grupy możemy już napisać match.group(1). A co zrobić, jeżeli potrzebujemy wyjąć też pozostałe informacje z tekstu? Metodą prób i błędów tworzymy wzorzec, który odpowiada całej linijce:
wzorzec = ">\s?(.+) \[(.+)\] Sequence ID: (\w+)"
Przeanalizujmy ten wzorzec krok po kroku:
najpierw musi być znak
>po nim zero lub jedna spacja (spacja a dokładniej biały znak bo
\s, zero lub jedna bo?)następnie ma być dokładnie jedna spacja, gdyż
' 'potem szukamy gatunku: ma być znak
[, jakiś niepusty tekst (bo.+) a na końcu]; przeszukujemy zachłannie - złapie się tak dużo znaków ile tylko możliwe, byle by tylko po nich nastapił]następnie musi pojawić się napis
Sequence ID:; jakakolwiek literówka w tym miejscu spowoduje, że wzorzec nie będzie działałna koniec wyławiamy identyfikator sekwencji
Ćwiczenie 4
Powyższy wzorzec ma kilka słabych punktów. Jednym z nich jest to, że po znaku ] kończącym nazwę gatunku musi nastąpić dokładnie jedna spacja a potem napis Sequence ID:. Zmodyfikuj wzorzec tak, aby przed i po napisie Sequence ID: mogła być jedna lub więcej spacji.
Na powyższym przykładzie można też pokazać, jakie są negatywne skutki wzorców zachłannych:
Ćwiczenie 5
Zmodyfikuj jeden z napisów (np s1) tak, aby były w nim dwie nazwy gatunków, jedna po drugiej, np tak: " [Streptomyces megasporus][Streptomyces clavuligerus] ". Wytłumacz wynik programu
Kompilowanie wyrażeń¶
Algorytmy umożliwiające działanie wyrażeń regularnych są dość skomplikowane. W praktyce tekst wyrażenia musi zostać skompilowany do bardziej użytecznej dla Pythona postaci. Odbywa się to we wszystkich powyższych przykładach, choć użytkownik tego nie widzi. Można jednak samemu skompilować wyrażenie. Służy do tego funkcja compile Przydaje się to, gdy ma być ono wykorzystane więcej niż raz. Skompilowane wyrażenie jest obiektem, a search() i match() to jego metody.
Podsumowanie¶
Wyrażenie regularne to bardzo potężne narzędzie, nie jest jednak pozbawione wad:
wyrażenia regularne są wolniejsze niż przetwarzanie tekstu metodami klasy
string; wykorzystywanie wyrażeń do wielkich plików może być mało wydajnewyrażenia regularne mogą przestać działać “same z siebie”, jeżeli napis przetwarzany nie pasuje do wzorca; często zdarza się, że dane wejściowe z zeszłego eksperymentu / tygodnia / innego przyrządu mają nieco inny format i “stare” wyrażenia już nie pasują
konstruowanie i testowanie wyrażeń może okazać się dość skomplikowane i czasochłonne, zwłaszcza kiedy mają one przetwarzać całą linię tekstu. W praktyce wyrażenie należy budować krok po kroku, dokładając kolejne elementy (jak np. w powyższym spisie) i sprawdzając na każdym etapie, czy działa prawidłowo. Warto korzystać z narzędzi do testowania wyrażeń regularnych, takich jak regex101 albo pythex. Należy też zgromadzić możliwie różnorodne przykłady tekstu, który nasze wyrażenie ma przetwarzać.
Przypisy
- 1
Znaki puste: spacja, tabulator i znak nowej linii (“enter”)
- 2
Domyślnie wyrażenia regularne operują na pojedynczych liniach tekstu. Oznacza to, że przeszukiwanie / dopasowywanie wzorca kończy się po napotkaniu znaku nowej linii (
\n). Można to zmienić, przekazując odpowiednią flagę jako dodatkowy argument funkcjisearch()lubmatch()