Регулярные выражения в Julia
Регулярные выражения в Julia¶
Введение¶
Регулярные выражения (regex) — это универсальный инструмент для поиска, извлечения и обработки текста по заданным шаблонам. Они позволяют решать задачи вроде проверки формата email, извлечения номеров телефонов или анализа данных из текстов. В языке Julia регулярные выражения особенно удобны благодаря простоте их интеграции и лаконичному синтаксису. Julia не требует дополнительных модулей для работы с regex (в отличие от некоторых других языков) и использует префикс r"..."
для создания шаблонов, что делает код интуитивным и читаемым. Этот материал покажет, как применять регулярные выражения в Julia, с акцентом на практические примеры и особенности языка.
Синтаксис регулярных выражений в Julia¶
Julia использует синтаксис PCRE, который поддерживает богатый набор возможностей. Разберём основные элементы регулярных выражений и их применение в Julia.
Базовые элементы синтаксиса¶
- Литералы: Обычные символы (например,
a
,b
,1
) ищутся в тексте как есть. Например,r"cat"
соответствует словуcat
. - Метасимволы:
.
— любой символ, кроме новой строки. Например,r"c.t"
соответствуетcat
,cot
, но неc\nt
.^
— начало строки:r"^hello"
найдётhello
только в начале строки.$
— конец строки:r"world$"
найдётworld
только в конце строки.\
— экранирование:r"\."
ищет точку как символ, а не любой символ.r"\^"
ищет именно символ^
.
- Квантификаторы:
*
— ноль или более:r"a*"
соответствует""
,a
,aa
и т.д.+
— одно или более:r"a+"
соответствуетa
,aa
, но не""
.?
— ноль или одно:r"colou?r"
соответствуетcolor
иcolour
.{n,m}
— диапазон повторений:r"a{2,4}"
соответствуетaa
,aaa
,aaaa
.
- Классы символов:
[abc]
— один из символов:r"[abc]"
соответствуетa
,b
илиc
.[a-z]
— диапазон:r"[a-z]+"
найдёт любое слово из строчных букв.[^abc]
— отрицание:r"[^abc]"
любой символ, кромеa
,b
,c
.\d
— цифра:r"\d+"
найдёт числа вроде123
.\w
— буква, цифра или_
:r"\w+"
найдёт слова вродеhello123
.\s
— пробельный символ:r"\s+"
найдёт пробелы или табуляции.
- Группировка:
()
— захватывает часть шаблона:r"(\d+)-(\d+)"
выделит из строки12-34
числа12
и34
и сохранит.(?:...)
— группирует, но не "сохранит" результат. Незахватывающая группа полезна для упрощения структуры выражения.
regex = r"(?:abc)+(\d+)(?:def)+(\&+)"
text = "abcabc123defdefdef&&&"
match(regex,text)[1] # вернёт "123"
match(regex,text)[2] # вернёт "&&&"
Функции в Julia¶
Julia предлагает удобные методы для работы с регулярными выражениями:
match(r"шаблон", строка)
: Находит первое совпадение. Возвращает объектRegexMatch
илиnothing
.
Особенность Julia — отсутствие необходимости в двойном экранировании (например,
r"\d"
вместо"\\d"
), что упрощает написание шаблонов.
m = match(r"\d+", "Возраст: 42") #\d+ `\d` - выбери цифру `+` - одно или больше вхождений
println(m.match)
eachmatch(r"шаблон", строка)
: Итератор всех совпадений.
for m in eachmatch(r"\w+", "Здравствуй, дорогой друг!") # `\w` - буква, цифра или _ (`,` и `!` не подходят)
println(m.match)
end
replace(строка, r"шаблон" => "замена")
: Заменяет совпадения.
new = replace("Формат даты: 01-02-2025", r"\d" => "X")
println(new)
occursin(r"шаблон", строка)
: Проверяет наличие совпадения.
@show occursin(r"[A-Z]", "Hello")
@show occursin(r"[A-Z]", "hello")
@show occursin(r"[A-Z]+", "HELLO");
Практические применеие¶
Пример 1: извлечение имени и фамилии¶
Пускай была сделана фотография журнала посетителей консерватории.
Далее мы оцифровали этот документ и выполнили распознавание текста, получив "документ" OCR_text
.
И вышло так, что некоторые буквы стали в нижнем регистре, где-то добавились лишние пробелы, а где-то исчезли. В каких-то случаях росчерк распознался как буква.
# Текст с данными
OCR_text = """
Журнал посетителей:
Фамилия: иванов имя: Иван
фамилия : Петров имя : пётр l
Фамилия - Римский-Корсаков Имя -Николай
"""
Указав флаг:
i
в выраженииr"..."i
- мы указываем независимость от регистра (case-insensitive). То есть "фамилия" и "Фамилия" будут считаться равнозначнымиm
в выраженииr"..."m
означает многострочность.^
в выражении будет означать начало строки после каждого\n
, а не просто начало "большой" строкиOCR_text
.x
в выраженииr"..."x
- мы можем использовать пробелы и указывать комментарии через#
(x от слова extended)
Значение круглых скобок обсудим ниже.
regex_fullname = r"
^Фамилия\s* # `Фамилия` в начале строки, а после 0 или более пробелов
[:-]\s* # далее один знак `:` или `-` и 0 или более пробелов
([\p{L}-]+) # [\p{L}-]+ - `\p{L}` - символы Unicode, `-` - дефис
\s* # после фамилии снова 0 или более пробелов
Имя\s*[:-]\s* # то же, что и с фамилией
(\p{L}+) # Любая последовательность букв (русских в том числе) это и есть имя"imx;
Для того, чтобы извлечь полезную информацию из нашего документа OCR_text
,используя регулярное выражение regex_fullname
, воспользуемся eachmatch
.
Заметим, что у нас есть 3 человека. И у каждого человека по 2 характеристики: Фамилия и Имя.
eachmatch
возвращает итератор, содержащий объекты типа RegexMatch, где каждый объект представляет одно совпадение шаблона в тексте.
Наш шаблон в себе содержит фамилию и имя. Фамилия в выражении у нас идёт первой, поэтому для фамилий мы будем использовать m.captures[1]
. Имя же у нас является вторым
Т.е. мы создали массив кортежей из фамилии и имени посетителей.
fullnames = [(m.captures[1], m.captures[2]) for m in eachmatch(regex_fullname, OCR_text)]
Выведем имена и фамилии, в формате Заголовка:
titlecase("abc"); # Abc
titlecase("aBC"); # Abc
for (surname, name) in fullnames
println("Здравствуйте, $(titlecase(surname)) $(titlecase(name))!")
end
Пример 2: Извлечение номеров телефонов¶
Допустим, нам нужно найти номер в формате +7-XXX-XXX-XX-XX
или 8-XXX-XXX-XX-XX
:
Пояснение:
\d{3}
означает ровно 3 цифры,
\+
экранирует плюс как литерал.
|
означает или
(?:...)
- "незахватывающая группа", т.е. это такая подчасть выражения, которую мы хотим отдельно определить (+7 или 8, а дальше уже набор цифр и дефисов).Но сама информация, записан ли телефон через +7 или через 8 нас НЕ интересует. Поэтому она и НЕзахватывающая.
text = "Российские номера это +7-912-345-67-89 или 8-987-654-32-10, но не +1-234-567-89-10"
russian_phone_regex = r"(?:\+7|8)-\d{3}-\d{3}-\d{2}-\d{2}"
for m in eachmatch(russian_phone_regex, text)
println("Найден российский номер: ", m.match)
end
Пример 3: Проверка email-адресов¶
Проверим корректность email:
email = "test_User-name.123@pochta.ru"
email_regex = r"^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}$"
if match(email_regex, email) !== nothing
println("Email корректен")
else
println("Некорректный email")
end
Пояснение:
^[a-zA-Z0-9._-]+
требует имя пользователя из букв, цифр и некоторых символов, а\.[a-z]{2,}$
— домен верхнего уровня длиной 2+ символа.
Пример 4: Обработка сносок на литературу¶
Извлечём сноски вида [1]
, [1, 2]
:
text = "Текст ссылается на [1], [2, 3] и [4], и содержит математические выражения: 1 + (2{3 - x[y-z]})."
ref_regex = r"\[\d+(?:,\s*\d+)*\]"
matches = [m.match for m in eachmatch(ref_regex, text)]
println("Сноски: ", join(matches, ", "))
Пояснение: (?:,\s*\d+)*
— незахватывающая группа для чисел с запятыми.
Пример 5: Получениее соннетов Шекспира¶
Пусть даны сонеты Уильяма Шекспира, пронумерованные римскими цифрами. Создадим пронумерованный в исходном порядке массив этих сонетов, чтобы можно было легко к ним обратиться по индексу.
sonnets_text = read("sonnets.txt",String);
print(sonnets_text[1:1000])
В сонетах присутствуют переносы строк. Поэтому точку не получится использовать для обозначения любого символа (посмотрите начало главы "Регулярные выржания"). Чтобы это обойти воспользуемся:
\s
означает пробельный символ.
\S
означает НЕ непробельный символ
А значит [\s\S]
означает любой символ.
function split_sonnets(text)
pattern = r"""
^ # Начало строки (с флагом m — для каждой строки)
\s* # Ноль или более пробелов перед римской цифрой
[IVXLCDM]+ # Одна или более римских цифр (I, V, X, L, C, D, M)
\s* # Ноль или более пробелов после цифры
$ # Конец строки (ограничивает строку только цифрой)
\s* # Пробелы или пустые строки после цифры
#___________________________________________________________________________________________________
( # Начало захватывающей группы для текста сонета
[\s\S]*? # Любой символ (включая \n), нежадно (до ближайшей остановки)
) # Конец захватывающей группы
#___________________________________________________________________________________________________
(?= # Положительный просмотр вперёд (условие остановки)
^ # Начало следующей строки
\s* # Пробелы перед следующей цифрой
[IVXLCDM]+ # Следующая римская цифра
\s* # Пробелы после неё
$ # Конец строки с цифрой
| # Или
\z # Абсолютный конец текста (для последнего сонета)
) # Конец просмотра вперёд
"""mx # Флаги: m (многострочный режим), x (расширенный режим)
sonnets = [strip(m.captures[1]) for m in eachmatch(pattern, text)]
return sonnets
end
sonnets = split_sonnets(sonnets_text)
Выведем теперь только первую строчку первых пяти сонетов.
Для это разделим, используя split
каждый сонет на 2 части таким образом:
- 1 часть: первая строка
- 2 часть: все последующие строки, кроме первой
s = """1 строка
2 строка
3 строка
4 строка"""
split(s,'\n',limit=2)
for (i, sonnet) in enumerate(sonnets[1:5])
println("""Соннет $i:$(split(sonnet,'\n',limit=2)[1])\n...""")
end
Измерим скорость выполнения нашей функции.
Pkg.add("BenchmarkTools")
using BenchmarkTools
@btime split_sonnets(sonnets_text);
1.24 миллисекунды это довольно хороший результат. для файла из 2.5 тысяч строк. Однако нужно понимать, что регулярные выражения могут уступать классическим подходам. В нашем случае мы могли решить задачу довольно явным способом. (Но можно в него и не погружаться, а посмотреть на скорость его выполнения)
function split_sonnets_fast(text)
sonnets = String[]
current_sonnet = String[]
in_sonnet = false
for line in eachline(text)
if !isempty(line) # Проверяем до отбрасывания пробелов и \n через функцию strip
stripped = strip(line)
# Если каждый (all) символ строки принадлежит IVXLCDM, то это римское число
if all(c -> c in "IVXLCDM", stripped)
if in_sonnet && !isempty(current_sonnet)
push!(sonnets, join(current_sonnet, '\n'))
end
current_sonnet = String[]
in_sonnet = true
elseif in_sonnet
push!(current_sonnet, line)
end
end
end
if in_sonnet && !isempty(current_sonnet)
push!(sonnets, join(current_sonnet, '\n'))
end
return sonnets
end
# Проверка работы
sonnets = split_sonnets_fast("sonnets.txt")
@btime split_sonnets_fast("sonnets.txt");
Заключение¶
Регулярные выражения в Julia — это мощный и удобный инструмент для работы с текстом. Благодаря простоте синтаксиса (r"..."
), встроенным функциям вроде match
и replace
, а также высокой производительности языка, они идеально подходят для задач обработки данных и анализа, поиска и замены. Но важно понимать, что регулярные выражения могут работать медленно для задач парсинга сложных (вложенных, например) структур, таких как JSON-файлы, HTML-файлы и пр.
И несмотря на некоторую сложность синтаксиса регулярных выражений, благодаря расширениям можно делать комментарии. Что является более универсальным инструментом для работы с текстом, чем встроенных функции работы с символами и строками в Julia.