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()`` oraz ``strip()`` usuwają *białe znaki* [#]_ 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 .. raw:: html
- ``napis.startswith(prefix)`` zwraca prawdę, jeżeli napis zaczyna się podanym prefixem; analogicznie ``napis.endswith(suffix)`` zwraca prawdę, jeżeli napis kończy się podanym fragmentem; przydaje się to m.in do rozpoznawania typu pliku: .. raw:: html
- ``isupper()`` oraz ``islower()`` 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; analogicznie ``lower()`` zamienia wielkie litery na małe - ``napis.split(sep)`` dzieli napis na kawałki i zwraca listę napisów, ``sep`` to napis definiujący separatory - znaki oddzielające wyrazy; domyślnym separatorem są *białe znaki* .. raw:: html
- ``napis.find(fragment)`` sprawdza, czy napis zawiera podany fragment .. admonition:: Ćwiczenie 1 (a) Co się stanie, jeżeli w przykładzie ilustrującym metodę ``split()`` zabraknie ``","``? (b) Co się stanie, jeżeli w przykładzie ilustrującym metodę ``split()`` zabraknie ``float()``, 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. .. raw:: html
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: .. raw:: html
Wyrażenia regularne """""""""""""""""""""""""""" .. admonition:: W skrócie :class: def *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. .. raw:: html
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. .. admonition:: Ć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 ========================== .. rubric:: 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. .. rubric:: 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}"`` .. rubric:: 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 [#]_ ! 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"``. .. admonition:: Ć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ń. .. raw:: html
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"``! .. admonition:: Ć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. .. raw:: html
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: .. raw:: html
.. admonition:: W skrócie :class: def *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 .. raw:: html
.. admonition:: Ć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*: .. admonition:: Ć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_wyrazen: 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 wydajne - wyraż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ć. .. rubric:: Przypisy .. [#] Znaki *puste*: spacja, tabulator i znak nowej linii (*"enter"*) .. [#] 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 funkcji ``search()`` lub ``match()``