# Регулярные выражения ## Что такое регулярные выражения? Если вам когда-нибудь приходилось работать с командной строкой, вы, вероятно, использовали маски имён файлов. Например, чтобы удалить все файлы в текущей директории, которые начинаются с буквы "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

Tproger — мой любимый сайт о программировании!

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

]*>``, которое запретит считать содержимым тега правую угловую скобку. Второй — объявить квантификатор не жадным, а ленивым. Делается это с помощью добавления справа к квантификатору символа ?. Т.е. для поиска всех тегов выражение обратится в ``<.*?>``. ## Ревнивая квантификация Иногда для увеличения скорости поиска (особенно в тех случаях, когда строка не соответствует регулярному выражению) можно использовать запрет алгоритму возвращаться к предыдущим шагам поиска для того, чтобы найти возможные соответствия для оставшейся части регулярного выражения. Это называется ревнивой квантификацией. Квантификатор делается ревнивым с помощью добавления к нему справа символа +. Ещё одно применение ревнивой квантификации — исключение нежелательных совпадений. Так, паттерну ab*+a в строке “ababa” будут соответствовать только первые три символа, но не символы с третьего по пятый, т.к. символ “a”, который стоит на третьей позиции, уже был использован для первого результата. ## Скобочные группы Для нашего шаблона “смеющегося” междометия осталась самая малость — учесть, что буква “х” может встречаться более одного раза, например, “Хахахахааахахооо”, а может и вовсе заканчиваться на букве “х”. Вероятно, здесь нужно применить квантификатор для группы [аиое]+х, но если мы просто напишем [аиое]х+, то квантификатор + будет относиться только к символу “х”, а не ко всему выражению. Чтобы это исправить, выражение нужно взять в круглые скобки: ([аиое]х)+. Таким образом, наше выражение превращается в [Хх]([аиое]х?)+ — сначала идёт заглавная или строчная “х”, а потом произвольное ненулевое количество гласных, которые (возможно, но не обязательно) перемежаются одиночными строчными “х”. Однако это выражение решает проблему лишь частично — под это выражение попадут и такие строки, как, например, “хихахех” — кто-то может быть так и смеётся, но допущение весьма сомнительное. Очевидно, мы можем использовать набор из всех гласных лишь единожды, а потом должны как-то опираться на результат первого поиска. Но как?… ## Запоминание результата поиска по группе (обратная связь) Оказывается, результат поиска по скобочной группе записывается в отдельную ячейку памяти, доступ к которой доступен для использования в последующих частях регулярного выражения. Возвращаясь к задаче с поиском HTML-тегов на странице, нам может понадобиться не только найти теги, но и узнать их название. В этом нам может помочь регулярное выражение <(.*?)>. ```html

Tproger — мой любимый сайт о программировании!

``` Результат поиска по всем регулярному выражению: “

”, “”, “”, “”, “”, “

”. Результат поиска по первой группе: “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 (?<=шаблон) | Позитивный просмотр назад | (?<=Сергей )Иванов | Сергей **Иванов**, Игорь Иванов (?){ // регулярное выражение создано конструтором с одним флагом 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){ 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 — граница слова); ## Найдите все натуральные числа, не находящиеся внутри или на границе слова; ## Найдите строчки, в которых есть символ * (. — это точно не конец строки!); ## Найдите строчки, в которых есть открывающая и когда-нибудь потом закрывающая скобки; ## Найдите пустые строчки;