На этом шаге мы закончим рассматривать синтаксис регулярных выражений.
Поддерживаются также два метасимвола, позволяющие указать привязку к началу или концу слова:
Рассмотрим несколько примеров:
>>> p = re.compile(r"\bpython\b") >>> print("Найдено" if p.search("python") else "Нет") Найдено >>> print("Найдено" if p.search("pythonware") else "Нет") Нет >>> p = re.compile(r"\Bth\B") >>> print("Найдено" if p.search("python") else "Нет") Найдено >>> print("Найдено" if p.search("this") else "Нет") Нет
Значение в скобках инвертируется, если после первой скобки вставить символ ^. Таким образом можно указать символы, которых не должно быть на этом месте в строке:
Как вы уже знаете, точка теряет свое специальное значение, если ее заключить в квадратные скобки. Кроме того, внутри квадратных скобок могут встретиться символы, которые имеют специальное значение (например, ^ и -). Символ теряет свое специальное значение, если он не расположен сразу после открывающей квадратной скобки. Чтобы отменить специальное значение символа -, его необходимо указать после перечисления всех символов, перед закрывающей квадратной скобкой или сразу после открывающей квадратной скобки. Все специальные символы можно сделать обычными, если перед ними указать символ \.
Метасимвол | позволяет сделать выбор между альтернативными значениями. Выражение n | m соответствует одному из символов: n или m. Пример:
>>> p = re.compile(r"красн((ая)|(ое))") >>> print("Найдено" if p.search("красная") else "Нет") Найдено >>> print("Найдено" if p.search("красный") else "Нет") Нет
Вместо перечисления символов можно использовать стандартные классы:
Количество вхождений символа в строку задается с помощью квантификаторов:
Все квантификаторы являются "жадными". При поиске соответствия ищется самая длинная подстрока, соответствующая шаблону, и не учитываются более короткие соответствия. Рассмотрим это на примере и получим содержимое всех тегов <b>, вместе с тегами:
>>> s = "<b>Text1</b>Text2<b>Text3</b>" >>> p = re.compile(r"<b>.*</b>", re.S) >>> p.findall(s) ['<b>Text1</b>Text2<b>Text3</b>']
Вместо желаемого результата мы получили полностью строку. Чтобы ограничить "жадность", необходимо после квантификатора указать символ ?:
>>> s = "<b>Text1</b>Text2<b>Text3</b>" >>> p = re.compile(r"<b>.*?</b>", re.S) >>> p.findall(s) ['<b>Text1</b>', '<b>Text3</b>']
Этот код вывел то, что мы искали. Если необходимо получить содержимое без тегов, то нужный фрагмент внутри шаблона следует разместить внутри круглых скобок:
>>> s = "<b>Text1</b>Text2<b>Text3</b>" >>> p = re.compile(r"<b>(.*?)</b>", re.S) >>> p.findall(s) ['Text1', 'Text3']
Круглые скобки часто используются для группировки фрагментов внутри шаблона. В этом случае не требуется, чтобы фрагмент запоминался и был доступен в результатах поиска. Чтобы избежать захвата фрагмента, следует после открывающей круглой скобки разместить символы ?:.
>>> s = "test text" >>> p = re.compile(r"([a-z]+((st)|(xt)))", re.S) >>> p.findall(s) [('test', 'st', 'st', ''), ('text', 'xt', '', 'xt')] >>> p = re.compile(r"([a-z]+(?:(?:st)|(?:xt)))", re.S) >>> p.findall(s) ['test', 'text']
В первом примере мы получили список с двумя элементами. Каждый элемент списка является кортежем, содержащим четыре элемента. Все эти элементы соответствуют фрагментам, заключенным в шаблоне в круглые скобки. Первый элемент кортежа содержит фрагмент, расположенный в первых круглых скобках, второй - во вторых круглых скобках и т. д. Три последних элемента кортежа являются лишними. Чтобы они не выводились в результатах, мы добавили символы ?: после каждой открывающей круглой скобки. В результате список состоит только из фрагментов, полностью соответствующих регулярному выражению.
К найденному фрагменту в круглых скобках внутри шаблона можно обратиться с помощью механизма обратных ссылок. Для этого порядковый номер круглых скобок в шаблоне указывается после слеша - например, так: \1. Нумерация скобок внутри шаблона начинается с 1. Для примера получим текст между одинаковыми парными тегами:
>>> s = "<b>Text1</b>Text2<I>Text3</I><b>Text4</b>" >>> p = re.compile(r"<([a-z]+)>(.*?)</\1>", re.S | re.I) >>> p.findall(s) [('b', 'Text1'), ('I', 'Text3'), ('b', 'Text4')]
Фрагментам внутри круглых скобок можно дать имена. Для этого после открывающей круглой скобки следует указать комбинацию символов ?P<name>. В качестве примера разберем e-mail на составные части:
>>> email = "test@mail.ru" >>> p = re.compile(r"""(?P<name>[a-z0-9_.-]+) # Название ящика @ # Символ "@" (?P<host>(?:[a-z0-9-]+\.)+[a-z]{2,6}) # Домен """, re.I | re.VERBOSE) >>> r = p.search(email) >>> r.group('name') 'test' >>> r.group('host') 'mail.ru'
Чтобы внутри шаблона обратиться к именованным фрагментам, используется следующий синтаксис: (?P=name). Для примера получим текст между одинаковыми парными тегами:
>>> s = "<b>Text1</b>Text2<b>Text3</b>" >>> p = re.compile(r"<(?P<tag>[a-z]+)>(.*?)</(?P=tag)>", re.I | re.S) >>> p.findall(s) [('b', 'Text1'), ('b', 'Text3')]
Кроме того, внутри круглых скобок могут быть расположены следующие конструкции:
>>> s = "text1, text2, text3 text4" >>> p = re.compile(r"\w+(?=[,])", re.I | re.S) >>> p.findall(s) ['text1', 'text2']
>>> s = "text1, text2, text3 text4" >>> p = re.compile(r"[a-z]+[0-9](?![,])", re.I | re.S) >>> p.findall(s) ['text3', 'text4']
>>> s = "text1, text2, text3 text4" >>> p = re.compile(r"(?<=[,][ ])[a-z]+[0-9]", re.I | re.S) >>> p.findall(s) ['text2', 'text3']
>>> s = "text1, text2, text3 text4" >>> p = re.compile(r"(?<![,]) ([a-z]+[0-9])", re.I | re.S) >>> p.findall(s) ['text4']
>>> s = "text1 'text2' 'text3 text4, text5" >>> p = re.compile(r"(')?([a-z]+[0-9])(?(1)'|,)", re.I | re.S) >>> p.findall(s) [("'", 'text2'), ('', 'text4')]
Рассмотрим небольшой пример. Предположим, необходимо получить все слова, расположенные после дефиса, причем перед дефисом и после слов должны следовать пробельные символы:
>>> s = "-word1 -word2 -word3 -word4 -word5" >>> re.findall(r"\s\-([a-z0-9]+)\s", s, re.S | re.I) ['word2', 'word4']
Как видно из примера, мы получили только два слова вместо пяти. Первое и последнее слова не попали в результат, т. к. расположены в начале и в конце строки. Чтобы эти слова попали в результат, необходимо добавить альтернативный выбор (^|\s); - для начала строки и (\s|$) - для конца строки. Чтобы найденные выражения внутри круглых скобок не попали в результат, следует добавить символы ?: после открывающей скобки:
>>> re.findall(r"(?:^|\s)\-([a-z0-9]+)(?:\s|$)", s, re.S | re.I) ['word1', 'word3', 'word5']
Первое и последнее слова успешно попали в результат. Почему же слова word2 и word4 не попали в список совпадений - ведь перед дефисом есть пробел и после слова есть пробел? Чтобы понять причину, рассмотрим поиск по шагам. Первое слово успешно попадает в результат, т. к. перед дефисом расположено начало строки, и после слова есть пробел. После поиска указатель перемещается, и строка для дальнейшего поиска примет следующий вид:
"-word1 <Указатель>-word2 -word3 -word4 -word5"
Обратите внимание на то, что перед фрагментом -word2 больше нет пробела, и дефис не расположен в начале строки. Поэтому следующим совпадением будет слово word3 и указатель снова будет перемещен:
"-word1 -word2 -word3 <Указатель>-word4 -word5"
Опять перед фрагментом -word4 нет пробела, и дефис не расположен в начале строки. Поэтому следующим совпадением будет слово word5, и поиск будет завершен. Таким образом, слова word2 и word4 не попадают в результат, поскольку пробел до фрагмента уже был использован в предыдущем поиске. Чтобы этого избежать, следует воспользоваться положительным просмотром вперед (?=...):
>>> re.findall(r"(?:^|\s)\-([a-z0-9]+)(?=\s|$)", s, re.S | re.I) ['word1', 'word2', 'word3', 'word4', 'word5']
В этом примере мы заменили фрагмент (?:\s|$) на (?=\s|$). Поэтому все слова успешно попали в список совпадений.
На следующем шаге мы рассмотрим поиск первого совпадения с шаблоном.