Шаг 54.
Основы языка Python.
Регулярные выражения. Синтаксис регулярных выражений (окончание)

    На этом шаге мы закончим рассматривать синтаксис регулярных выражений.

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

    Рассмотрим несколько примеров:

>>> 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 "Нет")
Нет

    Вместо перечисления символов можно использовать стандартные классы:


Замечание.     В Python 3 поддержка Unicode в регулярных выражениях установлена по умолчанию. При этом все классы трактуются гораздо шире. Так, класс \d соответствует не только десятичным цифрам, но и другим цифрам из кодировки Unicode, - например, дробям, класс \w включает не только латинские буквы, но и любые другие, а класс \s охватывает также неразрывные пробелы. Поэтому на практике лучше явно указывать символы внутри квадратных скобок, а не использовать классы.

    Количество вхождений символа в строку задается с помощью квантификаторов:

    Все квантификаторы являются "жадными". При поиске соответствия ищется самая длинная подстрока, соответствующая шаблону, и не учитываются более короткие соответствия. Рассмотрим это на примере и получим содержимое всех тегов <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 = "-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|$). Поэтому все слова успешно попали в список совпадений.

    На следующем шаге мы рассмотрим поиск первого совпадения с шаблоном.




Предыдущий шаг Содержание Следующий шаг