Автоматизирай Това Начало на книгата

Регулярни изрази

Повечето хора са запознати с търсенето на текст с CTRL-F и въвеждането на търсените думи. Регулярните изрази работят на по-високо ниво - те позволяват да се търси по текстов шаблон. Например в България мобилните телефонни номера започват с четири цифри, последвани от интервал и още шест цифри, разделени на две групи по три или три групи по две. Така хората разпознават телефонните номера, когато го видят. 0888 12 34 56 е валиден номер, а 0,888,123,456 - не е.

Всеки ден се разпознават много други шаблони. Имейл адреса съдържа символ @ няйъде в средата. ЕГН-тата са с десет цифри. Уебсайт адресите често имат точки и наклонени черти. Таговете в социалните мрежи започват с # и са без интервали. И така нататък.

Малко хора извън кръга на програмистите са запознати с регулярните изрази, макар те да са много полезни. Дори повечето модерни текстови редактори, като Microsoft Word или OpenOffice, позволяват търсене с използване на регулярни изрази. Регулярните изрази спестяват много време, не само на потребителите на софтуера, но и на програмистите. Всъщност писателят Кори Доктороу смята, че регулярните изрази трябва да се учат преди програмирането:

Познаването на [регулярни изрази] може да е разликата между решаване на проблем в 3 и 3000 стъпки. Когато си зубър лесно се забравя, че проблеми, решими с няколко натискания на клавиши, отнемат на други хора дни на досадна и пълна с грешки работа.

Тази глава започва с писане на програма, разпознаваща текстови шаблони без да се използват регулярни изрази. След това се използват регулярни изрази, които правят кода много по-чист. Ще се демонстрира просто използване на регулярни изрази, след това се преминава към по-мощните свойства, като заменяне на низове и създаване на собствени класове символи. Накрая на главата ще се напише програма, която автоматично извлича телефонни номера и имейл адреси от текст.

Намиране на шаблони в текст без регулярни изрази

Нека се търси мобилен телефонен номер в даден низ. Шаблонът ще е - четири цифри, интервал, три цифри, интервал, три цифри. Например 0888 123 456.

Следва функция с име isPhoneNumber(), която проверява дали даден низ съвпада с този шаблон. Ето и кода, записн като isPhoneNumber.py:

def isPhoneNumber(text):
    ➊ if len(text) != 12:
        return False
    for i in range(0, 4):
        ➋ if not text[i].isdecimal():
            return False
    ➌ if text[4] != ' ':
        return False
    for i in range(5, 8):
        ➍ if not text[i].isdecimal():
            return False
    ➎ if text[8] != ' ':
        return False
    for i in range(9, 12):
        ➏ if not text[i].isdecimal():
            return False

    ➐ return True

print('Дали 0888 123 456 е мобилен номер?')
print(isPhoneNumber('0888 123 456'))
print('Дали Иван Петкан е мобилен номер?')
print(isPhoneNumber('Иван Петкан'))

При изпълнение на програмата се получава следния резултат:

Дали 0888 123 456 е мобилен номер?
True
Дали Иван Петкан е мобилен номер?
False

Функцията isPhoneNumber() съдържа няколко проверки, определящи дали низа в text е валиден мобилен номер. Функцията връща False ако някоя проверка не мине. Първо кодът проверява дали низа и дълъг точно 12 символа ➊. След това проверява дали кодът на оператора (първите четири символа в text) се състои само от цифри ➋. Останалата част от функцията проверява дали низа следва шаблона за мобилен номер - след кода на оператора да има интервал ➌, още три цифри ➍, друг интервал ➎, и завършва с още три цифри ➏. Програмата връща True ➐, ако са изпълнени всички проверки.

Извикването на isPhoneNumber() с аргумент '0888 123 456' ще върне True. Извикването на isPhoneNumber() с 'Иван Петкан' връща False, още първата проверка не минава, защото 'Иван Петкан' не е 12 символа.

За намирането на телефонен номер в по-голям низ ще трябва да се пише още код. В програмата isPhoneNumber.py могат да се заменят последните четири извиквания на print() със следния код:

message = 'Обади ми се утре на 0888 123 456. 0787 987 654 ми е служебният.'
for i in range(len(message)):
    ➊ chunk = message[i:i+12]
    ➋ if isPhoneNumber(chunk):
        print('Намерен мобилен номер: ' + chunk)
print('Търсенето завърши')

При изпълнение на програмата се получава следния резултат:

Намерен мобилен номер: 0888 123 456
Намерен мобилен номер: 0787 987 654
Търсенето завърши

При всяко поредно изпълнение на тялото на цикъла for в променливата chunk има ново парче от 12 символа ➊. При първото изпълнение iе 0 и на chunk се присвоява message[0:12], тоест низа 'Обади ми се '. При следващото изпълнение i е 1 и на chunk се присвоява message[1:13]. низа 'бади ми се у'. С други думи, при всяко завъртане на цикъла, chunk приема следната стойност:

  • 'Обади ми се '
  • 'бади ми се у'
  • 'ади ми се ут'
  • 'ди ми се утр'
  • 'и ми се утре'
  • и така нататък

Променливата chunk се подава на функцията isPhoneNumber() за проверка дали съвпада с шаблона за телефонен номер ➋.

Цикълът преминава през целия низ, пробвайки всички 12-символни парчета, принтирайки всяко парче, което отговаря на проверките в isPhoneNumber(). След завършването на цикъла се принтира съобщение Търсенето завърши.

В този пример низът в message е кратък, но той може да бъде с дължина милиони символи. Програмата би се изпълнила за по-малко от секунда. Подобна програма за намиране на телефонни номера с регулярни изрази също ще се изпълни за по-малко от секунда, но ще е много по-бърза за написване.

Намиране на шаблони в текст с регулярни изрази

Предната програма за намиране на телефонни номера работи, но използва много код за ограничен брой случаи - функцията isPhoneNumber() е 17 реда, а може да намери само един тип телефонни номера. А ако номерът е записан като 0888 12 34 56 или (359) 888 123 456 ? Или разделен по някой друг начин? Функцията isPhoneNumber() не може да провери подобни номера. Тези типове номера могат да ес намират с писане на още код, но има и по-лесен начин.

Регулярните изрази (regular expressions, regex) описват шаблон за текст. Например \d в израз означава цифров символ, която и да е цифра от 0 до 9. Регулярният израз \d\d\d\d \d\d\d \d\d\d описва точно същия шаблон като функцията isPhoneNumber() - четири цифри, интервал, още три цифри, нов интервал и три цифри. Никакъв друг низ няма да съвпадне с регулярния израз \d\d\d\d \d\d\d \d\d\d.

Регулярните изрази могат да бъдат много по-изтънчени. Например добавянето на 3 в големи скоби ({3}) след шаблон означава "Този шаблон трябва да съвпадне три пъти". Така може да се напише по-кратък регулярен израз \d{4} \d{3} \d{3}, kойто също да съвпада с валиден телефонен номер.

Създаване на обект от тип регулярен израз

Всички Python функции за работа с регулярни изрази са в модула re. Използването на модула в интерактивната конзола става по следния начин:

>>> import re

БЕЛЕЖКА

Повечето примери в тази глава се нуждаят от модула re, така че той трябва да се импортира в началото на всеки скрипт. Иначе ще се получи грешка NameError: name 're' is not defined.

За създаването на обект от тип Regex (регулярен израз) се подава желаният низ на функцията re.compile(). Тази функция връща обект шаблон Regex.

Пример от интерактивната конзола за създаване на шаблон за телефонен номер (\d означава "цифров символ", digit)

>>> phoneNumberRegex = re.compile(r'\d\d\d\d \d\d\d \d\d\d')

Променливата phoneNumberRegex съдържа обект от тип Regex.

Съвпадане на регулярни изрази

Обектите от тип Regex имат метод search(), който търси съвпадения в подадения му низ. Методът search() ще върне None, ако регулярният израз не може да бъден намерен в низа. При намиране на съвпадение с шаблона, методът search() връща обект от тип Match. Този обект има метод group(), който връща съвпадналия текст от претърсвания низ. (Групите са описани след малко). Пример от интерактивната конзола:

>>> phoneNumberRegex = re.compile(r'\d\d\d\d \d\d\d \d\d\d')
>>> mo = phoneNumberRegex.search('Номерът ми е 0897 999 888')
>>> print('Намерен телефонен номер: ' + mo.group())
Намерен телефонен номер: 0897 999 888

Името на променливата mo е примерно име за използване с Match обекти. Примерът може да изглежда сложен на пръв поглед, но е по-кратък от програмата isPhoneNumber, а върши същата работа.

Тук на re.compile се подава желания шаблон. В phoneNumberRegex се записва резултатът, който е обект от тип Regex. След това на phoneNumberRegex се извиква search(), като се подава низа, в който да се търси. Резултатът от търсенето се записва в променливата mo. В този пример се знае, че шаблонът ще бъде намерен в низа, така че ще се върне обект от тип Match. На този обект може да се извика метода group(), който ще върне съвпадението. Резултатът от извикването mo.group() се подава на print(), което показва на екрана цялото съвпадение - 0897 999 888

Преглед на използването на регулярни изрази

Използването на регулярни изрази в Python става с няколко стъпки, но всяка от тях е сравнително проста.

  1. Импортиране на модула за регулярни изрази с import re.
  2. Създаване на Regex обект с функцията re.compile(). Трябва да се използва буквален низ.
  3. Низът, в който се търси, се подава в метода search() на обекта от тип Regex. Този метод връща обект от тип Match.
  4. На Match обекта се извиква метода group(), който връща низ със съвпадащия текст.

БЕЛЕЖКА

Добре е примерните кодове да се изпробват в интерактивната конзола. За изпробване на регулярни изрази са полезни и онлайн тестерите, които показват точно как изразът съвпада с част от въведения текст. Добър онлайн тестер е https://pythex.org/

Използване на регулярни изрази за напреднали

След описване на основните стъпки за създаване и намиране на регулярни изрази е време за навлизане в по-мощните възможности на шаблоните.

Групиране със скоби

Нека е необходимо разделяне на кода на оператора от останалата част от телефонния номер. Добавянето на скоби ще създаде групи в регулярния израз - (\d\d\d\d) (\d\d\d \d\d\d). След това с метода group() може да се вземе съвпадащия текст за само една група.

Първите скоби в регулярния израз определят група 1. Вторите скоби определят група 2. На метода group() може да се подаде цяло число 1 или 2, за да се вземе само тази част от съвпадащия текст. Подаване на 0 или нищо на метода group() ще върне целия съвпадащ текст. Пример от интерактивната конзола:

>>> phoneNumberRegex = re.compile(r'(\d\d\d\d) (\d\d\d \d\d\d)')
≫>> mo = phoneNumberRegex.search('Номерът ми е 0897 999 888')
>>> mo.group(1)
'0897'
>>> mo.group(2)
'999 888'
≫>> mo.group(0)
'0897 999 888'
>>> mo.group()
'0897 999 888'

За получаване на всички групи едновременно се използва методът groups() (да се прави разлика с метода group() - едното име е множествено число)

>>> mo.groups()
('0897', '999 888')
>>> code, number = mo.groups()
>>> print(code)
0897
>>> print(number)
999 888

mo.groups() връща комплект от няколко стойности, така че може да се използва множествено присвояване. Така всяка група ще се запише в отделна променлива, както в примерния ред code, number = mo.groups()

Скобите имат специално значение в регулярните изрази, но как тогава се търсят скоби в текста? Например, може телефонният номер да започва с код в скоби. В такъв случай трябва символите ( и ) да се екранират с обратна наклонена черта. Пример от интерактивната конзола:

>>> phoneNumberRegex = re.compile(r'(\(\d\d\d\d\)) (\d\d\d \d\d\d)')
>>> mo = phoneNumberRegex.search('Номерът ми е (0897) 999 888')
>>> mo.group(1)
'(0897)'
>>> mo.group(2)
'999 888'

Екраниращите символи \( и \) в буквалния низ, подаден на re.compile(), ще съвпаднат със символите за скоби. Следните символи имат специално значение в регулярните изрази:

. ^ $ * + ? { } [ ] \ | ( )

Тези символи трябва да се екранират, за да бъдат намерени като част от текстов шаблон.

\. \^ \$ \* \+ \? \{ \} \[ \] \\ \| \( \)

Трябва да внимава с използването на екраниращи символи. Например да не се бъркат \( и \) с ( и ) в регулярни изрази. При забравяне на затварящата скоба на група се получават съобщения за грешка подобни на "missing )" или "unbalanced parenthesis", Пример :

>>> re.compile(r'(\(Скоби\)')
Traceback (most recent call last):
    --изрязани редове--
re.error: missing ), unterminated subpattern at position 0

Съобщението за грешка подсказва, че има отваряща скоба на позиция 0 в низа r'(\(Скоби\)', чиято затварящи скоба липсва.

Съвпадане на няколко групи с избор

Символът вертикална черта | е оператор за избор (pipe). Може да се използва навсякъде, където трябва съвпадение с един от много изрази. Например регулярният израз r'Батман|Тина Фей' ще съвпадне с едното от 'Батман' или 'Тина Фей'.

Ако в претърсвания низ се съдържат и двата израза (Батман и Тина Фей), ще се върне първия резултат. Пример от интерактивната конзола:

>>> heroRegex = re.compile(r'Батман|Тина Фей')
>>> mo1 = heroRegex.search('Батман и Тина Фей')
>>> mo1.group()
'Батман'

>>> mo2 = heroRegex.search('Тина Фей и Батман')
>>> mo2.group()
'Тина Фей'

БЕЛЕЖКА

Всички срещания могат да се получат с използване на метода findall(), който е описан в секцията "Методът findall()".

Операторът за избор може да се използва и за съвпадане на един от няколко шаблони като част от регулярен израз. Например, нека да трябва да се намери съвпадение с един от низовете 'триъгълник', 'четириъгълник', 'правоъгълник' или 'петоъгълник'. Всичките низове завършват с 'ъгълник', така че ще е добре този суфикс да се зададе само веднъж. Това може да се постигне със скоби. Пример от интерактивната конзола:

>>> angleRegex = re.compile(r'(три|четири|право|пето)ъгълник')
>>> mo = angleRegex.search('Квадратът е четириъгълник')
>>> mo.group()
'четириъгълник'
>>> mo.group(1)
'четири'

Извикването на метода mo.group() връща целия съвпадащ текст 'четириъгълник', докато mo.group(1) връща само частта от съвпадащия текст, която е в първата група от скоби - 'четири'. С използването на оператора за избор и групиращи скоби могат да се определят няколко алтернативни шаблона, които да съвпадат в регулярен израз.

За съвпаданен на самия символ | трябва да се екранира с обратна наклонена черта \|.

Незадължително съвпадане с въпросителен знак

Понякога има част от шаблон, която евентуално да съвпада. Тоест, регулярният израз да намира съвпадение, без значение дали има определена част от текста или не. Символът  ? обозначава групата, след която е, като незадължителна част от шаблона. Пример от интерактивната конзола:

>>> ivoRegex = re.compile('Ив(айл)?о')
>>> mo1 = ivoRegex.search('Приключенията на Иво')
>>> mo1.group()
'Иво'
>>> mo2 = ivoRegex.search('Приключенията на Ивайло')
>>> mo2.group()
'Ивайло'

Частта (айл)? в регулярния израз означава, че шаблона айл е незадължителна група. Регулярният израз ще съвпадне с текст, в който айл се среща нула или един път. По тази причина регулярният израз съвпада и с 'Иво', и с 'Ивайло'.

Използвайки примера с телефонен номер, регулярният израз може да търси телефонни номера със или без интервали в тях. Пример от интерактивната конзола:

>>> phoneRegex = re.compile(r'\d\d\d\d( )?\d\d\d( )?\d\d\d')
>>> mo1 = phoneRegex.search('Номерът ми е 0897 999 888')
>>> mo1.group()
'0897 999 888'

>>> mo2 = phoneRegex.search('Номерът ми е 0897 999888')
>>> mo2.group()
'0897 999888'

Може да се приеме, че символът ? в регулярен израз означава "Групата преди този знак трябва да съвпада нула или един път".

За съвпадане със символа за въпросителен знак трябва да се използва екраниране - \?.

Нула или повече съвпадения със звезда

Символът * (наричан звезда, star, asterisk) означава "нула или повече съвпадения". Групата, последвана от звезда, може да се съдържа произволен брой пъти в текста. Може да липсва или да се повтаря многократно. Малка промяна на последния пример:

>>> ivoRegex = re.compile('Ив(айл)*о')
>>> mo1 = ivoRegex.search('Приключенията на Иво')
>>> mo1.group()
'Иво'
>>> mo2 = ivoRegex.search('Приключенията на Ивайло')
>>> mo2.group()
'Ивайло'
>>> mo3 = ivoRegex.search('Приключенията на Ивайлайлайло')
>>> mo3.group()
'Ивайлайлайло'

В 'Иво' частта (айл)* съвпада нула пъти в низа. В 'Ивайло' (айл)* го има веднъж, а в 'Ивайлайлайло' го има три пъти.

За съвпадане със символа звезда трябва да се използва екраниране - \*.

Едно или повече съвпадения с плюс

Символът * означава "нула или повече съвпадения", а + (плюс, plus) означава "едно или повече съвпадения". За разлика от звездата, която не изисква групата да се намира в текста, групата, последвана от плюс, трябва да я има поне веднъж. Това е задължително. Пример от интерактивната конзола, който може да се сравни с примера от предишната секция:

>>> ivoRegex = re.compile('Ив(айл)+о')
>>> mo1 = ivoRegex.search('Приключенията на Ивайло')
>>> mo1.group()
'Ивайло'
>>> mo2 = ivoRegex.search('Приключенията на Ивайлайлайло')
>>> mo2.group()
'Ивайлайлайло'
>>> mo3 = ivoRegex.search('Приключенията на Иво')
>>> mo3 == None
True

Регулярният израз Ив(айл)+о няма да има съвпадение с низа 'Приключенията на Иво', защото символът плюс изисква поне едно срещане на айл.

За съвпадане със символа плюс трябва да се използва екраниране - \+.

Определен брой съвпадения с фигурни скоби

Определен брой повторения на група се постига с написване на число във фигурни скоби, следващи групата. Например регулярният израз (Ха){3} ще съвпадне с низа 'ХаХаХа', но не и с 'ХаХа', в който групата се повтаря само два пъти.

Вместо конкретно число, в скобите може да се минимален и максимален брой повторения, разделени със запетая. Например регулярният израз (Ха){3,5} съвпада с 'ХаХаХа', 'ХаХаХаХа' и 'ХаХаХаХаХа'.

Първото или второто число в скобите може да се изпусне, което премахва ограничението за минимален или максимален брой повторения. (Ха){3,} съвпада с три или повече повторения на групата (Ха), а (Ха){,5} съвпада с нула до пет повторения. Използването на скоби прави регулярните изрази по-кратки. Следващите дв регулярни израза определят еднакви шаблони:

(Ха){3}
(Ха)(Ха)(Ха)

Тези два регулерни израза също дефинират еднакви шаблони:

(Ха){3,5}
((Ха)(Ха)(Ха)|(Ха)(Ха)(Ха)(Ха)|(Ха)(Ха)(Ха)(Ха))

Пример от интерактивната конзола:

>>> haRegex = re.compile(r'(Ха){3}')
>>> mo1 = haRegex.search('ХаХаХа')
>>> mo1.group()
'ХаХаХа'
>>> mo2 = haRegex.search('Ха')
>>> mo2 == None

Тук (Ха){3} съвпада с 'ХаХаХа', но не и с 'Ха'. Методът search() връща None при липса на съвпадение.

Алчно съвпадане

Регулярният израз (Ха){3,5} може да съвпадне с три, четири или пет повторения на Ха в низа 'ХаХаХаХаХа'. Но в предишния пример с фигурни скоби резултатът беше 'ХаХаХаХаХа', а не някоя от по-кратките възможности.

Регулярните изрази в Python по подразбиране са алчни (greedy). Това означава, че при много възможности те избират най-дългото възможно съвпадение. Неалчната (non-greedy, lazy) версия на фигурните скоби, които съвпадат с най-краткия възможен низ, се получава с добавяне на въпросителен знак след скобите.

Пример от интерактивната конзола:

>>> greedyHaRegex = re.compile(r'(Ха){3,5}')
>>> mo1 = greedyHaRegex.search('ХаХаХаХаХа')
>>> mo1.group()
'ХаХаХаХаХа'
>>> nongreedyHaRegex = re.compile(r'(Ха){3,5}?')
>>> mo2 = nongreedyHaRegex.search('ХаХаХаХаХа')
>>> mo2.group()
'ХаХаХа'

Въпросителният знак има две различни значения в регулярните изрази. Той обозначава неалчно съвпадение или незадължителна група. Тези две значение са напълно независими.

Методът findall()

Освен метод search(), обектите от тип Regex имат и метод findall(). Методът search() връща обект Match за първия съвпадащ резултат. Методът findall() връща всички съвпадащи низове. Пример от интерактивната конзола, показващ как search() връща Match обект, отговарящ само на първото съвпадение в текста:

>>> phoneNumRegex = re.compile(r'\d\d\d\d \d\d\d \d\d\d')
>>> mo = phoneNumRegex.search('Мобилен: 0893 456 789 Офис: 0789 987 654')
>>> mo.group()
'0893 456 789'

От друга страна findall() не връща Match обект, а списък от низове (ако в регулярния израз няма групи). Всеки низ в списъка е част от текста, която съвпада с регулярния израз. Пример от интерактивната конзола:

>>> phoneNumRegex = re.compile(r'\d\d\d\d \d\d\d \d\d\d') # няма групи
>>> phoneNumRegex.findall('Мобилен: 0893 456 789 Офис: 0789 987 654')
['0893 456 789', '0789 987 654']

При наличие на групи в регулярния израз, findall() връща списък от комплекти. Всеки комплект представя един намерен резултат, като елементите в комплекта са съвпадналите низове за всяка група от регулярния израз. Пример за резултата от findall() от интерактивната конзола (регулярният израз сега е с групи):

>>> phoneNumRegex = re.compile(r'(\d\d\d\d) (\d\d\d) (\d\d\d)') # има групи
>>> phoneNumRegex.findall('Мобилен: 0893 456 789 Офис: 0789 987 654')
[('0893', '456', '789'), ('0789', '987', '654')]

Обобщение на резултата от findall():

  • Ако в регулярния израз няма групи - списък от съвпадащите низове ['0893 456 789', '0789 987 654']
  • Ако в регулярния израз има групи - списък от комплекти от низове (по един низ за всяка група) [('0893', '456', '789'), ('0789', '987', '654')]

Символни класове

В примерите с регулярен израз за телефонен номер се използва \d, което представя коя да е цифра. Тоест \d е съкратен запис на регулярния израз (0|1|2|3|4|5|6|7|8|9). Има още много подобни съкратени записи, които представят символи от определени класове. Те са показани в следващата таблица:

Съкратен запис за някои Символни класове
Съкратен запис Представя
\d Цифра от 0 до 9
\D Символ, различен от цифрите от 0 до 9
\w Буква, цифра или подчертавка (символите от думите, "word")
\W Символ, който не е буква, цифра или подчертавка
\s Интервал, табулация или символ за нов ред (невидими символи, "space")
\S Символ, който не е интервал, табулация или нов ред

Символните класове са удобни за съкращаване на регулярни изрази. Символният клас [0-5] ще съвпадне само с цифри от 0 до 5. Това е доста по-кратко от писане на (0|1|2|3|4|5). \d съвпада с цифри, \w съвпада с цифри, букви и подчертавка, но няма кратко записване за съвпадане единствено на букви. (Това може да се постигне със символен клас [а-яА-Я], както е обяснено по-късно).

Пример от интерактивната конзола:

>>> xmasRegex = re.compile(r'\d+\s\w+')
>>> xmasRegex.findall('12 сарми, 11 ореха, 10 ябълки, 9 сливи, 8 питки')
['12 сарми', '11 ореха', '10 ябълки', '9 сливи', '8 питки']

Регулярният израз \d+\s\w+ ще съвпадне с текст, който има една или повече цифри (\d+), последван от невидим символ (\s), последван от една или повече букви/цифри/подчертавки (\w+). Методът findall() връща списък с всички съвпадащи низове.

Създаване на собствени символни класове

Има случаи, в които трябва да се намери съвпадение с множество от символи, но съкратените символни класове (\d, \w, \s и така нататък) са прекалено общи. С използването на квадратни скоби може да се дефинира собствен символен клас. Например символния клас [аъоуиеАЪОУИЕ] ще съвпадне с коя да е гласна, малка или голяма. Пример от интерактивната конзола:

>>> vowelRegex = re.compile(r'[аъоуиеАЪОУИЕ]')
>>> vowelRegex.findall('Кучето яде храна за бебета. ХРАНА ЗА БЕБЕТА.')
['у', 'е', 'о', 'е', 'а', 'а', 'а', 'е', 'е', 'а', 'А', 'А', 'А', 'Е', 'Е', 'А']

Могат да се използват и интервали от букви иил числа с използване на тире. Например символния клас [а-яА-Я0-9] ще съвпадна да всички малки и главни букви и цифри.

Вътре в квадратните скоби не трябва да се екранират символи. Тоест не трябва да се добавя обратна наклонена черта за екраниране на ., *, ? или (). Например символния клас [0-5.] ще съвпадне с цифрите от 0 до 5 и точка. Не е необходимо да се пише [0-5\.].

Добавянето на символ карета(^, циркумфлекс, caret ) след отварящата скоба на символен клас означава обръщане на символния клас (негативен символен клас). Негативен символен клас съвпада със символите, които не са в символния клас. Пример от интерактивната конзола:

>>> consonantRegex = re.compile(r'[^аъоуиеАЪОУИЕ]')
>>> consonantRegex.findall('Кучето яде храна за бебета. ХРАНА ЗА БЕБЕТА.')
['К', 'ч', 'т', ' ', 'я', 'д', ' ', 'х', 'р', 'н', ' ', 'з', ' ', 'б', 'б', 'т', '.', ' ', 'Х', 'Р', 'Н', ' ', 'З', ' ', 'Б', 'Б', 'Т', '.']

Така се намират всички символи, които не са гласни.

Символите карета и долар

Символът карета (^ , caret) може да се използва в началото на регулярен израз, за да укаже, че съвпадението трябва да се намира в началото на претърсвания текст. По подобен начин в края на регулярен израз може да се добави знак долар ( $ ), за индикация, че резултатът трябва да е в края на текста. Двата символа могат да се използват заедно, като тогава регулярния израз трябва да съвпадне с целия текст, а не само с някаква част от него.

Например регулярният израз r'Здравей' ще съвпадне с низ, който започва със 'Здравей'. Пример от интерактивната конзола:

>>> beginsWithHello = re.compile(r'^Здравей')
>>> beginsWithHello.search('Здравей, свят!')

>>> beginsWithHello.search('Той каза здравей.') == None

Регулярният израз r'\d$' ще съвпадне с низове, които завършват с цифра от 0 до 9. Пример от интерактивната конзола:

>>> endsWithNumber = re.compile(r'\d$')
>>> endsWithNumber.search('Твоето число е 42')

>>> endsWithNumber.search('Твоето число е четиридесет и две') == None
True

Регулярният израз r'^\d+$' ще съвпадне с низове, които започват и завършват с една или повече цифри. Пример от интерактивната конзола:

>>> wholeStringIsNum = re.compile(r'^\d+$')
>>> wholeStringIsNum.search('1234567890')

>>> wholeStringIsNum.search('12345абв67890') == None
True
>>> wholeStringIsNum.search('12 34567890') == None
True

Последните две извиквания на search() се демонстрира как целият низ трябва да съвпадне, когато се използват ^ и $.

Заместващ символ

Символът . (точка, dot) в регулярен израз се нарича заместител (жокер, wildcard), който съвпада с всеки символ, без нов ред. Пример от интерактивната конзола:

>>> onRegex = re.compile(r'.он')
>>> onRegex.findall('Дон на кон вижда слон в балон тежък тон')
['Дон', 'кон', 'лон', 'лон', 'тон']

Символът точка замества само един символ, така че съвпадението на текста балон и слон е само лон. За съвпадане със символа точка трябва да се използва екраниране - \. .

Съвпадане на всичко с точка-звезда

Понякога трябва да се намери съвпадение със всичко. Например, нека да трябва да се намери съвпадение на низа 'Име:', последван от всякакъв текст, последван от 'Фамилия:', също последван от всякакъв текст. За съвпадане на "всичко" може да се използва точка-звезда (.*, dot-star). Символът точка означава "който и да е символ без нов ред", а символът звезда означава "нула или повече повторения на предишния символ".

Пример от интерактивната конзола:

>>> nameRegex = re.compile(r'Име: (.*) Фамилия: (.*)')
>>> mo = nameRegex.search('Име: Станимир Фамилия: Денчев')
>>> mo.group(1)
'Станимир'
>>> mo.group(2)
'Денчев'

Точка-звезда е в алчен режим - винаги се опитва да съвпадне с колкото може повече текст. За съвпадане на всичко по не-алчен начин се използва точка, звезда и въпросителен знак (.*?). Както при фигурните скоби, въпросителният знак изключва аляния режим.

Пример от интерактивната конзола, показващ разликата между алчна и не-алчна версия:

>>> nongreedyRegex = re.compile(r'<.*?>')
>>> mo = nongreedyRegex.search('<Тест> алчен>')
>>> mo.group()
'<Тест>'
>>> greedyRegex = re.compile(r'<.*>')
>>> mo = greedyRegex.search('<Тест> алчен>')
>>> mo.group()
'<Тест> алчен>'

И двата регулярни израза се превеждат до "Намери отваряща ъглова скоба, последвана от каквото и да е, последвано от затваряща ъглова скоба". Но в низа '<Тест> алчен>' има две възможности за съвпадане на затварящата ъглова скоба. При неалчен режим Python намира най-краткото възможно съвпадение '<Тест>'. При алчен режим се намира най-дългото възможно съвпадение '<Тест> алчен>'.

Съвпадане на нови редове със символа точка.

Точка-звезда ще съвпадне с всичко без нови редове. Точката може да съвпадне с всички символи, включително нов ред, с подаване на re.DOTALL като втори аргумент при извикването на re.compile().

Пример от интерактивната конзола:

>>> noNewlineRegex = re.compile('.*')
>>> noNewlineRegex.search('Едно\nДве\nТри').group()
'Едно'
>>> newlineRegex = re.compile('.*', re.DOTALL)
>>> newlineRegex.search('Едно\nДве\nТри').group()
'Едно\nДве\nТри'

Регулярният израз noNewlineRegex, при създаването на който не се подава re.DOTALL, ще съвпадне с всичко до първия символ за нов ред. Регулярният израз newlineRegex, за създаването на който се използва re.DOTALL, съвпада с всичко. По тази причина извикването на newlineRegex.search() съвпада с целия низ, включително новите редове.

Преглед на символите в регулярните изрази

В главата се въвеждат много новости. Ето кратко обобщение на основния синтаксис на регулярните изрази:

  • ? съвпада с нула или едно повторение на предхождащата група.
  • * съвпада с нула или повече повторения на предхождащата група.
  • + съвпада с едно или повече повторения на предхождащата група.
  • {n} съвпада с точно n повторения на предхождащата група.
  • {n,} съвпада с n или повече повторения на предхождащата група.
  • {,m} съвпада с 0 до m повторения на предхождащата група.
  • {n, m} съвпада с най-малко n и най-много m повторения на предхождащата група.
  • {n,m}? или *? или +? съвпада с предхождащата група по неалчен начин.
  • ^spam означава, че низът трябва да започва с spam
  • spam$ означава, че низът трябва да завършва с spam
  • . съвпада с всеки символ без нови редове.
  • \d, \w и \s съвпадат съответно с цифра, дума или интервал
  • \D, \W и \S съвпадат с всичко без съответно цифра, дума или интервал
  • [абв] съвпада с един от символите между скобите (в случая а, б или в )
  • [^абв] съвпада с един символ, който не е между скобите

Съвпадане без значение на регистъра

Обикновено регулярните изрази съвпадат с текст, който използва точно същия регистър като зададения. Например следващите регулярни изрази съвпадат с напълно различни низове:

>>> regex1 = re.compile('РобоКоп')
>>> regex2 = re.compile('РОБОКОП')
>>> regex3 = re.compile('робОкоп')
>>> regex4 = re.compile('РобокОп')

Но понякога е от значение съвпадане на буквата, без значение дали е малка или главна. Това може да се постигне с подаване на re.IGNORECASE или re.I каго втори аргумент при извикване на re.compile(). Пример от интерактивната конзола:

>>> robocop = re.compile(r'робокоп', re.I)
>>> robocop.search('РобоКоп е отчасти човек, отчсти машина').group()
'РобоКоп'

>>> robocop.search('РОБОКОП защитава невинните.').group()
'РОБОКОП'

>>> robocop.search('Стани, защо толкова много се споменава робокоп?').group()
'робокоп'

Заместване на низове с метода sub()

Регулярните изрази могат не само да намират текст по шаблони, но и да подменят текста в тези шаблони. Regex обектът има метод sub(), приемащ два аргумента. Първият аргумент е низът, с който ще се заместят намерените съвпадения. Вторият аргумент е низът на регулярния израз. Методът sub() връща низ, в който са приложени заместванията.

Пример от интерактивната конзола:

>>> namesRegex = re.compile(r'Агент \w+')
>>> namesRegex.sub('ЗАЛИЧЕНО', 'Агент Ангел предаде секретните документи на Агент Пешо')
'ЗАЛИЧЕНО предаде секретните документи на ЗАЛИЧЕНО'

Понякога съвпадналият текст трябва да се използва като част от заместващия. В първия аргумент на sub() може да се използват \1, \2, \3 и така нататък, което означава "Добави текста на група 1, 2, 3 и така нататък в заместващия низ".

Например имената на агентите могат да се заличат и да се показва само првата буква от името. За тази цел може да се използва регулярния израз Агент (\w)\w*, а за първи аргумент на sub() да се използва r'\1****'. В този низ \1 ще се замести с каквото е съвпаднало с група 1 - тоест групата (\w).

>>> agentNameRegex = re.compile(r'Агент (\w)\w*')
>>> agentNameRegex.sub(r'\1****', 'Агент Ангел каза на Агент Пешо, че Агент Галя е знаела, че Агент Боби е двоен агент.')
'А**** каза на П****, че Г**** е знаела, че Б**** е двоен агент.'

Използване на сложни регулярни изрази

Регулярните изрази са удобни за търсене на прост шаблон. Но намирането на дълги и сложни шаблони може да се нуждае от дълги и объркващи регулярни изрази. Писането им може да се улесни с използването на "многословен" режим, в който могат да се добавят коментари. Този режим се активира с подаване на променливата re.VERBOSE като втори аргумент при извикването на re.compile().

Така вместо трудния за четене регулярен израз

phoneRegex = re.compile(r'((+359\s|08)(\d{2})(\s)?(\d{3})(\s)?(\d{3})))')

може да има разпределен по редове четим регулярен израз:

phoneRegex = re.compile(r'''(
 (\+359\s|08) # код
 (\d{2}) # код на оператор
 (\s)? # интервал
 (\d{3}) # първите три цифри
 (\s)? # интервал
 (\d{3}) # последните три цифри
 )''', re.VERBOSE)

За създаването на многоредов низ се използва тройни кавички ('''). По този начин регулярният израз е на няколко реда, което го прави много по разбираем.

Правилата за коментари в регулярни изрази са същите като в останалия код на Python - символът # и всичко след него се игнорира. Също така се игнорират и всички интервали в многоредовия низ. Подреденият по подобен начин регулярен израз е много по-четим.

Комбиниране на re.IGNORECASE, re.DOTALL и re.VERBOSE

Какво да се направи, ако трябва да се използва re.VERBOSE, за да могат да се напишат комнетари в регулярен израз, но и re.IGNORECASE, за да се игнорира регистъра на буквите? За съжаление функцията re.compile() приема само една стойност като втори аргумент. Това ограничение може да се заобиколи като re.IGNORECASE, re.DOTALL и re.VERBOSE се комбинират със символа права черта (|), който в този контекст е побитов или оператор (bitwise or).

Така, ако трябва трябва регулярен израз да не прави разлика между малки и главни букви и символът точка да включва и новите редове, извикването на re.compile() трябва да изглежда по подобен начин:

>>> someRegex = re.compile('шаблон', re.IGNORECASE | re.DOTALL)

Включването на всички три възможности изглежда така :

>>> someRegex = re.compile('шаблон', re.IGNORECASE | re.DOTALL | re.VERBOSE)

Този синтаксис е старомоден, защото произиза от по-старите версии на Python. Тук няма да се обсъждат побитовите оператори, но в интернет може д асе намери повече информация. Като втори аргумент могат да се подадат и други опции, но те не са често срещани. За тях също може да се прочете в интернет.

Проект: Извличане на телефонни номера и имейл адреси

Нужно е да се свърши скучната работа по намирането на всеки телефонен номер и имейл адрес в дълга уеб страница или документ. Ръчното превъртане на страницата и търсене може да отнеме дълго време. Но при наличие на програма, която претърсва за ноемра и имейл адреси в копиран текст, цялата работа ще се сведе до натискане на ctrl-A за избиране на целия текст, натискане на ctrl-C за копирането му и стартиране на програмата. Тя може да замени копирания текст с нов, в който има само намерените телефонни номера и имейл адреси.

При започване на проект е изкушаващо да се започне веднага с писането на код. Но често е добре да се отстъпи крачна назад, за да се види цялата картина. Препоръчително е първо да се направи общ план на програмата. Все още не се мисли за конкретния код - тоеа ще е на по-късен етап. В началото се мисли само на по-високо ниво.

Например, извличането на телефони и имейл адреси ще трябва да върши следното:

  1. Взема копирания текст.
  2. Намира всички телефонни номера и имейл адреси в текста.
  3. Поставя намерените резултати обратно в клипборда.

Сега може да се помисли как този план ще се пресъздаде с код. Кодът ще трябва да върши следното:

  1. Използва модула pyperclip за копиране и поставяне на низове.
  2. Създава два регулярни израза, един за наммиране на телефонни номера и друг за намиране на имейл адреси.
  3. Намира всички съвпадения, не само първото, и за двата регулярни израза.
  4. Форматира намерените низове в един низ по удобен начин.
  5. Показва някакво съобщение, ако няма намерени резултати.

Това е картата на проекта. Писането на кода за всяка от тези стъпки може да се направи самостоятелно. Всяка стъпка е постижима с досегашните знания за Python.

Стъпка 1 : Създаване на регулярен израз за телефонен номер

Първо, трябва да се създаде регулярен израз, с който да се търсят телефонни номера. Следва първият вариант на програмата, записана като phoneAndEmail.py:

#! python3
# phoneAndEmail.py - Намира телефонни номера и имейл адреси в копирания текст

import pyperclip, re

phoneRegex = re.compile(r'''(
    (\+359\s|08) # код
    (\d{2}) # код на оператор
    (\s)? # интервал
    (\d{3}) # първите три цифри
    (\s)? # интервал
    (\d{3}) # последните три цифри
    )''', re.VERBOSE)

# TODO: Създаване на регулярен израз за имейл

# TODO: Намиране на съвпаденията в копирания текст

# TODO: Копиране на резултатите в клипборда

TODO коментарите са скелета на програмата. Те ще се заместват пти писането на реалния код.

Телефонният номер може да започва с международния код на България или с телефонния код за мобилен номер. Тези две възможности са свързани с права черта - оператор за избор. Коментарите на всеки ред помагат да се запомни какво трябва да намери съответната група.

В телефонните номера се използват интервали за разделители. Те са представени с (\s). Разделянето не е задължително, така че след групата има въпросителен знак. Следващите части от израза са ясни - три цифри, следвани от разделител и още три цифри.

Стъпка 2: Създаване на регулярен израз за имейл адрес

Ще трябва да има и регулярен израз, който да намира имейл адреси. Програмата вече изглежда по този начин:

#! python3
# phoneAndEmail.py - Намира телефонни номера и имейл адреси в копирания текст

import pyperclip, re

phoneRegex = re.compile(r'''(
    --изрязани редове--

# регулярен израз за имейл
emailRegex = re.compile(r'''(
    ➊[a-zA-Z0-9._%+-]+ # потребителско име
    ➋@ # символът @
    ➌[a-zA-Z0-9.-] # името на домейна
    (\.[a-zA-Z]{2,4}) # точка нещо-си
)''', re.VERBOSE)

# TODO: Намиране на съвпаденията в копирания текст

# TODO: Копиране на резултатите в клипборда

Потребителското име в имейл адреса ➊ е един или повече символи, които могат да са : малки и главни букви, цифри, точки, подчертавки, проценти, плюс и тирета. Всички тези символи са сложени в един символен клас [a-zA-Z0-9._%+-].

Потребителското име е разделено от домейна със смвола @ ➋. В името на домейна ➌ се използва по-ограничен символен клас, съдържащ само букви, цифри, точки и тирета. Последната част е "точка-ком" (техническото име е домейн от първо ниво, top-level domain), който всъщност може да е точка-нещо. Дължината е между два и четири символа.

Целият формат на имейл адресите има някои странни правила. Този регулярен израз не съвпада с всеки възможен и валиден имейл адрес, но е подходящ за почти всички срещани имейл адреси.

Стъпка 3: Намиране на всички съвпадения в копирания текст

Python модула re може да свърши тежката работа по намиране на всички съвпадения, след като има регулярни изрази за телефонни номера и имейл адреси. Функцията pyperclip.paste() ще върне низ с копирания текст, а методът findall() ще върне списък от комплекти.

Програмата вече изглежда по този начин:

#! python3
# phoneAndEmail.py - Намира телефонни номера и имейл адреси в копирания текст

import pyperclip, re

phoneRegex = re.compile(r'''(
    --изрязани редове--

# Намиране на съвпаденията в копирания текст
text = str(pyperclip.paste())

➊ matches = []
➋ for groups in phoneRegex.findall(text):
    phoneNum = ' '.join([('08' + groups[2]), groups[4], groups[6]])
    matches.append(phoneNum)
➌ for groups in emailRegex.findall(text):
    matches.append(groups[0])

# TODO: Копиране на резултатите в клипборда

Всеки намерен резултат е представен с по един комплект. Всеки комплект съдържа низ за всяка група от регулярния израз. Група 0 е за целия регулярен израз, така че групата с индекс 0 е най-важната.

Както се вижда в ➊, резултатите се събират в списък с име matches. Започва се с празен списък. Следват два for цикъла. За имейл адресите просто се добавя група 0 за всеки резултат ➌. Но за телефонните номера не е достатъчно да се добави група 0. Програмата може да намери телефонни номера с различни формати, но в крайния резултат е по-добре да са в един стандартен формат. Променливата phoneNum съдържа низ, в който са събрани някои от групите в намерения текст ➋.

Стъпка 4: Събиране на резултатите в низ за клипборда

Вече има списък от низове, представляващи намерените телефонни номера и имейл адреси, в matches. Резултатите трябва да се копират, за да са удобни за поставяне някъде другаде. Функцията pyperclip.copy() приема само една низова стойност, а не списък от низове, така че трябва да се използва метода join().

За по-лесно ориентиране в работата на програмата ще е добре да се принтира резултатът. Програмата трябва да показва съобщение и ако няма няма нито един намерен резултат.

Програмата вече изглежда по този начин:

#! python3
# phoneAndEmail.py - Намира телефонни номера и имейл адреси в копирания текст

import pyperclip, re

phoneRegex = re.compile(r'''(
    --изрязани редове--

# Копиране на резултатите в клипборда
if len(matches) > 0:
    pyperclip.copy('\n'.join(matches))
    print('Копирани резултати:')
    print('\n'.join(matches))
else:
    print('Няма намерени телефонни номера или имейл адреси.

Изпълнение на програмата

За тестване може да се използва страница с контакти на НОИ ( https://www.noi.bg/aboutbg/contacts/230-shu/136-shu ). С Ctrl-A се селектира целия текст и се копира с Ctrl-C. При изпълнение на програмата ще има подобен резултат:

Копирани резултати:
0882 18 95 62
0882 18 95 64
0882 18 95 49
0882 18 95 37
0882 18 95 86
0882 18 95 47
0882 18 95 97
0882 18 95 55
0882 18 95 61
0882 18 95 66
0882 18 95 20
0882 18 95 21
0882 18 95 14
0882 18 95 40
Shumen@nssi.bg

Идеи за подобни програми

Намирането на шаблон в текст (и евентуалното му заменяне с метода sub()) има множество приложения. Например може :

  • Да се намерят адреси на уебсайт, започващи с http:// или https://.
  • Намиране на дати в различен формат (например 14.3.2021, 14/3/2021 и 2021/3/19) и замяната им с дати е един стандартен формат.
  • Намиране на чести грешки при писане като няколко   интервала   между думите, случайно случайно повторени думи или няколко удивителни на края на изречение. Те са дразнещи!!

Обобщение

Компютърът може да бързо да претърсва текст, но трябва да има прецизни инструкции какво да търси. Регулярните изрази позволяват да се използва шаблон от символи, които се търсят, а не точния текст. Всъщност някои текстови редактори и приложения за електронни таблици позволяват търсене и заместване, което работи с регулярни изрази.

Вграденият в Python модул re позволява изполването на Regex обекти. Тези обекти имат няколко метода - search() за намиране на един резултат, findall() за намиране на всички резултати и sub() за намиране и заместване на текст.

За повече информация може да се използва официалната Python документация на https://docs.python.org/3/library/re.html

Упражнения - въпроси

  1. Коя функция създава Regex обекти?
  2. Защо при създаване не регулярни изрази често се използват буквали низове?
  3. Какво връща методът search()?
  4. Как се получава съвпадналия низ от Match обект?
  5. Коя е група 0 в регулярния израз r'(\d\d\d)-(\d\d\d-\d\d\d\d)'? Група 1? Група 2?
  6. Скобите и точките имат специално значение в регулярните изрази. Как се намира съвпадение със символите скоба и точка?
  7. Методът findall() връща списък от низове или списък от комплекти от низове. Кога връща единия вариант и кога другия?
  8. Какво означава символът | в регулярен израз?
  9. Какви две неща означава символът ? в регулярен израз?
  10. Каква е разликата между {3} и {3,5} в регулярен израз?
  11. Какво означават съкратените записвания \d, \w и \s в регулярен израз?
  12. Какво означават съкратените записвания \D, \W и \S в регулярен израз?
  13. Каква е разликата между .* и .*??
  14. Как се създава символен клас, който съвпада с всички цифри и малки букви?
  15. Как може регулярен израз да не зависи от регистъра на буквите?
  16. С какво съвпада символът . при обикновена употреба? Как се променя при подава на re.DOTALL като втори аргумент на re.compile()?
  17. Ако numRegex = re.compile(r'\d+'), какъв ще е резултатът от numRegex.sub('X', '12 картофа, 11 ябълки, пет гроша, 3 книги')?
  18. Какво позволява подаването на re.VERBOSE като втори аргумент на re.compile()?
  19. Как трябва да се напише регулярен израз, който съвпада с число, използващо запетая между всеки три цифри? Трябва да съвпадне с:
    • '42'
    • '1,234'
    • '6,368,745'
    но не с:
    • '12,34,567' (има само две цифри между запетаите)
    • '1234' (лиспва запетая)
  20. Как трябва да се напише регулярен израз, който съвпада с пълното име на човек с фамилия Ампов? Може да се приеме, че собственото име е преди фамилията и винаги е една дума, започваща с главна буква. Регулярният израз трябва да съвпадне с:
    • 'Влади Ампов'
    • 'Графа Ампов'
    • 'Робокоп Ампов'
    но не с:
    • 'влади Ампов' (собственото име не започва с главна буква)
    • 'Проф. Ампов' (предходната дума има небуквен символ)
    • 'Ампов' (няма собствено име)
    • 'Влади ампов' (фамилията не е с главна буква)
  21. Как трябва да се напише регулярен израз, който съвпада с изречение, в което : първата дума е Алисия, Боян или Васил; втората дума е яде, има или хвърля; третата дума е ябълки, котки или книги. Изречението завършва с точка. Регулярният израз не трябва да зависи от малки и главни букви. Трябва да съвпадне с :
    • 'Алисия яде ябълки.'
    • 'Боян има котки.'
    • 'Васил хвърля книги.'
    • 'Алисия хвърля Ябълки.'
    • 'БОЯН ЯДЕ КОТКИ.'
    но не с:
    • 'РобоКоп яде ябълки.'
    • 'АЛИСИЯ ХВЪРЛЯ КАРТОФИ.'
    • 'Васил яде 7 котки.'

Практически упражнения

Да се напишат програми, които да вършат следните задачи.

Намиране на дати

Да се напише регулярен израз, който намира дати с формат ДД/ММ/ГГГГ. Денят може да е от 01 до 31, месецът от 01 о 12 и годината от 1000 до 2999. Ако денят или месеца е с една цифра ще има нула пред нея.

Регулярният израз не трябва да проверява за валидна дата спрямо месеца или високосна година. Той ще приема несъществуващи дати като 31/02/2020 или 31/04/2021. След това низовете ще се съхранят в променливи с имена day, month и year. Да се напише допълнителен код, който да проверява дали датата е съществуваща. Април, юни, септември и ноември са с по 30 дни, февруари е с 28 дни, а останалите месеци са с 31 дни. Февруари има 29 дни ако годината е високосна. Високосна е всяка година, която се дели на 4 без остатък, но не и ако се дели на 100 без остатък, освен ако не се дели на 400 без остатък. Този тип сметка пречи на използването на неголям регулярен израз за проверка за валидна дата.

Проверка за сигурна парола

Да се напише функция, която използва регулярни изрази за проверяване дали дадена парола е сигурна. За да е сигурна една пароля тя трябва да е поне 8 символа, да има малки и главни букви и поне една цифра. Може да се наложи низа с паролата да се тества с повече от един регулярен израз.

Версия на метода strip() с регулярен израз

Да се напише функция, която приема низ и върши същото като метода strip(). По подразбиране функцията трябва да премахне невидимите символи в началото и в края на низа. Функцията да приема и втори аргумент - низ със символи, които да се премахнат. В този случай функцията да премахва тези символи от първия аргумент.

Подкрепете автора чрез покупка на оригиналната книга от No Starch Press или Amazon.