Литералы и символьный класс

В языке Python имеется весьма полезный встроенный модуль re для обработки строк с помощью регулярных выражений. Для тех кто совсем не знает что это такое, приведу следующий простой пример:

import re
 
text = "Карта map и объект bitmap - это разные вещи"
 
match = re.findall("map", text)
print(match)

Результатом работы этой программы будет коллекция из двух найденных подстрок «map»:

['map', 'map']

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

match = re.findall("\\bmap\\b", text)

Здесь записаны два подряд слеша, чтобы итоговая строка стала такой:

"\bmap\b"

И, так как в обратные слеши приходится часто записывать в регулярных выражениях, то удобнее перед строкой поставить бувку ‘r’, которая указывает Python не преобразовывать строку, а брать такой, какой она записана:

match = re.findall(r"\bmap\b", text)

Запустим программу и увидим только одно вхождение map, которое соответствует первому слову. Видите, умея составлять шаблоны для регулярных выражений, можно с легкостью реализовывать самую разную обработку строк. В этом их удобство и в рамках языка Python еще и скорость работы, т.к. сам модуль re написан на языке Си, что обеспечивает высокую скорость обработки строк.

В практике программирования регулярные выражения используются для решения следующих задач:

  • проверка фрагмента текста заданному шаблону (например, формат записи номера телефона);
  • поиск подстрок по указанному шаблону в тексте;
  • поиск и замена регулярного выражения на заданную строку;
  • разбиение строки по найденным шаблонам, записанного в виде регулярного выражения (например, данные записанные в строке как ключ=значение разбиваются отдельно на ключи и значения).

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

Язык регулярных выражений

Этот язык является универсальным и не привязан к конкретному языку программирования. То есть, изучив его, вы сможете использовать регулярные выражения и в Perl и в JavaScript и в Java и т.д. Многие современные языки программирования поддерживают возможность обработки строк с помощью таких шаблонов.

Простейшее регулярное выражение – это обычные литералы символов, такие как a или 5. Если их просто записать в шаблоне:

text = "еда, беда, победа"
match = re.findall(r"еда", text)

то будут найдены все вхождения, где встречается данная комбинация символов:

"еда, беда, победа"

Алгоритм поиска работает так: находится один символ e, за которым следует один символ д, за которым следует один символ а. Однако, не все символы можно записывать в явном виде. Есть специальные символы, которые относятся к языку регулярных выражений. Они следующие:

\.^$?+*{}[]()|

Например, если взять слово «еда» в круглые скобки:

match = re.findall(r"(еда)", text)

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

match = re.findall(r"\(еда\)", text)

и тогда будет искаться подстрока «(еда)», которой у нас нет, поэтому получим пустую коллекцию. Но вот так:

text = "(еда), беда, победа"
match = re.findall(r"\(еда\)", text)

получим одно вхождение. Также в пределах языка Python можно использовать большинство стандартных символов строк:

\n, \t

или же записывать символы в виде шестнадцатиричных кодов в форматах:

\xHH, \uHHHHи \UHHHHHHHH

Символьный класс

Часто требуется найти не одно какое-то конкретное написание слова, а все его множественные формы, например:

еда, еду, Еда, Еду

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

[набор символов]

В нашем случае его можно использовать так:

text = "Еда, беду, победа"
match = re.findall(r"[еЕ]д[ау]", text)

Мы здесь в первых квадратных скобках указываем, что первым символом может быть е или Е. Далее, должен идти символ д. А в конце стоять символ а или у. Благодаря использованию символьного класса будут найдены все комбинации:

['Еда', 'еду', 'еда']

Или, например, мы хотим определить наличие одной (любой) цифры в нашем тексте. Для этого можно записать вот такой символьный класс:

text = "Еда, беду, 5 победа"
match = re.findall(r"[0123456789]", text)

и будет найдена цифра 5. Но, для удобства, в символьном классе можно указывать диапазон значений:

match = re.findall(r"[0-9]", text)

Результат будет тот же. Если нужно инвертировать последовательность символов в символьном классе, то используется символ ^:

match = re.findall(r"[^0-9]", text)

Данный шаблон означает поиск любого нецифрового символа. Результатом будет последовательность:

['Е', 'д', 'а', ',', ' ', 'б', 'е', 'д', 'у', ',', ' ', ' ', 'п', 'о', 'б', 'е', 'д', 'а']

Причем, обратите внимание, если символ дефиса ‘-‘ записать первым в символьном классе:

text = "Еда, беду, из-за, победа"
match = re.findall(r"[-0-9]", text)

то он будет соответствовать просто этому символу.

По аналогии можно задавать интервал и для символов, например, так:

match = re.findall(r"[а-я]", text)

на выходе получим найденные малые символы:

['д', 'а', 'б', 'е', 'д', 'у', 'и', 'з', 'з', 'а', 'п', 'о', 'б', 'е', 'д', 'а']

Или записывать несколько диапазонов:

match = re.findall(r"[а-яА-Я0-9]", text)

Будут находиться все буквы русского алфавита и цифры. Также внутри символьного класса все специальные символы (кроме обратного слеша ‘\’) теряют свое значение и воспринимаются как обычные символы:

text = "(еда), еда, победа"
match = re.findall(r"[(]еда[)]", text)

Здесь будет искаться строка «(еда)», так как круглые скобки записаны в символьном классе. Но два символа: ^ и - принимают новое значение, как мы только что видели.

Некоторые наборы символов, например, [0-9] или [^0-9] и другие довольно часто используются на практике, поэтому им были назначены специальные краткие формы:

Символ

Значение

.

Соответствует любому символу, кроме символа переноса строки (‘\n’). Но, если установлен флаг re.DOTALL, то точка соответствует вообще любому символу в тексте. Однако, если она записана внутри символьного класса [.], то воспринимается как символ точки.

\d

Соответствует любой цифре, если используется кодировка Юникода. Если же установлен флаг re.ASCII, то диапазону цифр [0-9].

\D

Соответствует любому не цифровому символу для Юникода или символьному классу [^0-9] при установленном флаге re.ASCII

\s

Для Юникода – любой пробельный символ. Для re.ASCII – символьному классу [ \t\n\r\f\v]

\S

Для Юникода – любой не пробельный символ. Для re.ASCII – символьному классу [^ \t\n\r\f\v]

\w

Для Юникода – любой символ слова. При флаге re.ASCII – набору символов [a-zA-Z0-9_]

\W

Для Юникода – любой не символ слова. При флаге re.ASCII – набору символов [^a-zA-Z0-9_]

Например, если записать вот такой шаблон:

text = "(еда), еда, победа"
match = re.findall(r".", text)

то получим список из всех символов строки text. Если записать так:

match = re.findall(r"\w", text)

то только символы слов:

['е', 'д', 'а', 'е', 'д', 'а', 'п', 'о', 'б', 'е', 'д', 'а']

Но, если указать флаг re.ASCII:

match = re.findall(r"\w", text, re.ASCII)

то получим пустой список, т.к. символьный класс в этом случае будет иметь вид [a-zA-Z0-9_] и в нем нет символов русского алфавита. Также специальные наборы можно записывать и внутри символьных классов, например, так:

text = "0xf, 0xa, 0x5"
match = re.findall(r"0x[\da-fA-F]")

Здесь мы выделяем шестнадцатиричные числа из строки.

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