2.35. Основы шаблонов

Регулярное выражение — это небольшой язык для описания строк по их форме, а не по точному содержимому. Используйте его, когда строка соответствует тогда и только тогда, когда она следует шаблону, который вы можете описать, но не можете перечислить — «последовательность цифр, за которой следует единица измерения», «строка, начинающаяся с ERROR и заканчивающаяся числом», «любое из этих расширений файлов в любом порядке с необязательными префиксами v».

Обращайтесь к re только тогда, когда обычный строковый метод не подходит.

  • str.startswith(), str.endswith() — проверка фиксированного префикса или суффикса.

  • in — проверка наличия фиксированной подстроки.

  • str.split(), str.find(), str.replace() — работа с фиксированными разделителями.

Каждый из них быстрее, легче читается и менее подвержен ошибкам, чем эквивалентное регулярное выражение. Используйте регулярные выражения, когда важна форма строки, а точная подстрока — нет.

2.35.1. Четыре вещи, которыми вы будете пользоваться

Модуль MicroPython re предоставляет четыре вещи:

  • re.compile() — превратить строку шаблона в скомпилированный объект шаблона, который можно использовать повторно.

  • re.match() — попробовать шаблон в начале строки. Шаблон привязан к позиции 0.

  • re.search() — попробовать шаблон в любом месте строки. Возвращает первое совпадение.

  • re.sub() — найти каждое совпадение и заменить его.

Заметные отличия от CPython: нет re.findall, нет re.finditer, нет re.split на уровне модуля (вместо этого у скомпилированных шаблонов есть метод split), нет re.fullmatch, нет констант флагов вроде re.IGNORECASE. Там, где в CPython вы обратились бы к одной из них, постройте эквивалент из re.search() в цикле.

2.35.2. Первый шаблон

Шаблон r'\d+' соответствует одной или более цифрам:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

На что стоит обратить внимание:

  • Шаблон записан как сырая строка (r'...'), чтобы обратная косая черта в \d достигла re, а не была обработана как escape-последовательность строки Python. Всегда используйте сырые строки для шаблонов регулярных выражений.

  • re.search() возвращает объект совпадения при успехе и None при неудаче. Всегда проверяйте результат перед вызовом match.group().

  • m.group(0) — это полный текст, которому соответствовал шаблон. Группы 1, 2, … появляются позже, когда шаблон содержит захватывающие круглые скобки.

Тот же шаблон с re.match() возвращает None, потому что строка не начинается с цифры:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. Составные части шаблона

Большинство полезных шаблонов строятся из небольшого набора составных частей. Те, что работают в MicroPython:

Литеральные символы — любой символ, не являющийся специальным, соответствует самому себе. hello соответствует hello.

Специальные символы. ^ $ * + ? { } [ ] \ | ( ) все имеют значения, описанные ниже. Чтобы сопоставить один из них буквально, экранируйте его обратной косой чертой: \. соответствует литеральной точке.

Классы символов — сокращения для распространённых наборов символов:

  • \d — любая цифра 0-9

  • \D — любой не-цифровой символ

  • \s — любой пробельный символ (пробел, табуляция, перевод строки)

  • \S — любой непробельный символ

  • \w — любой символ «слова»: буквы, цифры, подчёркивание

  • \W — любой символ, не являющийся символом слова

  • . — любой символ, кроме перевода строки

Квантификаторы — сколько раз должна совпасть предыдущая часть:

  • * — ноль или более (жадный)

  • + — один или более (жадный)

  • ? — ноль или один

  • {n} — ровно n

  • {m,n} — от m до n (включительно)

Комбинирование: \d{3}-\d{4} соответствует трём цифрам, дефису, четырём цифрам. \s+ соответствует одному или более пробельным символам. hello.*world соответствует hello, чему угодно (включая ничего), затем world.

Примечание

Жадный означает, что квантификатор поглощает столько входных данных, сколько может, при этом по-прежнему позволяя совпасть остальной части шаблона. Против hello x world y world .* в hello.*world соответствует самой длинной последовательности, которая всё ещё оставляет world в конце — он захватывает x world y, а не более короткое x. То же справедливо для + и диапазонной формы {m,n}: движок берёт самое длинное совпадение, какое может, а затем отступает только если остальная часть шаблона не совпадает.

2.35.4. Подстановка

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

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

Третий аргумент — это строка, над которой выполняется операция; результатом является новая строка с заменённым каждым совпадением.

2.35.5. Разбиение — только на скомпилированном шаблоне

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

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

Необязательный второй аргумент ограничивает число разбиений:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. Компиляция для повторного использования

Если один и тот же шаблон выполняется много раз — внутри цикла или в часто вызываемой функции — скомпилируйте его один раз и используйте скомпилированный объект повторно:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

Вызов pattern.match() и pattern.search() на скомпилированном объекте делает то же, что и функции уровня модуля, но избегает затрат на перекомпиляцию при каждом вызове.

2.35.7. Шаблоны, которые ничему не соответствуют

Три шаблона в особенности ставят разработчиков в тупик:

  • .* соответствует пустой строке. re.search(r'.*', s).group(0) возвращает '' на любых входных данных.

  • Шаблон с неэкранированным специальным символом является синтаксической ошибкой. re.compile(r'cost: $5') вызывает ValueError, потому что $ означает «конец строки». Используйте r'cost: \$5'.

  • Точка . не соответствует переводу строки. Чтобы сопоставлять через переводы строк, напишите шаблон, обрабатывающий их явно с помощью [\s\S], или подавайте по одной строке за раз.

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