Reguläre Ausdrücke in Kotlin: Eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты работаешь в огромной библиотеке, где миллионы книг. Директор даёт тебе задание: «Найди все книги, в названии которых есть слово „Кот“, но только если это слово написано с большой буквы и после него идёт пробел или точка». Если бы ты просматривал каждую книгу вручную, это заняло бы несколько лет. Но есть волшебный инструмент — регулярное выражение (или regex от английского regular expression).
Регулярное выражение — это как умный поисковый запрос, который может найти не просто точное слово, а целый шаблон. Например, ты можешь сказать: «Найди все номера телефонов в тексте» или «Проверь, правильно ли введён email». В нашей библиотеке регулярное выражение — это библиотечный робот, который мгновенно просматривает все книги и находит то, что нужно по заданному шаблону.
В программировании регулярные выражения используются повсюду:
- Валидация данных — проверка, правильно ли пользователь ввёл телефон, email, пароль
- Поиск и замена — найти все даты в тексте и заменить их формат
- Парсинг данных — извлечь информацию из логов, HTML-страниц, файлов
- Фильтрация — убрать из текста все лишние символы
Важно: Регулярные выражения — это не особенность Kotlin или Python. Это универсальный язык для описания шаблонов в тексте. Он работает одинаково во всех современных языках программирования, хотя есть небольшие различия в синтаксисе.
Давай сразу посмотрим на простой пример. Допустим, у нас есть строка:
val text = "Мой телефон: +7-912-345-67-89, а домашний: 8(495)123-45-67"
Если мы хотим найти все номера телефонов, мы не можем просто искать слово «телефон» — номера выглядят по-разному. Но мы можем описать шаблон: «цифра, потом ещё несколько цифр, потом тире или скобки, потом снова цифры...». Именно это и делают регулярные выражения.
Теория: как это работает изнутри
Чтобы понять, как работают регулярные выражения, нужно представить конечный автомат. Не пугайся сложного названия — это просто воображаемая машина, которая читает текст символ за символом и решает, подходит ли он под наш шаблон.
Представь, что ты стоишь на старте лабиринта. У тебя есть карта (регулярное выражение). Ты делаешь шаг (читаешь символ) и смотришь на карту: «Если я вижу букву „а“, иди налево; если вижу цифру — направо». Если ты доходишь до конца лабиринта — символ подходит под шаблон. Если заходишь в тупик — не подходит.
Регулярное выражение состоит из символов и метасимволов:
- Обычные символы — буквы, цифры, пробелы. Они ищут сами себя. Например, шаблон
catнайдёт слово «cat» в тексте. - Метасимволы — специальные знаки, которые имеют особое значение:
.,*,+,?,[],(),|,^,$,.
Вот как работает «движок» регулярных выражений (на примере шаблона a.b для строки «acb»):
- Движок начинает с первого символа строки — «a».
- Сравнивает с первым символом шаблона — тоже «a». Совпадение!
- Переходит ко второму символу шаблона — это точка
.. Точка означает «любой символ». В строке второй символ — «c». Подходит! - Переходит к третьему символу шаблона — «b». В строке третий символ — «b». Совпадение!
- Движок дошёл до конца шаблона — значит, найдено совпадение.
Если бы в строке было «acd», то на третьем шаге движок бы увидел «d» вместо «b» и сказал бы: «Не подходит, ищу дальше». Он бы сдвинулся на один символ в строке и начал проверку заново со второго символа.
Этот процесс называется backtracking (возврат). Движок постоянно пробует разные варианты, возвращается назад, если что-то пошло не так. Именно поэтому сложные регулярные выражения могут работать медленно — движок перебирает миллионы комбинаций.
Запомни: Регулярные выражения жадные по умолчанию. Это значит, что они стараются захватить как можно больше текста. Например, шаблонa.*bв строке «a123b456b» найдёт «a123b456b», а не «a123b». Чтобы сделать поиск «ленивым», нужно добавить знак вопроса:a.*?b.
Синтаксис в Kotlin: все формы и варианты
В Kotlin для работы с регулярными выражениями используется класс Regex. Он находится в стандартной библиотеке, поэтому ничего дополнительно устанавливать не нужно.
Создание регулярного выражения
Есть три основных способа создать объект Regex:
| Способ | Пример | Описание |
|---|---|---|
| Конструктор класса | Regex("шаблон") |
Самый простой способ. Передаёшь строку с шаблоном. |
Метод toRegex() |
"шаблон".toRegex() |
Удобно, если шаблон уже есть в виде строки. |
| Сырая строка (raw string) | Regex("""шаблон""") |
Лучший способ для сложных шаблонов. Не нужно экранировать обратные слеши. |
Основные методы класса Regex
| Метод | Что делает | Пример |
|---|---|---|
containsMatchIn(input) |
Проверяет, есть ли хотя бы одно совпадение в строке | regex.containsMatchIn("Hello") → true/false |
matches(input) |
Проверяет, соответствует ли вся строка шаблону | regex.matches("123") → true/false |
find(input) |
Находит первое совпадение и возвращает объект MatchResult? |
regex.find("abc123") → MatchResult или null |
findAll(input) |
Находит все совпадения и возвращает последовательность Sequence<MatchResult> |
regex.findAll("a1 b2 c3") → все совпадения |
replace(input, replacement) |
Заменяет все совпадения на указанную строку | regex.replace("Hello", "Hi") → новая строка |
split(input) |
Разделяет строку по совпадениям с шаблоном | regex.split("a,b,c") → список строк |
Специальные символы в Kotlin
В Kotlin, как и в других языках, используются стандартные метасимволы. Но есть важная особенность: обратный слеш нужно удваивать в обычных строках. В сырых строках ("""...""") этого делать не нужно.
| Символ | Значение | Пример в обычной строке | Пример в сырой строке |
|---|---|---|---|
. |
Любой символ (кроме перевода строки) | "a.b" |
"""a.b""" |
d |
Любая цифра (0-9) | "\d" |
"""d""" |
w |
Любая буква, цифра или подчёркивание | "\w" |
"""w""" |
s |
Любой пробельный символ (пробел, табуляция, перевод строки) | "\s" |
"""s""" |
^ |
Начало строки | "^Hello" |
"""^Hello""" |
$ |
Конец строки | "world$" |
"""world$""" |
* |
0 или более повторений предыдущего символа | "a*" |
"""a*""" |
+ |
1 или более повторений | "a+" |
"""a+""" |
? |
0 или 1 повторение (делает символ необязательным) | "colou?r" |
"""colou?r""" |
{n,m} |
От n до m повторений | "\d{3,5}" |
"""d{3,5}""" |
[...] |
Набор символов (любой из перечисленных) | "[aeiou]" |
"""[aeiou]""" |
(...) |
Группировка (захват группы) | "(\w+)@(\w+)" |
"""(w+)@(w+)""" |
| |
Или (логическое ИЛИ) | "cat|dog" |
"""cat|dog""" |
Флаги (опции) регулярных выражений
В Kotlin можно задать дополнительные опции, которые меняют поведение поиска:
| Флаг | Константа | Описание |
|---|---|---|
i |
RegexOption.IGNORE_CASE |
Игнорировать регистр букв |
m |
RegexOption.MULTILINE |
Символы ^ и $ работают для каждой строки, а не для всего текста |
s |
RegexOption.DOT_MATCHES_ALL |
Точка . также совпадает с переводом строки |
l |
RegexOption.LITERAL |
Все метасимволы отключаются, шаблон ищется как обычный текст |
Пример использования флагов:
// Создаём регулярное выражение с флагом IGNORE_CASE
val regex = Regex("kotlin", RegexOption.IGNORE_CASE)
// Теперь найдёт и "Kotlin", и "KOTLIN", и "kotlin"
println(regex.containsMatchIn("Я люблю KOTLIN")) // true
println(regex.containsMatchIn("Я люблю Kotlin")) // true
println(regex.containsMatchIn("Я люблю kotlin")) // true
Примеры кода — от простого к сложному
Пример 1: Проверка, содержит ли строка цифры
fun main() {
// Создаём шаблон: одна или более цифр
val hasDigits = Regex("""d+""")
val text1 = "Привет, мне 25 лет"
val text2 = "Привет, мне двадцать пять лет"
// containsMatchIn проверяет, есть ли хотя бы одно совпадение
println("Текст 1 содержит цифры: ${hasDigits.containsMatchIn(text1)}") // true
println("Текст 2 содержит цифры: ${hasDigits.containsMatchIn(text2)}") // false
}
Пример 2: Извлечение всех чисел из текста
fun main() {
val text = "В 2024 году было 12 месяцев, 52 недели и 365 дней"
// Шаблон: одна или более цифр
val numberPattern = Regex("""d+""")
// findAll возвращает все совпадения
val numbers = numberPattern.findAll(text)
println("Найденные числа:")
for (match in numbers) {
// match.value содержит текст совпадения
println(" - ${match.value}")
}
// Вывод:
// Найденные числа:
// - 2024
// - 12
// - 52
// - 365
}
Пример 3: Проверка email-адреса
fun main() {
// Простой шаблон для email:
// ^ - начало строки
// [a-zA-Z0-9._%+-]+ - имя пользователя (буквы, цифры, некоторые символы)
// @ - обязательная собачка
// [a-zA-Z0-9.-]+ - домен
// . - точка (экранированная)
// [a-zA-Z]{2,} - доменная зона (не менее 2 букв)
// $ - конец строки
val emailPattern = Regex("""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$""")
val emails = listOf(
"user@example.com",
"user.name@domain.co.uk",
"invalid-email",
"user@.com",
"user@domain"
)
for (email in emails) {
// matches проверяет, соответствует ли ВСЯ строка шаблону
val isValid = emailPattern.matches(email)
println("$email -> ${if (isValid) "✓ корректный" else "✗ некорректный"}")
}
}
Пример 4: Замена всех дат в тексте
fun main() {
val text = "Встреча назначена на 15.03.2024, а дедлайн - 01.04.2024"
// Шаблон для даты в формате ДД.ММ.ГГГГ
// d{2} - две цифры (день)
// . - точка (экранированная)
// d{2} - две цифры (месяц)
// . - точка
// d{4} - четыре цифры (год)
val datePattern = Regex("""d{2}.d{2}.d{4}""")
// Заменяем все даты на "[ДАТА]"
val result = datePattern.replace(text, "[ДАТА]")
println("Исходный текст: $text")
println("После замены: $result")
// Вывод:
// Исходный текст: Встреча назначена на 15.03.2024, а дедлайн - 01.04.2024
// После замены: Встреча назначена на [ДАТА], а дедлайн - [ДАТА]
}
Пример 5: Извлечение групп (имя пользователя и домен из email)
fun main() {
val text = "Мои контакты: john.doe@gmail.com и jane_smith@yahoo.com"
// Шаблон с группами:
// (w+.?w+) - первая группа: имя пользователя (буквы, цифры, подчёркивание, точка)
// @ - собачка
// (w+.w+) - вторая группа: домен
val emailPattern = Regex("""(w+.?w+)@(w+.w+)""")
// Находим все совпадения
val matches = emailPattern.findAll(text)
for (match in matches) {
// match.groupValues[0] - всё совпадение целиком
// match.groupValues[1] - первая группа (имя пользователя)
// match.groupValues[2] - вторая группа (домен)
println("Email: ${match.value}")
println(" Имя пользователя: ${match.groupValues[1]}")
println(" Домен: ${match.groupValues[2]}")
println()
}
}
Пример 6: Разделение строки по нескольким разделителям
fun main() {
val text = "яблоко,груша;банан:апельсин вишня"
// Шаблон: запятая, точка с запятой, двоеточие или пробел
val separators = Regex("""[,;: ]""")
// split разделяет строку по всем совпадениям
val fruits = separators.split(text)
println("Исходная строка: $text")
println("Разделённые фрукты:")
for (fruit in fruits) {
println(" - $fruit")
}
}
Пример 7: Сложный парсинг — извлечение данных из лога
fun main() {
val log = """
[2024-03-15 10:30:45] INFO - Пользователь user1 вошёл в систему
[2024-03-15 10:31:12] ERROR - Ошибка подключения к базе данных
[2024-03-15 10:32:00] WARN - Превышен лимит памяти
[2024-03-15 10:33:22] INFO - Пользователь user2 вышел из системы
""".trimIndent()
// Сложный шаблон для парсинга логов:
// [ - открывающая скобка (экранированная)
// (d{4}-d{2}-d{2} d{2}:d{2}:d{2}) - группа 1: дата и время
// ] - закрывающая скобка
// s+ - один или более пробелов
// (INFO|ERROR|WARN) - группа 2: уровень (только эти три варианта)
// s+ - пробелы
// - - тире
// s+ - пробелы
// (.+) - группа 3: сообщение (всё остальное)
val logPattern = Regex("""[(d{4}-d{2}-d{2} d{2}:d{2}:d{2})]s+(INFO|ERROR|WARN)s+-s+(.+)""")
val matches = logPattern.findAll(log)
println("Разбор логов:")
println("=" .repeat(50))
for (match in matches) {
val timestamp = match.groupValues[1]
val level = match.groupValues[2]
val message = match.groupValues[3]
println("Время: $timestamp")
println("Уровень: $level")
println("Сообщение: $message")
println("-" .repeat(50))
}
}
Пример 8: Валидация пароля (сложный случай)
fun main() {
// Проверка пароля:
// - минимум 8 символов
// - хотя бы одна заглавная буква
// - хотя бы одна строчная буква
// - хотя бы одна цифра
// - хотя бы один специальный символ
val password = "MyP@ssw0rd"
// Отдельные проверки (так понятнее и надёжнее, чем один огромный regex)
val hasMinLength = password.length >= 8
val hasUpperCase = Regex("""[A-Z]""").containsMatchIn(password)
val hasLowerCase = Regex("""[a-z]""").containsMatchIn(password)
val hasDigit = Regex("""d""").containsMatchIn(password)
val hasSpecial = Regex("""[!@#$%^&*()_+-=[]{};':"\|,.<>/?]""").containsMatchIn(password)
println("Проверка пароля: $password")
println(" Длина >= 8: $hasMinLength")
println(" Заглавная буква: $hasUpperCase")
println(" Строчная буква: $hasLowerCase")
println(" Цифра: $hasDigit")
println(" Спецсимвол: $hasSpecial")
val isValid = hasMinLength && hasUpperCase && hasLowerCase && hasDigit && hasSpecial
println(" Пароль ${if (isValid) "корректный" else "некорректный"}")
}
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет легко освоить регулярные выражения в Kotlin. Они очень похожи, но есть важные различия.
| Характеристика | Kotlin | Python |
|---|---|---|
| Модуль/класс | Regex (встроенный класс) |
re (встроенный модуль) |
| Создание объекта | Regex("pattern") или "pattern".toRegex() |
re.compile("pattern") |
| Поиск первого совпадения | regex.find(text) |
re.search(pattern, text) |
| Поиск всех совпадений | regex.findAll(text) |
re.findall(pattern, text) |
| Проверка полного совпадения | regex.matches(text) |
re.fullmatch(pattern, text) |
| Замена | regex.replace(text, replacement) |
re.sub(pattern, replacement, text) |
| Разделение | regex.split(text) |
re.split(pattern, text) |
| Сырые строки | """...""" (тройные кавычки) |
r"..." (префикс r) |
| Флаги | RegexOption.IGNORE_CASE |
re.IGNORECASE |
| Именованные группы | (?<name>...) |
(?P<name>...) |
Главные отличия:
- Синтаксис обратного слеша: В Python ты используешь
r"d", в Kotlin —"""d""". Оба подхода отключают экранирование, но выглядят по-разному. - Тип результата: В Kotlin
find()возвращаетMatchResult?(может быть null), а в Pythonre.search()возвращаетMatchилиNone. Разница в том, что Kotlin использует безопасные типы (nullable), а Python — динамическую типизацию. - Производительность: В Kotlin, если ты используешь один и тот же шаблон много раз, лучше создать
Regexодин раз и переиспользовать его. В Python то же самое сre.compile(). Но в Kotlin это более критично, так как компиляция шаблона — дорогая операция. - Null-безопасность: В Kotlin нужно явно обрабатывать случай, когда совпадение не найдено (использовать
?:или проверять на null). В Python ты просто проверяешьif match:.
Совет: Если ты переходишь с Python на Kotlin, запомни главное правило: в Kotlin всегда используй сырые строки """...""" для регулярных выражений. Это избавит тебя от головной боли с экранированием обратных слешей.
Типичные ошибки и подводные камни
Ошибка 1: Забыл экранировать специальные символы
// НЕПРАВИЛЬНО: точка означает "любой символ"
val wrongPattern = Regex("""hello.world""")
// Найдёт "helloXworld", "hello world", "hello.world" и т.д.
// ПРАВИЛЬНО: экранируем точку
val correctPattern = Regex("""hello.world""")
// Найдёт только "hello.world"
Ошибка 2: Использование matches() вместо containsMatchIn()
val pattern = Regex("""d+""")
// matches() проверяет, что ВСЯ строка состоит из цифр
println(pattern.matches("abc123")) // false — вся строка не состоит из цифр
println(pattern.matches("123")) // true
// containsMatchIn() проверяет, есть ли цифры ВНУТРИ строки
println(pattern.containsMatchIn("abc123")) // true — цифры есть
Ошибка 3: Жадность квантификаторов
val text = "<h1>Заголовок</h1> <p>Текст</p>"
// Жадный поиск — захватит всё от первого < до последнего >
val greedy = Regex("""<.*>""")
println(greedy.find(text)?.value) // "Заголовок
Текст
"
// Ленивый поиск — захватит минимально возможное
val lazy = Regex("""<.*?>""")
println(lazy.find(text)?.value) // ""
Ошибка 4: Неправильное использование обратных ссылок
// Хотим найти повторяющиеся слова
val text = "кот кот собака"
// 1 — обратная ссылка на первую группу
val pattern = Regex("""(w+)s+1""")
println(pattern.find(text)?.value) // "кот кот"
// Ошибка: если группа не захвачена, обратная ссылка не работает
val wrongPattern = Regex("""w+s+1""") // 1 ссылается на несуществующую группу
Ошибка 5: Проблемы с Unicode и кириллицей
// w не работает с кириллицей по умолчанию
val text = "Привет, мир!"
// w+ найдёт только "Привет" (без запятой и пробела)
val pattern =