Если вам когда-нибудь приходилось работать с командной строкой, вы, вероятно, использовали маски имён файлов. Например, чтобы удалить все файлы в текущей директории, которые начинаются с буквы "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-теги в строке
<p><b>Tproger</b> — мой <i>любимый</i> сайт о программировании!</p>
Очевидное решение <.*> здесь не сработает — оно найдёт всю строку целиком, т.к. она начинается с тега абзаца и им же заканчивается. То есть содержимым тега будет считаться строка
p><b>Tproger</b> — мой <i>любимый</i> сайт о программировании!</p
Это происходит из-за того, что по умолчанию квантификатор работают по т.н. жадному алгоритму — старается вернуть как можно более длинную строку, соответствующую условию. Решить проблему можно двумя способами. Первый — использовать выражение <[^>]*>
, которое запретит считать содержимым тега правую угловую скобку. Второй — объявить квантификатор не жадным, а ленивым. Делается это с помощью добавления справа к квантификатору символа ?. Т.е. для поиска всех тегов выражение обратится в <.*?>
.
Иногда для увеличения скорости поиска (особенно в тех случаях, когда строка не соответствует регулярному выражению) можно использовать запрет алгоритму возвращаться к предыдущим шагам поиска для того, чтобы найти возможные соответствия для оставшейся части регулярного выражения. Это называется ревнивой квантификацией. Квантификатор делается ревнивым с помощью добавления к нему справа символа +. Ещё одно применение ревнивой квантификации — исключение нежелательных совпадений. Так, паттерну ab*+a в строке “ababa” будут соответствовать только первые три символа, но не символы с третьего по пятый, т.к. символ “a”, который стоит на третьей позиции, уже был использован для первого результата.
Для нашего шаблона “смеющегося” междометия осталась самая малость — учесть, что буква “х” может встречаться более одного раза, например, “Хахахахааахахооо”, а может и вовсе заканчиваться на букве “х”. Вероятно, здесь нужно применить квантификатор для группы [аиое]+х, но если мы просто напишем [аиое]х+, то квантификатор + будет относиться только к символу “х”, а не ко всему выражению. Чтобы это исправить, выражение нужно взять в круглые скобки: ([аиое]х)+.
Таким образом, наше выражение превращается в Хх+ — сначала идёт заглавная или строчная “х”, а потом произвольное ненулевое количество гласных, которые (возможно, но не обязательно) перемежаются одиночными строчными “х”. Однако это выражение решает проблему лишь частично — под это выражение попадут и такие строки, как, например, “хихахех” — кто-то может быть так и смеётся, но допущение весьма сомнительное. Очевидно, мы можем использовать набор из всех гласных лишь единожды, а потом должны как-то опираться на результат первого поиска. Но как?…
Оказывается, результат поиска по скобочной группе записывается в отдельную ячейку памяти, доступ к которой доступен для использования в последующих частях регулярного выражения. Возвращаясь к задаче с поиском HTML-тегов на странице, нам может понадобиться не только найти теги, но и узнать их название. В этом нам может помочь регулярное выражение <(.*?)>.
<p><b>Tproger</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х?)*|[Аа]х?(?:ах?)+
Если группа используется только для группировки и её результат в дальнейшем не потребуется, то можно использовать группировку вида (?:шаблон). Под результат такой группировки не выделяется отдельная область памяти и, соответственно, ей не назначается номер. Это положительно влияет на скорость выполнения выражения, но понижает удобочитаемость.
Атомарная группировка вида (?>шаблон) также, как и группировка без обратной связи, не создаёт обратных связей. В отличие от неё, такая группировка запрещает возвращаться назад по строке, если часть шаблона уже найдена.
Модификаторы (В котлине не поддерживаются)
Модификаторы действуют с момента вхождения и до конца регулярного выражения или противоположного модификатора. Некоторые интерпретаторы могут применить модификатор ко всему выражению, а не с момента его вхождения.
нечувствительность выражения к регистру символов (англ. 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, или +, или -, или …, или число, или любое количество пробелов. Без тройных кавычек нам пришлось бы дважды записать каждый из .
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
Во втором варианте создадим регулярное выражение через метод строки:
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 включает в себя следующие свойства:
Некоторые другие полезные методы, связанные:
Мини-пример:
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? А в строке JavaScript?
Напишите регулярное выражение для поиска HTML-цвета, заданного как #ABCDEF, то есть символ "#" и затем 6 шестнадцатеричных символов.
Арифметическое выражение состоит из двух чисел и операции между ними, например:
Список операций: “+”, «-», “*” и “/”.
Также могут присутствовать пробелы вокруг оператора и чисел.
Напишите регулярное выражение, которое найдёт как всё арифметическое действие, так и (через группы) два операнда.