Python – regex

Wyrażenia regularne są uniwersalne, lecz każdy język programowania ma swoje własne charakterystyki. Jak to jest w Pythone? Poznajemy bibliotekę re.

Python regex

Python regex


Wyrażenia regularne (regex) z założenia są uniwersalne. Jednak każdy język programowania posiada swoje własne, charakterystyczne elementy, czy właściwości tych wzorców. W tym artykule przedstawimy jak to jest w języku Python. W celu poznania podstaw wyrażeń regularnych polecam artykuł:

Pythonowa biblioteka – re

Aby móc pracować z wyrażeniami regularnymi w Pythonie, polecam skorzystać z gotowej biblioteki re. Znajduje się ona w zbiorze standardowych bibliotek Pythona, także nie wymaga dodatkowej instalacji. Umożliwia ona kompilację, przetwarzanie, czy porównywanie wyrażeń regularnych z łańcuchami znaków w czytelny sposób. Jednak należy mieć na uwadze, że same wyrażenia, jak i biblioteka do ich obsługi posiada pewne ograniczenia, dlatego w niektórych przypadkach bardziej polecane jest wykorzystanie kodu w Pythonie.

Kompilacja wyrażenia regularnego

Na potrzeby tego akapitu posłużmy się prostym wyrażeniem regularnym, które służy do znajdywania liczb w systemie heksadecymalnym. Zatem musimy napisać wyrażenie, które będzie się zaczynało od 0x, po którym wystąpi jeden lub więcej znaków z zakresu małych i dużych liter z zakresu a-f oraz liczb 0-9:

hexPattern = "^0x[A-Fa-f0-9]+$"

W powyższym wzorze, istotne jest umieszczenie na początku i na końcu znaków ^ oraz $, gdyż interesują nas tylko i wyłącznie ciągi znaków reprezentujące w całości liczby szesnastkowe. W przypadku nieumieszczenia na przykład znaku oznaczającego koniec, czyli $, zostaną dopasowane niepoprawne elementy, takie jak 0x12AG. Dlaczego akurat taki ciąg mógłby zostać dopasowany? Wyrażenie ^0x[A-Fa-f0-9]+, oznacza liczbę zaczynającą się od frazy 0x, gdzie następnie występuje jeden lub więcej znaków znajdujących się w nawiasie. Wyrażenie nie musie się na tym kończyć, więc z racji, że 0x12A je spełnia, a nie ma znaku końca, możliwe są dalsze znaki — w naszym przypadku G.

Mając przygotowany nasz wzór, w pierwszym kroku musimy wykonać na nim kompilację. Do tego celu wybieramy z biblioteki re, metodę compile, która zwróci nam obiekt naszego wzorca, posiadający dodatkowointerfejs z potrzebnymi funkcjami do jego obsługi:

reHex = re.compile(hexPattern) 

W celu zweryfikowania działania naszego wyrażenia regularnego dokonamy przeszukania podanej listy pod kątem liczb w systemie szesnastkowym. Posłużymy się tutaj prostą pętlą przechodzącą po wszystkich elementach listy, sprawdzając przy tym, czy dany element pasuje do naszego wzorca. Metoda match, w przypadku dopasowania do wzorca zwraca nam jego obiekt, natomiast w przypadku braku dopasowania zwraca wartość None:

inputList = ['123', '0xabcd', '0xAG12', '12AF', '0x12AF', '0x1', '0xFF', 'unknown', '0x']
 
for item in inputList:
    if reHex.match(item) is not None:
        print(item)

Rezultatem działania powyższego kodu będzie:

qabrio@test:~$  python regex.py 
0xabcd
0x12AF
0x1
0xFF

Jak widzimy, odrzucone zostały wszystkie elementy listy niezawierające liczb heksadecymalnych. W powyższym przykładzie z powodzeniem możemy użyć metody search z tej samej biblioteki. Metoda ta w przypadku dopasowania zwraca nam obiekt takiego samego typu co metoda match. Różnica pomiędzy tymi dwoma metodami polega na tym, że match dopasowuje obiekt od pierwszego miejsca, natomiast search przeszukuje obiekt w poszukiwaniu wzorca. Jako że w naszym przykładzie użyliśmy znacznika początku wyrażenia ^, obie metody zachowałaby się tak samo.

Dopasowywanie do wzorca – findall i finditer

Ograniczeniem metod match i search jest fakt, że znajdują one tylko pierwsze dopasowanie do wzorca w ciągu znaków. Co jednak, jeśli chcemy znaleźć wszystkie dopasowania w danym ciągu. Z pomocą przychodzą nam metody findall oraz finditer. Pierwsza z nich zwraca nam listę z dopasowaniami, natomiast kolejna zwróci nam listę obiektów typu match. Sprawdźmy to na przykładzie przeszukiwania stringu pod kątem wszystkich wystąpień ciągów znaków rozpoczynający się i kończących na literę ‚a’:

pattern = "a[a-z]*a"
rePattern = re.compile(pattern)
 
randomStr = "aa abdhs asksdcdk asdasds asadssa sadsdfapa apdfsdjf asdsdf"
 
fIter = rePattern.finditer(randomStr)
fAll = rePattern.findall(randomStr)
 
print('Iterator:')
for item in fIter:
    print(item)
 
print('List:')
for item in fAll:
    print(item)

Wynikiem powyższego kodu będzie:

qabrio@test:~$  python regex.py 
Iterator:
<_sre.SRE_Match object; span=(0, 2), match='aa'>
<_sre.SRE_Match object; span=(18, 22), match='asda'>
<_sre.SRE_Match object; span=(26, 33), match='asadssa'>
<_sre.SRE_Match object; span=(35, 43), match='adsdfapa'>
List:
aa
asda
asadssa
adsdfapa

Operowanie na obiektach typu match

Wiemy już jak uzyskać obiekt typu match, jednak co dalej? Bibliotek re udostępnia nam kilka metod do operowania na tego typu obiektach. Pierwszą, często używaną funkcją jest group, która zwraca nam ciąg znaków dopasowania. Kolejnymi przydatnymi metodami są start oraz end, które zwracają nam odpowiednio położenie w ciągu znaków początku i końca dopasowania. Połączeniem obu powyższych metod jest span, który zwraca nam krotkę z początkiem i końcem dopasowania. Wszystkie cztery metody zostały przedstawione na poniższym przykładzie:

>>> matchObject
<_sre.SRE_Match object; span=(0, 6), match='0x12AF'>
>>> matchObject.group()
'0x12AF'
>>> matchObject.start()
0
>>> matchObject.end()
6
>>> matchObject.span()
(0, 6)

Dopasowania zachłanne i leniwe

W poprzednim artykule wspomniałem o dopasowaniach zachłannych oraz leniwych. Jednak jak to wygląda w praktyce? Stwórzmy po jednym wzorze dla oby przypadków, przeszukującym ciąg znaków pod kątem wystąpień wyrażeń zaczynających się na 1 i kończących na 3, a następnie przy ich wykorzystaniu przeszukajmy podanego stringa:

>>> stdPattern = re.compile('1[0-9]*3')
>>> lazyPattern = re.compile('1[0-9]*?3')
>>> inputStr = '1734238505473198563457147291842934'
>>> stdPattern.findall(inputStr)
['173423850547319856345714729184293']
>>> lazyPattern.findall(inputStr)
['173', '198563', '14729184293']

Jak widzimy na powyższym przykładzie, standardowe dopasowanie znalazło nam największy z możliwych ciągów znaków, które pasują do zapytania, natomiast leniwe zapytania znalazło nam wszystkie najmniejsze wystąpienia (części ciągu) pasujące do naszego wzorca. 

Jak widzimy, wyrażenia regularne mogą okazać się bardzo pomocnym narzędziem podczas pracy z ciągami znaków. Są one przy tym szybsze i niejednokrotnie czytelniejsze od standardowego kodu Python’owego. Należy jednak rozważyć, czy w każdym przypadku są one dla nas ułatwieniem. Kiedy jednak mamy do czynienia z bardzo skomplikowanymi wyrażeniami może okazać się, że optymalnym rozwiązaniem będzie wykorzystanie kodu w Pythonie, gdyż będzie to wymagało znacznie mniejszego nakładu pracy.

close

Newsletter