09. Регулярни изрази
За какво ще си говорим днес
- Очевидно, регулярни изрази
- С какво ще ни улеснят живота -- в и извън рамките на Python
- Особености в Python 3
- Плюс една дребна задачка (вече традиционна)
Проблематика
Работа с низове
- Търсене на по-сложна последователност от символи в низ
- Заместване на такива последователности с нещо друго
- Проверка дали даден низ отговаря на определени условия
Примерни проблеми -- много
- Често срещано: извличане на информация от текстови и markup-документи (XML, HMTL, ...)
- Искаме да проверим дали даден низ съдържа валиден телефонен номер
Това означава
- Трябва да съдържа само цифри
- Може да започва с код на населеното място: 02, 032 или 052
- След кода, дължината му може да е между 5 и 7 цифри
- Самият номер (след кода) не може да започва с 0, 1, 2, 3 или 4
- Затворете очи... Виждате ли редовете код вече?
Вариант за решение
def validate_phone_str(number): if '02' == number[:2]: return validate_phone_str(number[2:]) elif number[:3] in {'032', '052'}: return validate_phone_str(number[3:]) if all([c.isdigit() for c in number]): return 5 <= len(number) <= 7 return False
- Бъгав...
Втори вариант за решение
def validate_phone_re(number): pattern = r'^(02|032|052)?[5-9]\d{4,6}$' return bool(re.search(pattern, number))
- Cooler, eh?
- Ще се заемем да разучим този втори вариант
Преди това, обещаната задачка
Всичко е чудесно, но аз вече съм майстор на регулярните изрази.
За всички вас имаме следната задача
Да се провери дали дадено число е просто чрез един ред пайтън-код и регулярен израз. Разрешени операции са
- Самото число, разбира се.
re.search
с подходящ шаблон.- Употребата на низа '1'.
- Операторa
*
.
Решения по-късно
Понятия
- Основно: "шаблон" (pattern), още "регулярен израз"
- Специални (meta) символи
- Екраниране (escape-ване) на специалните символи
Регулярните изрази в контекста на Пайтън
- import re -- модулът, реализиращ PCRE-функционалността
- Escape-ване на специални символи: чрез \
- За задаване на шаблоните обикновено се ползват raw-низовете
- Пример: r'\s+'
- Py3k unicode особености: unicode шаблон и низ, или 8-bit шаблон и низ -- консистентност
Задаване на шаблон
- Всеки символ, освен някои специални, означава себе си.
- Цялата магия е в специалните символи:
. \| ( ) [ ] { } + \ ^ $ * ?
- \ пред специален символ го прави неспециален такъв.
Нашата помощна функция matcher
- Примерите ще демонстрираме чрез наша функция matcher().
- Не е част от стандартната библиотека на Python :)
- Ще ви покажем 4-те й реда код по-късно.
- Сигнатура: matcher(pattern, string).
Пример
>>> matcher('pat', 'Find a pattern.') 'Find a pattern.' >>> matcher('#', 'What ###?') 'What ###?'
Магия от level 1 -- Повторения (quantifiers)
Важат за непосредствено предхождащия ги символ/клас/група. Нека го означим с s.
- s* означава нула или повече повторения на s.
- s+ търси едно или повече повторения на s.
- s? съвпада с нула или едно повторение на s.
- s{m,n} означава между m и n повторения на s, където можем да пропуснем m или n. s{,n} има смисъл на нула до n повторения, а s{m,} — поне m повторения.`
Скоби и групиране
Символите ( и ) се използват за логическо групиране на части от шаблона с цел
- Контролиране областта на влияние на дадена операция
- Възможност за референция към "ограденото" в скобите
- Задаване на по-специални (и не толкова често употребявани) конструкции
Повече за групите -- след малко.
Примери
matcher('o+', 'Goooooooogle') # 'Goooooooogle' matcher('[hH]o+', 'Hohohoho...') # 'Hohohoho...' # Хм. Не искахме точно това. По-скоро: matcher('([hH]o)+', 'Hohohoho...') # 'Hohohoho...' matcher('([hH]o){2,3}', 'Hohohoho...') # 'Hohohoho...'
По подразбиране — алчно търсене за съвпадение (greedy). Деактивира се с ? след квантора.
matcher('[hH]o+', 'Hoooooohohooo...') # 'Hoooooohohooo...' matcher('[hH]o+?', 'Hoooooohohooo...') # 'Hoooooohohooo...'
Значения на специалните символи
- . съвпада с един произволен символ. По подразбиране символите за нов ред не се включват в тази група.
- ^ съвпада с началото на низ (или на ред, ако се работи в MULTILINE режим.)
- $ съвпада с края на низ (или на ред, ако се работи в MULTILINE режим.)
Значения на специалните символи
- | има смисъл на или, например
matcher('day|nice', 'A nice dance-day.') # 'A nice dance-day.' matcher('da(y|n)ce', 'A nice dance-day.') # 'A nice dance-day.'
NB! Единствено |
се прилага не над непосредствените му символи/класове, а на целия низ отляво/отдясно
matcher('ab|c|e', 'abcdef') # 'abcdef' matcher('am|c|e', 'abcdef') # 'abcdef' matcher('a(m)|c|e', 'abcdef') # 'abcdef'
Магия от level 2 (DRY) -- Символни класове
- Набор от символи, заграден от [ и ], например [aeoui].
- Съвпадат с точно един от символите, описани в класа, например
>>> matcher('[aeoui]', 'Google')
'Google'
- Отрицание на клас -- посредством ^ в началото на класа
matcher('[^CBL][aeoui]', 'Cobol') # 'Cobol'
- Диапазони от символи
>>> matcher('[0-9]{1,3}-[a-z]', 'Figure 42-b') 'Figure 42-b' >>> matcher('[^a-zA-Z-]', 'Figure-42-b') 'Figure-42-b'
Предефинирани класове
- \d -- една цифра; същото като [0-9].
- \D -- един символ, който не е цифра; същото като [^0-9].
- \s -- един whitespace символ -- [\t\r\n\f\v].
- \S -- един символ, който не е whitespace -- [^\t\r\n\f\v].
- \w -- една буква или цифра.
- \W -- един символ, който не е буква или цифра.
- \b -- нула символа, но граница на дума.
- И други.
Примери за употреба на класове
matcher(r'\d+', 'Phone number: 5551234') # 'Phone number: 5551234' matcher(r'\w+', 'Phone number: 5551234') # 'Phone number: 5551234' matcher(r'\s+', 'Phone number: 5551234') # 'Phone number: 5551234'
Gandalf The Gray -- Групи
- Групите са частите от даден шаблон, оградени в ( и ).
- Към тях можем да се обръщаме и от самия шаблон чрез специалните класове \1 — първата група, \2 — втората и така нататък.
- Няколко примера
matcher(r'(\w+).*\1', 'Matches str if str repeats one of its words.'); 'Matches str if str repeats one of its words.' # Хм. Не точно. Нека опитаме пак matcher(r'(\b\w+\b).*\1', 'Matches str if str repeats one of its words.'); 'Matches str if str repeats one of its words.'
Групи за напреднали (Gandalf The White)
- (?:...) -- използване на скоби, без да се създава група.
- (?P
...) -- текстът, отговарящ на групата, може да бъде достъпван чрез име, вместо чрез номер. - (?P=name) -- търси съвпадение за текста, намерен по-рано от групата, кръстена name.
- (?#...) -- коментар, игнорира се.
- (?=...) -- съвпада, ако ... следва, но не го "консумира" (look-ahead).
- (?!...) -- съвпада, ако ... не следва.
- (?(id/name)yes|no) -- търси за шаблона 'yes', ако групата с номер/име съвпада, или с (опционалния) шаблон 'no' иначе.
- Още: help(re)
Методи на модула re
- re.search() -- проверява дали даден низ съдържа текст, отговарящ на зададения шаблон
- re.match() -- същото както горното, само че се търси за съвпадение в началото на низа
- re.findall() -- връща като списък всички съвпадения на шаблона в дадения низ
- re.finditer() -- същото като горното, но връща итератор
Методи на модула re (2)
- re.sub(pattern, repl, string, count=0) -- заместване в низ, на база на шаблон
- re.split(pattern, string, maxsplit=0) -- разделяне на низ на парчета, на база на шаблон
- re.escape(pattern) -- escape-ва всички специални за регулярен израз символи
- Пример: re.escape('a(a)\s+') ще върне 'a\\(a\\)\\\\s\\+'
- Още: help(re)
MatchObject
- group() -- връща частта от низа, отговаряща на шаблона (и още...)
- start() -- връща началото на съвпадението в низа
- end() -- връща края на съвпадението в низа
- span() -- връща (start, end) под формата на tuple
Флагове
- re.I (re.IGNORECASE) — case-insensitive търсене.
- re.L (re.LOCALE) — кара \w, \W, \b, \B да зависят от текущия locale.
- re.M (re.MULTILINE) — кара "^" да съвпада както с начало на низ, така и с начало на ред, докато "$" ще съвпада с край на ред или края на низа.
- re.S (re.DOTALL) — "." ще съвпада с всеки символ, включително и нов ред.
- re.X (re.VERBOSE) — режим на игнориране на white-space и коментари (за по-дългички RE).
- re.А (re.ASCII) — кара \w, \W, \b, \B, \d, \D да отговарят на съответните ASCII-класове.
Кодът на matcher()
def matcher(regex, string): match = re.search(regex, string) if match is None: return string start, end = match.span() return string[:start] + '<<<' + string[start:end] + '>>>' + string[end:]
На финалната права...
- Имате ли предложения за задачата от началото?
- 'prime' if not re.search(<някакъв шаблон>, '1' * <число>) else 'not prime'
- Шаблонът: r'^1?$|^(11+?)\1+$'. Въпроси?
XKCD
Още въпроси?
- Пишете ни на fmi@py-bg.net
- Страница на курса: http://fmi.py-bg.net/
- Форуми на курса: http://fmi.py-bg.net/topics
- Курсът в Twitter: http://twitter.com/pyfmi
- Курсът във Facebook: http://www.facebook.com/group.php?gid=104970619536589