Обратите внимание, что в предыдущем примере была предусмотрена возможность пропуска необязательных пробельных символов и использования заглавных и строчных букв. Это позволяет выделить основную причину, по которой может оказаться предпочтительнее использовать шаблоны, — они поддерживают более обобщенный способ обработки текста, чем строковые методы. Ниже приводится еще один показательный пример: мы уже видели, что строковые методы способны разбивать строки и выполнять замену подстрок, но они не смогут проделать эту работу, если в качестве разделителей используются различные строки:
>>> ‘aaa—bbb—ccc’.split(‘—‘)
[‘aaa’, ‘bbb’, ‘ccc’]
>>> ‘aaa—bbb—ccc’.replace(‘—‘, ‘…’) # строковые методы используют ‘aaa…bbb…ccc‘ # фиксированные строки >>> ‘aaa—bbb==ccc‘.split([‘—‘, ‘==’])
TypeError: Can’t convert ‘list’ object to str implicitly
(TypeError: Невозможно неявно преобразовать объект ‘list’ в str)
>>> ‘aaa—bbb==ccc’.replace([‘—‘, ‘==’], ‘…’)
TypeError: Can’t convert ‘list’ object to str implicitly
(TypeError: Невозможно неявно преобразовать объект ‘list’ в str)
Шаблоны не только справляются с подобными задачами, но и дают возможность напрямую определять альтернативы, благодаря особому синтаксису шаблонов. В следующем примере конструкция —|== соответствует ли бо строке —, ли бо строке ==. Конструкции [-=] соответствует либо символ -, либо = (множество символов). А конструкция (?:) может использоваться для группировки вложенных частей шаблона без создания группы, сохраняющей подстроку (функция split интерпретирует группы особым образом):
>>> import re
>>> re.split(‘—‘, ‘aaa—bbb—ccc’)
[‘aaa’, ‘bbb’, ‘ccc’]
>>> re.sub(‘—‘, ‘…’, ‘aaa—bbb—ccc’) # случай с единственным
# разделителем
‘aaa…bbb…ccc’
>>> re.split(‘—|==’, ‘aaa—bbb==ccc’) # разбить по — или ==
[‘aaa’, ‘bbb’, ‘ccc’]
>>> re.sub(‘—|==’, ‘…’, ‘aaa—bbb==ccc’) # заменить — или == ‘aaa…bbb…ccc’
>>> re.split(‘[-=]’, ‘aaa—bbb=ccc‘) # односимвольные альтернативы
[‘aaa‘, ‘bbb‘, ‘ccc‘]
>>> re.split(‘(—)|(==)’, ‘aaa—bbb==ccc‘) # результат разбиения
[‘aaa‘, ‘—‘, None, ‘bbb‘, None, ‘==’, ‘ccc‘] # включает группы
>>> re.split(‘(?:—)|(?:==)’, ‘aaa—bbb==ccc‘) # часть выражения, не группы [‘aaa‘, ‘bbb‘, ‘ccc‘]
Операция разбиения тоже может извлекать простые подстроки при использовании фиксированных разделителей, но шаблоны способны обрабатывать окружающий контекст подобно скобкам, помечать части как необязательные, пропускать пробельные символы и многое другое. Проверки \s* в следующем примере обозначают ноль или более пробельных символов (задан класс символов); \s+ — означают один или более таких символов; /? соответствует необязательному символу слэша; [a-z] — любому строчному символу (задан диапазон); (.*?) обозначает необходимость сохранения подстроки, состоящей из нуля или более любых символов — но не более, чем необходимо для совпадения с оставшейся частью шаблона (минимально); а метод groups используется для извлечения сразу всех подстрок, совпавших с фрагментами шаблона, заключенными в круглые скобки:
>>> ‘spam/ham/eggs’.split(‘/’) [‘spam’, ‘ham’, ‘eggs’]
>>> re.match(‘(.*)/(.*)/(.*)’, ‘spam/ham/eggs’).groups() (‘spam’, ‘ham’, ‘eggs’)
>>> re.match(‘<(.*)>/<(.*)>/<(.*)>’, ‘<spam>/<ham>/<eggs>’).groups() (‘spam’, ‘ham’, ‘eggs’)
>>> re.match(‘\s*<(.*)>/?<(.*)>/?<(.*)>’, ‘ <spam>/<ham><eggs>’).groups() (‘spam’, ‘ham’, ‘eggs’)
>>> ‘Hello pattern world!’.split()
[‘Hello’, ‘pattern’, ‘world!’]
>>> re.match(‘Hello\s*([a-z]*)\s+(.*?)\s*!’, ‘Hellopattern world !’).groups() (‘pattern’, ‘world’)
На практике иногда существует более одного совпадения. Метод findall предоставляет возможность разделывать объекты строк в пух и прах — он отыскивает все совпадения с шаблоном и возвращает все совпавшие подстроки (или список кортежей, при наличии нескольких групп). Метод search похож на него, но он прекращает сопоставление после первого же совпадения — он напоминает метод match с начальным сканированием вперед. В следующем примере строковый метод обнаруживает только одну заданную строку, а шаблоны позволяют отыскать и извлечь текст в скобках, находящийся в любом месте строки, даже при наличии дополнительного текста:
>>> ‘<spam>/<ham>/<eggs>’.find(‘ham‘) # отыскать смещение подстроки 8
>>> re.findall(‘<(.*?)>’, ‘<spam>/<ham>/<eggs>’) # отыскать все
[‘spam’, ‘ham’, ‘eggs’] # совпадения/группы
>>> re.findall(‘<(.*?)>’, ‘<spam> / <ham><eggs>’)
[‘spam’, ‘ham’, ‘eggs’]
>>> re.findall(‘<(.*?)>/?<(.*?)>’, ‘<spam>/<ham> … <eggs><cheese>’) [(‘spam’, ‘ham’), (‘eggs’, ‘cheese’)]
>>> re.search(‘<(.*?)>/?<(.*?)>’,
‘todays menu: <spam>/<ham>…<eggs><s>’).groups()
(‘spam‘, ‘ham‘)
При использовании findall особенно удобно использовать оператор (?s), чтобы обеспечить совпадение . с символами кон ца стро ки в многострочном тексте — без этого . будет совпадать с любыми другими символами кроме символов конца строки. Ниже демонстрируется поиск двух смежных строк в скобках с произвольным текстом между ними, с пропуском и без пропуска символов конца строки:
>>> re.findall(‘<(.*?)>.*<(.*?)>’, ‘<spam> \n <ham>\n<eggs>’) # останов
# на \n
[]
>>> re.findall(‘(?s)<(.*?)>.*<(.*?)>’, ‘<spam> \n <ham>\n<eggs>’) # максим. [(‘spam’, ‘eggs’)]
>>> re.findall(‘(?s)<(.*?)>.*?<(.*?)>’, ‘<spam> \n <ham>\n<eggs>’) # миним.
[(‘spam‘, ‘ham‘)]
Чтобы сделать крупные шаблоны более удобочитаемыми, группам с совпавшими подстроками можно даже присваивать име на, используя синтаксис <?P<name>, и после сопоставления извлекать их по именам, однако эта особенность имеет ограниченное применение для findall. В следующем примере выполняется поиск строк с символами «слов» (\w), разделенных символами /; это не более чем операция разбиения строки, но в результате получаются именованные группы, и обе функции, search и findall, выполняют сканирование вперед:
>>> re.search(‘(?P<part1>\w*)/(?P<part2>\w*)’, ‘…aaa/bbb/ccc]’).groups() (‘aaa’, ‘bbb’)
>>> re.search(‘(?P<part1>\w*)/(?P<part2>\w*)’, ‘…aaa/bbb/ccc]’).groupdict() {‘part1’: ‘aaa’, ‘part2’: ‘bbb’}
>>> re.search(‘(?P<part1>\w*)/(?P<part2>\w*)’, ‘…aaa/bbb/ccc]’).group(2) ‘bbb’
>>> re.search(‘(?P<part1>\w*)/(?P<part2>\w*)’, ‘…aaa/bbb/ccc]’).group(‘part2’)
‘bbb’
>>> re.findall(‘(?P<part1>\w*)/(?P<part2>\w*)’, ‘…aaa/bbb ccc/ddd]’) [(‘aaa’, ‘bbb’), (‘ccc’, ‘ddd’)]
Наконец, несмотря на то, что базовых операций, таких как извлечение среза и разбиение, часто бывает вполне достаточно, шаблоны позволяют получить более гибкое решение. В следующем примере используется конструкция [" ], соответствующая любому символу, отличному от символа, следующего за ", и выполняется экранирование дефиса в наборе альтернатив внутри [] с использованием \- , благодаря чему он не воспринимается как разделитель в определении диапазона символов. Здесь выполняются эквивалентные операции извлечения срезов, разбиения и сопоставления, а также более обобщенная операция сопоставления, решающая задачу, непосильную для других двух способов:
>>> line = ‘aaa bbb ccc’
> >> line[:3], line[4:7], line[8:11] # срез извлекается
(‘aaa‘, ‘bbb‘, ‘ccc‘) # по фиксированным смещениям
> >> line.split() # разбиение выполняется по фиксированным
[‘aaa’, ‘bbb’, ‘ccc’] # разделителям
> >> re.split(‘ +’, line) # обобщенное разбиение по разделителям
[‘aaa’, ‘bbb’, ‘ccc’]
> >> re.findall(‘[" ]+’, line) # поиск строк, не являющихся
# разделителями
[‘aaa‘, ‘bbb‘, ‘ccc‘]
>>> line = ‘aaa…bbb—ccc / ddd.-/e&e*e‘ # обработка обобщенного текста
>>> re.findall(‘[“ .\-/]+’, line)
[‘aaa’, ‘bbb’, ‘ccc’, ‘ddd’, ‘e&e*e’]
Если в прошлом вам не приходилось использовать регулярные выражения, от представленных примеров ваша голова может пойти кругом (или вообще отключиться!). Поэтому, прежде чем перейти к другим примерам, познакомимся с некоторыми подробностями, лежащими в основе модуля re и его шаблонов регулярных выражений.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011