🎯 Kotlin

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»):

  1. Движок начинает с первого символа строки — «a».
  2. Сравнивает с первым символом шаблона — тоже «a». Совпадение!
  3. Переходит ко второму символу шаблона — это точка .. Точка означает «любой символ». В строке второй символ — «c». Подходит!
  4. Переходит к третьему символу шаблона — «b». В строке третий символ — «b». Совпадение!
  5. Движок дошёл до конца шаблона — значит, найдено совпадение.

Если бы в строке было «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>...)

Главные отличия:

  1. Синтаксис обратного слеша: В Python ты используешь r"d", в Kotlin — """d""". Оба подхода отключают экранирование, но выглядят по-разному.
  2. Тип результата: В Kotlin find() возвращает MatchResult? (может быть null), а в Python re.search() возвращает Match или None. Разница в том, что Kotlin использует безопасные типы (nullable), а Python — динамическую типизацию.
  3. Производительность: В Kotlin, если ты используешь один и тот же шаблон много раз, лучше создать Regex один раз и переиспользовать его. В Python то же самое с re.compile(). Но в Kotlin это более критично, так как компиляция шаблона — дорогая операция.
  4. 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 =