Skip to content

Latest commit

 

History

History
173 lines (132 loc) · 14.4 KB

chapter06_5.adoc

File metadata and controls

173 lines (132 loc) · 14.4 KB

6.5. Доктор RegExp

Регулярные выражения (RegExp) — специальный язык для описания множества строк. Они помогают решать задачу поиска какого-либо текста (из описанного множества) в другом тексте, описывают интересующий нас текст и работают достаточно эффективно для быстрого решения задачи поиска.

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

Примеры регулярных выражений (см. слайды):

  • KotlinAsFirst

  • @[A-Z0-9.-]\.[A-Z]{2,}

  • ˆ4[0-9]{12}(?:[0-9]{3})?$

  • [-]?[0-9]*\.?[0-9]

  • <()([ˆ<])(?:>(.)<\/\1>|\s+\/>)

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

Возможности языка регулярных выражений

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

  • KotlinAsFirst

  • Трансмогрификация

  • Мама мыла раму

  • 42

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

  • [0123456789] — любая цифра

  • [aeiouy] — любая буква из перечисленных

  • [~!@#$%^&*+-] — любой символ из перечисленных

Отрицание класса символов ищет любой символ НЕ из заданного множества:

  • [^0123456789] — всё, что угодно, кроме цифры

  • [^a-z] — всё, что угодно, кроме строчной латинской буквы

  • [^-az] — всё, что угодно, кроме -, a, z

Классы и их отрицания, как видим, используют специальные символы […​] для обозначения класса, специальный символ - для обозначения интервала символов и последовательность [^…​] для обозначения отрицания.

Якоря позволяют найти начало или конец всей строки:

  • ^fun — fun в начале строки

  • \.$ — точка в конце строки

  • ^Kotlin is great as the first language!$ — ВСЯ строка с заданной фразой (и более ничем)

Здесь ^ используется для обозначения начала строки, а $ для обозначения конца. Следует иметь в виду, что якоря никак не учитывают переводы строк — имеется в виду начало или конец всего текста, а не одной строки в тексте.

\. использует экранирование для обозначения символа ., поскольку в регулярных выражениях точка является специальным символом (и обозначает любой символ). Таким образом, \ в регулярных выражениях экранирует последующий символ, делая его из специального символа обыкновенным. Для обозначения символа \ применяется пара \\. Аналогично, \^ обозначает символ-шапку, \$ — символ доллара, \[ — открывающую квадратную скобку, \] — закрывающую квадратную скобку.

Особые символы ищут символы по специальным правилам:

  • ` …​.. ` — любая последовательность из пяти символов, начинающаяся и заканчивающаяся пробелов

  • \t — табуляция, \n — новая строка, \r — возврат каретки (два последних символа унаследованы компьютерами от эпохи пишущих машинок, когда для начала печати с новой строки необходимо было выполнить два действия — возврат каретки в начало строки и перевод каретки на новую строку)

  • \s — произвольный вид пробела (пробел, табуляция, новая строка, возврат каретки)

  • \d — произвольная цифра, аналог [0-9]

  • \w — произвольная "символ в слове", обычно аналог [a-zA-z0-9], то есть, латинская буква или цифра

  • \S — НЕ пробел, \D — НЕ цифра, \W — НЕ "символ в слове"

Шаблон выбора | ищет одну строку из нескольких, например:

  • Марат|Михаил — Марат или Михаил

  • ^\[|\]$ — открывающая квадратная скобка в начале строки или закрывающая в конце

  • for.(val|var). — цикл for с последующим val или var

Шаблоны количества ищут определённое число совпадений:

  • .* — любое количество (в том числе ноль) любых символов

  • (Марат)+ — строка Марат один или более раз (но не ноль)

  • (Михаил)? — строка Михаил ноль или один раз

  • ([0-9]{4}) — последовательность из ровно четырёх любых цифр

  • \w{8,16} — последовательность из 8-16 "символов в слове"

Круглые скобки () задают так называемые группы поиска, объединяя несколько символов вместе.

  • (Kotlin)+AsFirst — KotlinAsFirst, KotlinKotlinAsFirst, KotlinKotlinKotlinAsFirst, …​

  • (?:\$\$)+ — `, ``, `, …​

  • (\w+)\s\1 — слово, за которым следует пробел и то же самое слово.

  • fun\s+(/w+)\s*\{.\1.\} — fun с последующими пробелами, произвольным словом в круглых скобках, пробелами и тем же словом в фигурных скобках

Здесь \1 (\2, \3, …​) ищет уже описанную группу поиска по её номеру внутри регулярного выражения (в данном случае — первую группу). Комбинация (?:…​) задаёт группу поиска без номера. В целом, (?…​) задаёт группы особого поиска:

  • Марат(?=\sАхин) — Марат, за которым следует пробел и Ахин

  • (?⇐Михаил\s)Глухих — Глухих, перед которым стоит Михаил с пробелом

  • \d+(?![$\d]) — число, после которого НЕ стоит знак доллара

  • (?<!root\s)beer — beer, перед которым НЕ стоит root с пробелом

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

Для описания регулярных выражений в Котлине используется тип Regex. Для создания регулярного выражения следует вызвать его конструктор, например Regex("KotlinAsFirst"). Второй способ создания регулярного выражения — вызов функции toRegex() на строке-получателе, например "KotlinAsFirst".toRegex().

При создании регулярных выражений вместо обычных строк в двойных кавычках рекомендуется использовать так называемые raw string literals (необработанные строки). Перед и после такого литерала должны стоять три двойных кавычки. Внутри необработанных строк не применяется экранирование, что позволяет применять специфичные для регулярных выражений символы без дополнительных ухищрений. Например: Regex("""x|+|-|\*|/|\(|\)|\d+?| ?""")` -- задаёт выражение `x`, или `, или -, или …​, или число, или любое количество пробелов. Без тройных кавычек нам пришлось бы дважды записать каждый из \.

Для анализа результата поиска применяется тип MatchResult, который можно получить, вызвав find на регулярном выражении-получатале: Regex("""…​""").find(string, startIndex). find ищет первое вхождение регулярного выражения в строку string, начиная с индекса startIndex (по умолчанию — 0). Если вхождений регулярного выражения не найдено, результат find равен null.

Regex("""…​""").findAll(string, startIndex) ищет ВСЕ вхождения регулярного выражения, которые после этого можно перебрать с помощью цикла for.

Тип MatchResult включает в себя следующие свойства:

  • result.value — подстрока исходной строки, с которой совпало регулярное выражение (совпадение)

  • result.range — интервал индексов символов, в котором было найдено совпадение

  • result.groupValues — список строк, 0-й элемент которого содержит всё регулярное выражение, а последующие содержат значения групп поиска из регулярного выражения (то есть размер списка равен числу групп поиска в выражении + 1)

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

  • Regex("""…​""").replace("MyString", "Replacement") — находит в данной строке все вхождения регулярного выражения и заменяет их на `"Replacement"

  • "MyString".contains(Regex("""…​""")) — есть ли в данной строке хоть одно вхождение регулярного выражения

  • Regex("""…​""").containsMatchIn("MyString") — то же самое, но в другом порядке

  • "MyString".matches(Regex("""…​""")) — соответствует ли данная строка данному регулярному выражению

  • Regex("""…​""").matches("MyString") — то же самое, но в другом порядке

  • Regex("""…​""").split("MyString") — деление строки на части с использованием заданного регулярного выражения как разделителя

Мини-пример:

fun timeStrToSeconds(str: String): Int {
    val matchResult = Regex("""(\d\d):(\d\d):(\d\d)""").find(str)
    if (matchResult == null) return -1
    return matchResult.groupValues.drop(1).map { it.toInt() }.fold(0) {
        previous, next -> previous * 60 + next
    }
}

Здесь мы разбираем исходную строку вида "12:34:56" с целью найти в ней три одинаковых группы поиска (\d\d). Каждая из групп поиска включает в себя две цифры. Убедившись с помощью проверки на null, что регулярное выражение успешно найдено, мы отбрасываем первый элемент groupValues с помощью функции drop(1), оставляя, таким образом, в списке только значения трёх групп поиска. Далее каждая из пар цифр конвертируется в число. Результат сворачивается в число секунд, прошедших с начала дня, с помощью функции высшего порядка fold — см. раздел 4.