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

## Что такое регулярные выражения?

Если вам когда-нибудь приходилось работать с командной строкой, вы, вероятно, использовали маски имён файлов. Например, чтобы удалить все файлы в текущей директории, которые начинаются с буквы "d", можно написать ``rm d*``.

Регулярные выражения представляют собой похожий, но гораздо более сильный инструмент для поиска строк, проверки их на соответствие какому-либо шаблону и другой подобной работы. Англоязычное название этого инструмента — Regular Expressions или просто RegExp. Строго говоря, регулярные выражения — специальный язык для описания шаблонов строк.

Реализация этого инструмента различается в разных языках программирования, хоть и не сильно. В данной статье мы будем ориентироваться в первую очередь на реализацию Perl Compatible Regular Expressions. 

## Основы синтаксиса

В первую очередь стоит заметить, что любая строка сама по себе является регулярным выражением. Так, выражению "Хаха", очевидно, будет соответствовать строка "Хаха" и только она. Регулярные выражения являются регистрозависимыми, поэтому строка "хаха" (с маленькой буквы) уже не будет соответствовать выражению выше.

Однако уже здесь следует быть аккуратным — как и любой язык, регулярные выражения имеют спецсимволы, которые нужно экранировать. Вот их список: ``. ^ $ * + ? { } [ ] \ | ( )``. Экранирование осуществляется обычным способом — добавлением \ перед спецсимволом.


## Набор символов

Предположим, мы хотим найти в тексте все междометия, обозначающие смех. Просто "Хаха" нам не подойдёт — ведь под него не попадут "Хехе", "Хохо" и "Хихи". Да и проблему с регистром первой буквы нужно как-то решить.

Здесь нам на помощь придут наборы — вместо указания конкретного символа, мы можем записать целый список, и если в исследуемой строке на указанном месте будет стоять любой из перечисленных символов, строка будет считаться подходящей. Наборы записываются в квадратных скобках — шаблону [abcd] будет соответствовать любой из символов "a", "b", "c" или "d".

Внутри набора большая часть спецсимволов не нуждается в экранировании, однако использование "\" перед ними не будет считаться ошибкой. По прежнему необходимо экранировать символы "\" и "^", и, желательно, "]" (так, ``[][]`` обозначает любой из символов "]" или "[", тогда как [[]х] – исключительно последовательность "[х]"). Необычное на первый взгляд поведение регулярок с символом "]" на самом деле определяется известными правилами, но гораздо легче просто экранировать этот символ, чем их запоминать. Кроме этого, экранировать нужно символ "-", он используется для задания диапазонов (см. ниже).

Если сразу после "[" записать символ "^", то набор приобретёт обратный смысл — подходящим будет считаться любой символ кроме указанных. Так, шаблону [^xyz] соответствует любой символ, кроме, собственно, "x", "y" или "z".

Итак, применяя данный инструмент к нашему случаю, если мы напишем [Хх][аоие]х[аоие], то каждая из строк "Хаха", "хехе", "хихи" и даже "Хохо" будут соответствовать шаблону.

## Предопределённые классы символов

Для некоторых наборов, которые используются достаточно часто, существуют специальные шаблоны. Так, для описания любого пробельного символа (пробел, табуляция, перенос строки) используется \s, для цифр — \d, для символов латиницы, цифр и подчёркивания "_" — \w.

Если необходимо описать вообще любой символ, для этого используется точка — ".". Если указанные классы написать с заглавной буквы (\S, \D, \W) то они поменяют свой смысл на противоположный — любой непробельный символ, любой символ, который не является цифрой, и любой символ кроме латиницы, цифр или подчёркивания соответственно.

Также с помощью регулярных выражений есть возможность проверить положение строки относительно остального текста. Выражение \b обозначает границу слова, \B — не границу слова, ^ — начало текста, а $ — конец (cледует иметь в виду, что якоря никак не учитывают переводы строк — имеется в виду начало или конец всего текста, а не одной строки в тексте). Так, по шаблону \bJava\b в строке "Java and JavaScript" найдутся первые 4 символа, а по паттерну \bJava\B — символы c 10-го по 13-й (в составе слова "JavaScript").

## Диапазоны

У вас может возникнуть необходимость обозначить набор, в который входят буквы, например, от "б" до "ф". Вместо того, чтобы писать [бвгдежзиклмнопрстуф] можно воспользоваться механизмом диапазонов и написать [б-ф]. Так, паттерну x[0-8A-F][0-8A-F] соответствует строка "xA6", но не соответствует "xb9" (во-первых, из-за того, что в диапазоне указаны только заглавные буквы, во-вторых, из-за того, что 9 не входит в промежуток 0-8).

Механизм диапазонов особенно актуален для русского языка, ведь для него нет конструкции, аналогичной \w. Чтобы обозначить все буквы русского алфавита, можно использовать паттерн [а-яА-ЯёЁ]. Обратите внимание, что буква "ё" не включается в общий диапазон букв, и её нужно указывать отдельно.

## Квантификаторы (указание количества повторений)

Вернёмся к нашему примеру. Что, если в "смеющемся" междометии будет больше одной гласной между буквами “х”, например “Хаахаааа”? Наша старая регулярка уже не сможет нам помочь. Здесь нам придётся воспользоваться квантификаторами.

Квантификатор | Число повторений | Пример | Подходящие строки
--------------|------------------|--------|------------------
{n} | Ровно n раз | Ха{3}ха | Хаааха
{m,n} | От m до n включительно | Ха{2,4}ха | Хааха, Хаааха, Хааааха
{m,} | Не менее m | Ха{2,}ха | Хааха, Хаааха, Хааааха и т. д.
{,n} | Не более n | Ха{,3}ха | Хха, Хаха, Хааха, Хаааха


Обратите внимание, что квантификатор применяется только к символу, который стоит перед ним.

Некоторые часто используемые конструкции получили в языке регулярных выражений специальные обозначения:

Квантификатор | Аналог | Значение
:------------:|:------:|---------
? | {0,1} | Ноль или одно вхождение
``*`` | {0,} | Ноль или более
``+`` |	{1,} | Одно или более


Таким образом, с помощью квантификаторов мы можем улучшить наш шаблон для междометий до ``[Хх][аоеи]+х[аоеи]*``, и он сможет распознавать строки “Хааха”, “хееееех” и “Хихии”.

## Ленивая квантификация

Предположим, перед нами стоит задача — найти все HTML-теги в строке

```html
<p><b>Tproger</b> — мой <i>любимый</i> сайт о программировании!</p>
```

Очевидное решение <.*> здесь не сработает — оно найдёт всю строку целиком, т.к. она начинается с тега абзаца и им же заканчивается. То есть содержимым тега будет считаться строка

```
p><b>Tproger</b> — мой <i>любимый</i> сайт о программировании!</p
```

Это происходит из-за того, что по умолчанию квантификатор работают по т.н. жадному алгоритму — старается вернуть как можно более длинную строку, соответствующую условию. Решить проблему можно двумя способами. Первый — использовать выражение ``<[^>]*>``, которое запретит считать содержимым тега правую угловую скобку. Второй — объявить квантификатор не жадным, а ленивым. Делается это с помощью добавления справа к квантификатору символа ?. Т.е. для поиска всех тегов выражение обратится в ``<.*?>``.

## Ревнивая квантификация

Иногда для увеличения скорости поиска (особенно в тех случаях, когда строка не соответствует регулярному выражению) можно использовать запрет алгоритму возвращаться к предыдущим шагам поиска для того, чтобы найти возможные соответствия для оставшейся части регулярного выражения. Это называется ревнивой квантификацией. Квантификатор делается ревнивым с помощью добавления к нему справа символа +. Ещё одно применение ревнивой квантификации — исключение нежелательных совпадений. Так, паттерну ab*+a в строке “ababa” будут соответствовать только первые три символа, но не символы с третьего по пятый, т.к. символ “a”, который стоит на третьей позиции, уже был использован для первого результата.

## Скобочные группы

Для нашего шаблона “смеющегося” междометия осталась самая малость — учесть, что буква “х” может встречаться более одного раза, например, “Хахахахааахахооо”, а может и вовсе заканчиваться на букве “х”. Вероятно, здесь нужно применить квантификатор для группы [аиое]+х, но если мы просто напишем [аиое]х+, то квантификатор + будет относиться только к символу “х”, а не ко всему выражению. Чтобы это исправить, выражение нужно взять в круглые скобки: ([аиое]х)+.

Таким образом, наше выражение превращается в [Хх]([аиое]х?)+ — сначала идёт заглавная или строчная “х”, а потом произвольное ненулевое количество гласных, которые (возможно, но не обязательно) перемежаются одиночными строчными “х”. Однако это выражение решает проблему лишь частично — под это выражение попадут и такие строки, как, например, “хихахех” — кто-то может быть так и смеётся, но допущение весьма сомнительное. Очевидно, мы можем использовать набор из всех гласных лишь единожды, а потом должны как-то опираться на результат первого поиска. Но как?…

## Запоминание результата поиска по группе (обратная связь)

Оказывается, результат поиска по скобочной группе записывается в отдельную ячейку памяти, доступ к которой доступен для использования в последующих частях регулярного выражения. Возвращаясь к задаче с поиском HTML-тегов на странице, нам может понадобиться не только найти теги, но и узнать их название. В этом нам может помочь регулярное выражение <(.*?)>.

```html
<p><b>Tproger</b> — мой <i>любимый</i> сайт о программировании!</p>
```

Результат поиска по всем регулярному выражению: “<p>”, “<b>”, “</b>”, “<i>”, “</i>”, “</p>”.

Результат поиска по первой группе: “p”, “b”, “/b”, “i”, “/i”, “/i”, “/p”.

На результат поиска по группе можно ссылаться с помощью выражения \n, где n — цифра от 1 до 9. Например выражению (\w)(\w)\1\2 соответствуют строки “aaaa”, “abab”, но не соответствует “aabb”.

Если выражение берётся в скобки только для применения к ней квантификатора (не планируется запоминать результат поиска по этой группе), то сразу после первой скобки стоит добавить ?:, например ``(?:[abcd]+\w)``.

С использованием этого механизма мы можем переписать наше выражение к виду ``[Хх]([аоие])х?(?:\1х?)*``.


## Перечисление

Чтобы проверить, удовлетворяет ли строка хотя бы одному из шаблонов, можно воспользоваться аналогом булевого оператора OR, который записывается с помощью символа "|". Так, под шаблон ``Анна|Одиночество`` попадают строки “Анна” и “Одиночество” соответственно. Особенно удобно использовать перечисления внутри скобочных групп. Так, например (?:a|b|c|d) полностью эквивалентно [abcd] (в данном случае второй вариант предпочтительнее в силу производительности и читаемости).

С помощью этого оператора мы сможем добавить к нашему регулярному выражению для поиска междометий возможность распознавать смех вида “Ахахаах” — единственной усмешке, которая начинается с гласной: ``[Хх]([аоие])х?(?:\1х?)*|[Аа]х?(?:ах?)+``

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

* \t — табуляция
* \n — новая строка
* \r — возврат каретки (два последних символа унаследованы компьютерами от эпохи пишущих машинок, когда для начала печати с новой строки необходимо было выполнить два действия — возврат каретки в начало строки и перевод каретки на новую строку)
* \s — произвольный вид пробела (пробел, табуляция, новая строка, возврат каретки)
* \d — произвольная цифра, аналог [0-9]
* \w — произвольный «символ в слове», обычно аналог [a-zA-z0-9], то есть, латинская буква или цифра
* \S — НЕ пробел, \D — НЕ цифра, \W — НЕ «символ в слове»

## [(?...)](https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%83%D0%BB%D1%8F%D1%80%D0%BD%D1%8B%D0%B5_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F#%D0%9A%D0%B2%D0%B0%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F_(%D0%BF%D0%BE%D0%B8%D1%81%D0%BA_%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B5%D0%B9))

### Группировка без обратной связи (?:...)

Если группа используется только для группировки и её результат в дальнейшем не потребуется, то можно использовать группировку вида (?:шаблон). Под результат такой группировки не выделяется отдельная область памяти и, соответственно, ей не назначается номер. Это положительно влияет на скорость выполнения выражения, но понижает удобочитаемость.

### Атомарная группировка (?>...)

Атомарная группировка вида (?>шаблон) также, как и группировка без обратной связи, не создаёт обратных связей. В отличие от неё, такая группировка запрещает возвращаться назад по строке, если часть шаблона уже найдена.

>### Модификаторы (В котлине не поддерживаются)
>
>Модификаторы действуют с момента вхождения и до конца регулярного выражения или противоположного модификатора. Некоторые интерпретаторы могут применить модификатор ко всему выражению, а не с момента его вхождения.
>
>#### нечувствительность выражения к регистру символов (англ. case insensitivity)
>* (?i) - Включает
>* (?-i) - Выключает
>
>#### режим соответствия точки символам переноса строки и возврата >каретки
>* (?s) - Включает
>* (?-s) - Выключает
>
>#### Комментарии
>
>Для добавления комментариев в регулярное выражение можно использовать группы-комментарии вида (?#комментарий). Такая группа интерпретатором полностью игнорируется и не проверяется на вхождение в текст. Например, выражение А(?#тут комментарий)Б соответствует строке АБ.


### Просмотр вперёд и назад

В большинстве реализаций регулярных выражений есть способ производить поиск фрагмента текста, «просматривая» (но не включая в найденное) окружающий текст, который расположен до или после искомого фрагмента текста. Просмотр с отрицанием используется реже и «следит» за тем, чтобы указанные соответствия, напротив, не встречались до или после искомого текстового фрагмента.

Представление | Вид просмотра | Пример | Соответствие
--------------|---------------|--------|-------------
(?=шаблон) | Позитивный просмотр вперёд | Людовик(?=XVI) | ЛюдовикXV, **Людовик**XVI, **Людовик**XVIII, ЛюдовикLXVII, ЛюдовикXXL
(?!шаблон) | Негативный просмотр вперёд (с отрицанием) | Людовик(?!XVI) | **Людовик**XV, ЛюдовикXVI, ЛюдовикXVIII, **Людовик**LXVII, **Людовик**XXL
(?<=шаблон) | Позитивный просмотр назад | (?<=Сергей )Иванов | Сергей **Иванов**, Игорь Иванов
(?<!шаблон) | Негативный просмотр назад (с отрицанием) | (?<!Сергей )Иванов | Сергей Иванов, Игорь **Иванов**

### Поиск по условию

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

Представление | Пояснение | Пример | Соответствие
--------------|-----------|--------|--------------
(?:(?=если)то\|иначе) | Если операция просмотра успешна, то далее выполняется часть то, иначе выполняется часть иначе. В выражении может использоваться любая из четырёх операций просмотра. Следует учитывать, что операция просмотра нулевой ширины, поэтому части то в случае позитивного или иначе в случае негативного просмотра должны включать в себя описание шаблона из операции просмотра. | (?:(?<=а)м\|п) | ма**м**, **п**ап
(?(n)то\|иначе) | Если n-я группа вернула значение, то поиск по условию выполняется по шаблону то, иначе по шаблону иначе. | (а)?(?(1)м\|п) | м**ам**, **п**а**п**

### Флаги

Регулярные выражения могут иметь флаги, которые влияют на поиск.

В Котлине флаги задаются вторым параметром в конструкторе

Флаг | Действие
-----|---------
IGNORE_CASE | Регистронезависимый поиск
MULTILINE | Мультистроковый поиск
LITERAL |
UNIX_LINES |
COMMENTS |
CANON_EQ |

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

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

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

```kt
fun main(args: Array<String>){
    // регулярное выражение создано конструтором с одним флагом
    val regex = Regex("""(\d+)""", RegexOption.IGNORE_CASE)
    val res = regex.find("найдет только число 99, а число 22 не найдет")
    if(res!=null)
        for (r in res.groupValues)
            println(r)
}
```

Программа выдаст:

```
99
99
```

Во втором варианте создадим регулярное выражение через метод строки:

```kt
fun main(args: Array<String>){
    val regex = """(\d+)""".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
    val res = regex.findAll("найдет число 99, и число 22 тоже")
    if(res!=null)
        for (r in res)
            for(r2 in r.groupValues.drop(1))
                println(r2)
}
```

Программа выдаст:

```
99
22
```

Для анализа результата поиска применяется тип 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") — деление строки на части с использованием заданного регулярного выражения как разделителя

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

```kt
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

# Задания для закрепления

## Найдите время

Время имеет формат часы:минуты. И часы, и минуты состоят из двух цифр, пример: 09:00. Напишите регулярное выражение для поиска времени в строке: “Завтрак в 09:00”. Учтите, что “37:98” – некорректное время.

## Java[^script]

Найдет ли регулярка Java[^script] что-нибудь в строке Java? А в строке JavaScript?

## Цвет

Напишите регулярное выражение для поиска HTML-цвета, заданного как #ABCDEF, то есть символ "#" и затем 6 шестнадцатеричных символов.

## Разобрать арифметическое выражение

Арифметическое выражение состоит из двух чисел и операции между ними, например:

* 1 + 2
* 1.2 *3.4
* -3/ -6
* -2-2

Список операций: “+”, «-», “*” и “/”.

Также могут присутствовать пробелы вокруг оператора и чисел.

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

## Найдите все натуральные числа (возможно, окружённые буквами);

## Найдите все «слова», написанные капсом (то есть строго заглавными), возможно внутри настоящих слов (аааБББввв);

## Найдите слова, в которых есть русская буква, а когда-нибудь за ней цифра;

## Найдите все слова, начинающиеся с русской или латинской большой буквы (\b — граница слова);

## Найдите слова, которые начинаются на гласную (\b — граница слова);

## Найдите все натуральные числа, не находящиеся внутри или на границе слова;

## Найдите строчки, в которых есть символ * (. — это точно не конец строки!);

## Найдите строчки, в которых есть открывающая и когда-нибудь потом закрывающая скобки;

## Найдите пустые строчки;