2.36. Группы и якоря

Шаблон может не только сообщать «эта строка соответствует» — он способен разбирать совпавшие части и передавать каждую из них приложению по имени. Круглые скобки вокруг части шаблона делают её захватывающей группой; объект совпадения затем предоставляет каждую группу как отдельную подстроку.

2.36.1. Захватывающие группы

Заключите любую часть шаблона в (...), чтобы захватить то, чему она соответствовала:

>>> import re
>>> m = re.search(r'temp (\d+) at (\d+)s', 'temp 42 at 137s ok')
>>> m.group(0)
'temp 42 at 137s'
>>> m.group(1)
'42'
>>> m.group(2)
'137'
  • Группа 0 — это всегда всё совпадение целиком.

  • Группы 1, 2, … — это захваченные подстроки, нумеруемые слева направо по их открывающей скобке.

  • Вызов match.group() с индексом, выходящим за пределы последней группы, вызывает IndexError.

Распространённый приём — «сопоставить известную структуру, захватив переменные части как целые числа»:

def parse_temp(line):
    m = re.search(r'temp (\d+) at (\d+)s', line)
    if not m:
        return None
    return int(m.group(1)), int(m.group(2))

2.36.2. Незахватывающие группы

Круглые скобки также группируют подвыражение, чтобы квантификатор мог применяться ко всей группе. Это единственное назначение группировки в r'(ab)+' — «один или более ab». То, что ab появляется как группа 1, — это побочный эффект.

Чтобы сгруппировать без захвата, используйте (?:...):

>>> re.search(r'(?:ab)+', 'xababy').group(0)
'abab'

Незахватывающие группы позволяют сохранять номера групп упорядоченными, когда шаблон использует группировку для структуры, но не нуждается в извлечении каждой части по отдельности.

2.36.3. Якоря

Якоря не сопоставляются с символом — они сопоставляются с позицией.

  • ^ — начало строки.

  • $ — конец строки.

Именно якоря заставляют re.match() и re.search() вести себя по-разному. re.match(p, s) — это то же самое, что re.search('^' + p, s): оно вынуждает шаблон начинаться с позиции 0. Добавление $ в конец шаблона затем заставляет шаблон соответствовать всей строке и ничему более:

>>> re.search(r'^\d+$', '12345')
<match num=1>
>>> re.search(r'^\d+$', '12345 ok') is None
True

^ и $ в MicroPython re всегда означают начало и конец всей строки, переданной в re.search(). Здесь нет флага re.MULTILINE, который заставил бы их совпадать на каждом встроенном переводе строки, а $ также не совпадает с позицией перед завершающим \n — это должен быть абсолютный конец входных данных. Чтобы получить построчное поведение, сначала разделите входные данные по переводам строк и запустите шаблон на каждой строке.

2.36.4. Наборы символов

Квадратные скобки определяют явный набор символов. Совпадение поглощает ровно один символ из набора.

  • [abc] — один из a, b, c.

  • [a-z] — один символ в диапазоне a-z (включительно).

  • [a-zA-Z0-9] — буквы или цифры. Три объединённых диапазона.

  • [^abc]не один из a, b, c. ^ отрицает, только когда он является первым символом внутри скобок.

Примеры:

>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest').group(0)
'1A2B3C'
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest') is None
True

Первый вызов на практике возвращает None, потому что литеральный текст записан в нижнем регистре. В MicroPython re нет флага re.IGNORECASE — чтобы выполнять сопоставление без учёта регистра, впишите оба варианта регистра в набор:

>>> re.search(r'[A-Fa-f0-9]{6}', 'colour #1a2b3c rest').group(0)
'1a2b3c'

Сокращения для классов (\d, \s, \w и их отрицания) можно использовать и внутри [...]: [\w-] означает «символы слова или литеральный дефис».

2.36.5. Жадные и ленивые квантификаторы

Квантификаторы *, +, ? и {m,n} по умолчанию жадные — они сопоставляются с как можно большим количеством символов, насколько это ещё позволяет остальная часть шаблона. Часто это именно то, что нужно; иногда нет:

>>> re.search(r'<(.+)>', 'a <b> <c> d').group(1)
'b> <c'

Жадный .+ захватил всё вплоть до последнего >. Добавление ? делает квантификатор ленивым — он сопоставляется с как можно меньшим количеством:

>>> re.search(r'<(.+?)>', 'a <b> <c> d').group(1)
'b'

Ленивая форма останавливается на первом >. Ленивые квантификаторы постоянно возникают при извлечении сбалансированных разделителей из строки.

2.36.6. Обратные ссылки в подстановке

re.sub() может ссылаться на захваченные группы в строке замены через \1, \2, … Подстановка переписывает каждое совпадение, используя захваченные части:

>>> re.sub(r'(\d+)\.(\d+)', r'\2.\1', 'swap 12.34 and 5.6')
'swap 34.12 and 6.5'

Каждое совпадение захватывает два числа, и замена меняет их местами. \g<1> — альтернативный синтаксис для того же самого — полезен, когда следующий символ в замене является цифрой (r'\g<1>0' для добавления литерального нуля к группе 1, а не для чтения «группы 10»).

2.36.7. Что недоступно

Напоминание о том, что MicroPython re не поддерживает — на случай, если сюда попадёт шаблон из CPython и удивит вас:

  • Опережающая проверка (?=...) и ретроспективная проверка (?<=...) — не реализованы.

  • Именованные группы (?P<name>...) и именованные обратные ссылки (?P=name) — не реализованы.

  • Константы флагов вроде re.IGNORECASE, re.MULTILINE, re.DOTALL — не учитываются. Создавайте набор без учёта регистра или предварительно разделяйте входные данные самостоятельно.

  • Методы match.groups(), match.span(), match.start() и match.end() ограничены уровнем ROM, который не включён ни на одной поставляемой плате OpenMV. Код, опирающийся на них, не будет работать на камере.

С шаблонами, группами и якорями набор инструментов регулярных выражений на камере достаточно мал, чтобы освоить его за один присест, и достаточно богат, чтобы делать всё, кроме контекстно-зависимого разбора.