Регулярное выражение

Регулярное выражение — шаблон, который может быть весьма сложным и который используется для сопоставления со строками, бинарниками. Синоним: regular expression.

Раньше в стандартной поставке Эрланга за регулярные выражения отвечал модуль regexp, сейчас ему на смену пришёл модуль re (regexp удалён).

Регулярные выражения — очень мощный и иногда практически незаменимый инструмент для работы с текстом.

Упражняться в регулярных выражениях удобно в оболочке.

Как найти в одном куске текста другой кусок

Если нужен простой поиск (найти одну строку в другой), то проще, пожалуй, использовать string:find, а не регулярные выражения.

1> string:find("To be, or not to be, that is the question.", "be").
"be, or not to be, that is the question."
2> string:find("To be, or not to be, that is the question.", "bee").
nomatch

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

1> re:run("To be, or not to be, that is the question.", "be").
{match,[{3,2}]}
2> re:run("To be, or not to be, that is the question.", "bee").
nomatch
3> re:run(<<"To be, or not to be, that is the question.">>, "be").
{match,[{3,2}]}

Следует учесть, что поиск с помощью string:find в несколько раз быстрее и по умолчанию ищет по юникоду, нежели re:run. Зато поиск по регулярным выражениям более универсален.

Из модуля re мы запускаем функцию run/2 с двумя аргументами: строка, в которой мы ищем, и строка, которую ищем. Когда совпадение найдено, мы получаем кортеж, в котором два элемента: match (совпадение) и список. В списке, очевидно, могут быть отражены все найденные совпадения, но в данном случае было найдено только первое.

Совпадение отображается как кортеж, состоящий из двух чисел. Первое число — позиция в строке, в которой мы ищем. Буква Т находится на позиции 0, а буква b из слова be — на позиции 3. Второе число — длина шаблона. В данном случае она равна двум.

Если совпадения не найдены, как во втором примере, функция re:run/2 возвращает просто nomatch.

Функция re:run/2 также позволяет искать в бинарнике. Об этом свидетельствует третий пример.

Как найти все совпадения

В предыдущем примере мы заметили, что похоже на то, что список мог бы содержать не одно найденное совпадение, а несколько. Действительно, так можно сделать, но для этого придётся уже применять функцию re:run/3. Третьим аргументом у неё будут опции, с помощью которых мы можем менять “поведение” функции. Сейчас нам пригодится опция global.

4> re:run("To be, or not to be, that is the question.", "be", [global]).
{match,[[{3,2}],[{17,2}]]}

Как мы видим, опции задаются через список. В данном случае в нём только одна опция.

Функция re:run/3 сработала вроде бы ожидаемым образом. Но возникла одна странность. Раньше возвращался список с одним кортежем. Логично было бы ожидать, что теперь выведется список из двух кортежей. Но это не так. Мы получили список, в котором два списка, каждый из которых содержит кортеж.

Как сделать замену в тексте

Для этих целей служит функция re:replace/4.

1> re:replace("To be, or not to be, that is the question.", "be", "beer", [{return,list},global]).
"To beer, or not to beer, that is the question."

Первый аргумент здесь — исходная строка. Второй — тот шаблон, который мы ищем в исходной строке. Третий аргумент — строка, на которую меняем. Четвёртый — опции. Тут список из двух элементов. Второй нам уже знаком — global отвечает за то, чтобы замена произошла по всему тексту. А кортеж {return,list} определяет, как будет выводиться полученная строка (в данном случае в виде списка).

Юникод и кириллица

Если мы попробуем найти что-нибудь в кириллическом тексте (юникод), то получим ошибку:

1> re:run("Быть или не быть, вот в чём вопрос.", "быть", []).
** exception error: bad argument
     in function  re:run/3
        called as re:run([1041,1099,1090,1100,32,1080,1083,1080,32,1085,1077,
                          32,1073,1099,1090,1100,44,32,1074,1086,1090,32,
                          1074,32,1095,1105,1084,32|...],
                         [1073,1099,1090,1100],
                         [])
        *** argument 1: not an iodata term
        *** argument 2: neither an iodata term nor a compiled regular expression

По умолчанию эта функция ожидает, что будет иодата, то есть символы с номерами до 255. У нас же кириллица, и номера символов значительно больше. Чтобы run работала с кириллицей, надо задать параметр — атом unicode.

2> re:run("Быть или не быть, вот в чём вопрос.", "ыть", [unicode]).
{match,[{2,6}]}

Кириллические буквы занимают два байта, поэтому в результате мы получили двойку. 2 — это количество байтов, после которых был найден шаблон. Буква “Б”, как и другие русские буквы, занимает два байта. Проверим это ещё на другом примере:

3> re:run("Быть или не быть, вот в чём вопрос.", "\s.", [unicode]).
{match,[{8,3}]}

Здесь у нас уже полноценное регулярное выражение, включающее специальные символы. Оно ищет пробел, после которого идёт какой-нибудь другой символ. После “Быть” как раз и есть такой пробел. Это слово занимает 4x2=8 байтов. Отсюда и взялась восьмёрка.

Синтаксис регулярных выражений

Алгоритмы сопоставления основаны на известной Си-библиотеке PCRE, которая в свою очередь вдохновилась синтаксисом регулярных выражений, взятым из языка Perl. Синтаксис довольно сложный, с нюансами, можно долго осваивать искусство составление регулярных выражений, но оно того стоит, потому что часто это очень сокращает код программы.

Большинство символов, если попадают в регулярное выражение, обозначают сами себя. Например, буквы и цифры. Но есть ещё метасимволы, которые по-разному меняют смысл регулярного выражения. Метасимволы делятся на две категории: используемые внутри квадратных скобок и снаружи.

Квадратные скобки определяют некоторое множество символов. Например, множество [оы] состоит из двух русских букв “о” и “ы”. Программа будет считать, что сопоставление прошло удачно, если встретится одна из этих двух букв:

4> re:run("Быть или не быть, вот в чём вопрос.", "[оы]т", [unicode, global]).
{match,[[{2,4}],[{23,4}],[{33,4}]]}

В этом примере программа нашла три участка, которые её удовлетворили: “ыт”, “ыт” и “от”.

Метасимвол ^, например, внутри квадратных скобок и снаружи обладает разным “поведением”. Снаружи он обозначает самое начало строки. Внутри — отрицание множества.

5> re:run("Быть или не быть, вот в чём вопрос.", "[^оы]т", [unicode, global]).
nomatch

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

Метасимволы

Метасимволы, применяемые внутри квадратных скобок:

Метасимволы, применяемые вне квадратных скобок:

Специальные символы

Для обозначения непечатных символов есть следующее:

Символы, обозначающие тот или иной тип:

Сложная замена с применением фунтермов

Для замены с применением регулярных выражений используется re:replace/4. Есть ещё вариант re:replace/3, но обычно нужно указывать опции, которые идут четвёртым аргументом.

re:replace(Subject, RE, Replacement, Options)

Вторым идёт регулярное выражение, третьим — на что меняем. Этот третий аргумент может быть иодатой, строкой юникода или же фунтермом. Конечно, интереснее всего третий вариант, потому что он максимально гибок.

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

Допустим, наша программа должна обрабатывать строки такого рода: "Миша на 23 февраля съел 1+3 яблока, а Маша на 8 марта съела 12 + 5 абрикосов.". Мы должны вычленять участки текста вроде 1+3 и 12 + 5, суммировать два найденных таким образом числа и подставлять полученное значение в текст.

Для поиска в строке будем использовать шаблон "(\\d+)\s*\\+\s*(\\d+)". Он находит пары чисел, между которыми стоит плюс. Рядом с плюсом пробел(ы) могут стоять, а могут и не стоять. Круглые скобки нужны для того, чтобы захватить оба найденных числа и передать их в фунтерм.

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

Funterm = fun(Raz, Dva) ->
                  io:format("Raz: ~p~n", [Raz]),
                  io:format("Dva: ~p~n", [Dva]),
                  <<"0">> end,

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

Raz: <<"1+3">>
Dva: [<<"1">>,<<"3">>]
Raz: <<"12 + 5">>
Dva: [<<"12">>,<<"5">>]
"Миша на 23 февраля съел 0 яблока, а Маша на 8 марта съела 0 абрикосов."

Конечно, фунтерм не имеет доступа ко всей длинной строке про Мишу и Машу. Первым аргументом он получает весь найденный участок, например “1+3”. Во втором аргументе он получает список из захваченных с помощью круглых скобок участков. Дальше дело техники. Нам надо в фунтерме из бинарников извлечь два числа, сложить их и вернуть — опять же в виде бинарника.

Итог выглядит так:

main() ->
    Stroka = "Миша на 23 февраля съел 1+3 яблока, а Маша на 8 марта съела 12 + 5 абрикосов.",
    Funterm = fun(_, [A,B]) ->
                      Aa = list_to_integer(binary_to_list(A)),
                      Bb = list_to_integer(binary_to_list(B)),
                      integer_to_binary(Aa + Bb)
              end,
    re:replace(Stroka, "(\\d+)\s*\\+\s*(\\d+)", Funterm, [{return, list}, global, unicode]).

"Миша на 23 февраля съел 4 яблока, а Маша на 8 марта съела 17 абрикосов."

Если мы уберём опцию {return, list}, тогда получим не строку, а список из нескольких бинарников. Если уберём опцию unicode, то получим ошибку, связанную с тем, что функция ожидает иодату, но получила строку юникода. Опция global нужна для того, чтобы замена прошла везде, а не только в одном первом попавшемся месте.

Как использовать переменную в регулярном выражении?

Регулярное выражение это строка. Часто оно задаётся как строковый литерал, например " .. ". Но эта строка, которая будет интерпретирована функцией как регулярное выражение, может быть сформирована во время исполнения программы. Поэтому, если мы хотим использовать в регулярном выражении какую-либо переменную, для этого можно воспользоваться оператором ++. С его помощью мы соединим литерал (известный на этапе написания программы) с переменной (которая заранее может быть не известна).

main() ->
    Stroka = "To be, or not to be, that is the question.",
    Bukva = "b",
    re:run(Stroka, " .. " ++ Bukva).

{match,[{13,5}]}

Документация

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


© Алексей Карманов, 2024.