Разработчики делятся на три типа: те, кто уже понимает регулярные выражения и порой решает сложные задачи одной строкой; те, кто все еще боится их и всячески избегает; и те, кто уже прочитал первую часть этой серии статей и полон оптимизма разобраться с этими магическими письменами. Эта статья специально для третьих, чтобы им было проще стать первыми.
Используйте навигацию, если не хотите читать текст полностью:
→ Пред(исловие|остережение)
→ Диалекты
→ Что угодно — и точка
→ Лекарство от жадности
→ Альфа и Омега
→ О квадратном
→ Опции
→ Финал
Пред(исловие|остережение)
В предыдущей статье моей основной задачей было показать, что регулярные выражения — это не так страшно, как обычно представляют. Увы, это не совсем так. Регулярные выражения прошли долгий путь от чисто математической абстракции до конкретных реализаций и в процессе обросли массой подробностей, которые не всегда логичны.
Чтобы было проще к этому относиться, нужно воспринимать регулярки как некое тайное знание. А в любом тайном знании всегда есть контринтуитивные штуки, которые невозможно понять логикой. Только зазубрить. Все ради того, чтобы посвященные могли снисходительно смотреть на непосвященных и на вопрос «А почему так?» отвечать: «Просто потому что здесь так принято».
Диалекты
Важно понимать, что не существует единого стандарта регулярных выражений. Чтобы регулярки было сложно не только читать, но и писать, каждая школа магии добавила в них что-то от себя. В результате существует с десяток основных диалектов (по английски flavors — привкусов) и несколько десятков специфичных.
Отличия между диалектами самые разные: некоторые фичи могут отсутствовать вовсе; некоторые присутствуют, но имеют отличающийся синтаксис; некоторые имеют одинаковый синтаксис, но работают по-разному. Короче, скучать не придется.
Зачем столько диалектов? Люди задавались этим вопросом на StackOverflow еще 15 лет назад. Ситуация с тех пор лучше не стала.
Однако, не пугайтесь. Разбираться в тонкостях ароматов, букетов и терруаров пока не будем. Разберем базовые ванильные вещи, более-менее общие для большинства диалектов. Однако, не стоит слепо верить всему, что написано в этой статье — всегда сверяйтесь с документацией вашего языка программирования.
К счастью, Regex101 поддерживает несколько основных диалектов. А Regexper, увы, — только диалект JavaScript.
Что угодно — и точка
Один из наиболее популярных, но опасных специальных символов — точка. Точка означает вообще любой символ кроме переноса строки. Такой джокер в мире регулярок.
Соответственно, такое регулярное выражение означает «что угодно сколько угодно раз». То есть ему соответствует вообще любая строка, от пустой до пентиллиардов смеющихся смайликов:
.*
Например, мы хотим найти все слова, которые начинаются на «к» и заканчиваются на «а». Самый очевидный подход:
к.*а
Получаем:
Вроде прекрасно работает, даже для слов с дефисом и несколькими «а» в середине. Так в чем же опасность? Дело в том, что тесты на картинке специально подобраны так, чтобы можно было сказать: «на моей машине все работает». Однако, в реальности все не так очевидно.
Совсем не то, что хотели. Вместо отдельных слов попадаются и части слова, и части соседних слов, и даже целые предложения.
Причин несколько:
- Точка действительно означает любой символ, в том числе пробел.
- Мы никак не указали, что хотим отдельные слова.
- Звездочка жадная.
Лекарство от жадности
Как известно, на данный момент человечество алгоритмически реализовало только два смертных греха из семи: жадность и лень, остальные пять пока ожидают своего часа.
Все квантификаторы (*, + и ?) по умолчанию являются «жадными», то есть стараются загрести как можно больше подходящих символов. Если можно взять — надо брать, такой у них принцип.
Рассмотрим стандартную ошибку, допускаемую при написании регулярок. Например, мы хотим найти в коде все строковые константы, то есть то, что начинается и заканчивается двойными кавычками.
".*"
Ситуация та же, что и в предыдущем примере. Как видим, любой символ между кавычек — это действительно любой символ, в том числе и сами кавычки. Один из способов это исправить — сделать квантификаторы ленивыми. Делается это при помощи добавления знака вопроса (?) после квантификатора.
- *? — ленивая звездочка
- +? — ленивый плюс
- ?? — ленивый вопрос
У ленивых принцип обратный: если можно уйти пораньше с работы — надо уходить. При первой же подходящей возможности они останавливаются.
".*?"
Теперь работает так, как задумывалось.
Для наглядности посмотрим, как это выглядит на графе (обратите внимание на стрелки).
Жадный. Приоритетное направление — повторить, даже если встречаем кавычку.
Ленивый. Приоритетное направление — пропустить, как только встречаем кавычку.
Жадность можно отключать и для фигурных скобок по тому же принципу. Например, выражение a{2,5}? будет искать от двух до пяти букв «а» подряд, но уже без особого энтузиазма.
Альфа и Омега
Как мы неоднократно видели, строка, соответствующая регулярному выражению, может находиться в середине текста. И это хорошо! Но только в том случае, когда мы выполняем поиск. Однако это не так хорошо, когда нужно выполнить валидацию.
Для простоты рассмотрим максимально нереалистичный пример. Допустим, мы хотим с помощью регулярных выражений проверять пароль. Разумеется, никогда не стоит этого делать в реальной жизни! Однако статья у нас имеет скорее развлекательный, чем поучительный характер, поэтому почему бы и нет. И, раз уж начали делать опрометчивые поступки, то и пароль будет password.
Соответственно, если строка соответствует регулярному выражению password, то и пароль правильный. Вроде бы логично. Проблема в том, что во многих языках для того, чтобы строка считалась валидной, она не обязательно должна вся соответствовать регулярному выражению. Достаточно только какой-то подстроки! Поэтому password1234, passwordpassword, 111password111 и так далее — все это будет считаться правильным паролем.
Не особо безопасно! Как это исправить (помимо того, что не стоит использовать регулярки для проверки пароля)? Нужно указать, что у нас ничего не должно быть ни до, ни после искомой строки.
Для этого есть две руны:
^ — руна дома, символизирует рождение, свет, начало строки.
$ — руна доллара, символизирует крах, тьму, конец строки.
Соответственно, такому выражению подойдет только строка password без каких-либо приставок и окончаний.
^password$
Теперь подходит только password.
Идем дальше. Как и в жизни, дом и доллар не обязательно должны быть вместе, в некоторых случаях достаточно чего-то одного. Например, выражение ^однажды соответствует слову «однажды» только в начале строки,
А выражение и поделом$ соответствует словосочетанию «и поделом» только в конце строки.
Обратите внимание, что выражению будут соответствовать только указанные слова, а не вся строка. Если интересует строка целиком, начинающаяся на «однажды», можно написать так:
^однажды.*
И соответственно:
.*и поделом$
Помимо спецсимволов для начала и конца строки, есть спецсимвол для начала/конца слова — \b (от слова boundary). Однако, у него достаточно странные понятия о том, что считать словом, а что нет. Об этом поговорим чуть позже.
О квадратном
Как уже говорилось в предыдущей статье, квадратные скобки позволяют перечислить набор интересующих нас символов.
Например, [aeiou] означает «любая английская гласная» (в нижнем регистре).
А выражение [0123456789] означает «любая десятичная цифра».
Если нужны не только гласные, а вообще все буквы английского алфавита, то, конечно, можно написать и так:
[abcdefghijklmnopqrstuvwxyz]
Прекрасный шанс напевать песенку про алфавит, но получается уж больно громоздко. Чтобы не выгореть раньше времени, можно использовать интервал:
[a-z]
Соответственно, для цифр можно написать:
[0-9]
Логично предположить, что и для русских букв можно написать:
[а-я]
Конечно же, это не так. Задавая интервал, мы имеем в виду все символы, чьи коды находятся в указанном промежутке. Те, у кого в ФИО есть буква «ё», наверняка уже поняли, куда я клоню: [а-я] означает все русские буквы, кроме «ё».
К счастью, перечисления и интервалы можно совмещать. Выражение для всех русских букв, включая «ё», будет выглядеть так:
[а-яё]
Интервалов тоже можно указать несколько, например для шестнадцатеричных цифр можно написать:
[0-9a-f]
Но есть нюанс: не только шестнадцатеричные цифры удовлетворяют выражению.
Ну и, конечно, можно совмещать несколько интервалов и перечислений. Любую букву английского и русского алфавитов обоих регистров можно записать следующим образом (обратите внимание, что если не используется опция Case Insensitive, то буквы разных регистров нужно прописывать явно):
[a-zа-яёA-ZА-ЯЁ]
Инверсия
Иногда набор символов настолько велик, что проще указать, что в него не должно входить. Помните опасную точку, которая означает любой символ? Иногда нужно указать «любой символ, кроме…». В таком случае сразу после открывающей скобки ставим ^. Да-да, та самая руна дома, которая во внешнем мире означала начало строки, внутри квадратных скобок означает отрицание. Не спрашивайте, почему так, просто здесь так принято. Тайное знание, как никак.
Примеры:
- [^aeiou] — что угодно кроме гласных букв (в том числе и вовсе не буквы),
- [^0-9] — что угодно кроме цифр,
- [^”] — что угодно кроме кавычки.
Если нужно использовать «домик» как обычный символ, можно просто поставить его не в начало строки, а куда-нибудь в середину. Например, [!@#$%^&*] означает, что кошка прошла по клавиатуре.
Экранирование
Внутреннее содержимое квадратных скобок — это свой отдельный мир, в котором действуют свои законы физики, не такие, как во внешнем мире. Например, большинство спецсимволов здесь теряют сверхспособности.
То есть [+*?.] — это именно плюс, звездочка, вопросительный знак или точка — обычные символы, несмотря на то, что их не пришлось экранировать.
Но тут есть ловушка. Выражение ниже может показаться перечислением арифметических операций: умножение, вычитание и сложение:
[*-+]
Однако минус здесь не просто символ, а спецсимвол, задающий интервал. Получается, что на самом деле здесь задан интервал от «*» (код 42) до «+» (код 43), то есть включает в себя только звездочку и плюс, но не минус.
Если же поменять порядок, чтобы это уже не было интервалом, то минус теряет сверхспособность:
[-*+]
Таким образом, позиция минуса влияет на его смысл: в начале или в конце это просто символ, а в середине означает интервал. С домиком «^» все наоборот: в середине списка это просто символ, а в начале означает инверсию. Забавное (хотя, если задуматься, вполне логичное) исключение: если в начале стоят и домик, и минус, то домик будет спецсимволом, а минус — обычным символом.
Примеры:
- [^-] — все, кроме минуса,
- [-^] — минус или домик,
- [^-+] — все, кроме минуса и плюса,
- [^+-] — все, кроме минуса и плюса,
- [-^+] — минус, домик или плюс,
- [-+^] — минус, домик или плюс,
- [+^-] — минус, домик или плюс,
- [+-^] — внезапно! интервал от + (код 43) до ^ (код 94), включающий в себя цифры, заглавные буквы и другие символы!
Некоторые символы экранировать все-таки придется. Например, что делать, если нужно использовать квадратные скобки внутри квадратных скобок? К счастью, косая черта работает и здесь:
[\[\]]
Косая черта работает и в обратную сторону: если поставить ее перед обычным символом, он станет «особенным»:
[a-z\s] — буквы или пробел, или таб, или перенос строки
Встроенные наборы
Некоторые наборы символов настолько популярны, что для них придумали сокращенные варианты.
Из первой части статьи о регулярках мы уже знаем \s, который означает пробел или таб. Любая цифра — это \d (от digit) = [0-9]. Латинская буква, цифра или подчеркивание — \w (от word) = [a-zA-Z0-9_].
Обратите внимание, что \w включает подчеркивание, но не включает никакие алфавиты помимо латинского, поэтому больше подходит для языков программирования (да и то не для всех), чем для естественных. Вобщем, используйте осмотрительно.
Помните, мы упоминали \b? Так вот, он означает границу между \s и \w (в любом порядке). Начало строки также является границей слова. Однако, так как \w считает цифры и подчеркивание частью слова, а любые «иностранные» буквы не считает частью слова, то и \b работает не совсем так, как хотелось бы. Просто здесь так принято.
У каждого из этих суперсимволов есть злой двойник.
- \S — что угодно, кроме пробелов, табов и переносов строк,
- \D — что угодно, кроме цифр,
- \W — что угодно, кроме [a-zA-Z0-9_],
- \B — то место, где точно нет разрыва слова.
Многабукв
Помните, мы говорили, что инструкции действуют только на один символ предшествующей инструкции? Так вот, вся конструкция с квадратными скобками считается одним символом (и в принципе, так оно и есть).
- [A-Z]{4} — четыре заглавные буквы, например HOPE, BEAF или XHZF;
- [A-Z0-9]{4} — четыре заглавные буквы или цифры, например R2D2;
- #[0-9a-fA-F]{6} — цвет в шестнадцатеричном формате, например #e0e0e0;
- \d+ — целое неотрицательное число, непример 123;
- [-+]?\d+ — целое число с опциональным знаком, например, 123, +123, -123;
- \w+ — последовательность из английских букв, цифр или подчеркиваний, например int_2_str или 123password;
- \b[_a-zA-Z]\w+\b — идентификатор в некоторых языках программирования. Отличается от предыдущего примера тем, что не может начинаться с цифры и является целым словом;
- “[^”]*” — текст в кавычках, здесь вместо ленивого режима мы используем класс «что угодно кроме кавычек»;
- к[а-яёА-ЯЁ]*а — последовательность кириллических букв, начинающаяся на «к» и оканчивающаяся на «а», но не обязательно слово. Может быть и часть слова;
- \bк[а-яёА-ЯЁ]*а\b — хотелось бы, чтобы это, наконец, стало отдельным словом, но, увы, нет — \b так не работает.
Опции
Во многих движках можно указывать опции регулярного выражения. Их набор и смысл могут отличаться, поэтому рассмотрим только самые распространенные.
- i — case insensitive,
- g — global,
- m — multiline,
- s — single line.
Case Insensitive переводится как «чемодан бесчувственный», и название говорит само за себя. Обратите внимание, что если эта опция присутствует, нечувствительность к регистру относится только к тексту, на спецсимволы это никак не влияет, то есть \s и \S продолжают означать противоположные вещи.
Global означает глобальный поиск. Без этой опции будет найдено только первое подходящее соответствие. А с ней — все возможные соответствия.
Multiline влияет на то, как воспринимаются спецсимволы начала и конца строки: ^ и $, точнее, что считать строкой. В английском языке есть два слова: string (как последовательность символов) и line (как элемент текста). В русском словосочетание «многострочная строка» звучит как масло масляное, поэтому используем слово «текст». Итак, если опция отключена, то ^ — начало всего текста, $ — конец всего текста. Если опция включена, то ^ — начало каждой строки текста, $ — конец каждой строки текста.
Single line — неудачно названная опция, потому что может показаться, что она является антонимом к multiline, но это не так. Multiline и single line могут быть активны одновременно, они не взаимоисключающие. Исторически так сложилось, что точка означает любой символ, кроме переноса строки. Так вот, опция single line изменяет это поведение: если опция включена, то точка означает вообще все, в том числе и перенос строки, что делает ее еще мощнее и опаснее.
Финал
Давайте соберем все вместе и все-таки решим задачу с козой и капустой. Что нам нужно?
- Слово начинается на «к», это может быть как начало, так и середина строки.
- Заканчивается на «а», опять же это может быть как конец строки, так и ее середина.
- Между «к» и «а» могут стоять кириллические буквы и дефис.
- Это должно быть целое слово, а не часть.
- Мы не можем использовать \b, потому что кириллица.
Можно разделить выражение на пять частей:
- То, что идет перед словом: начало строки или НЕ буква.
- Буква «к».
- Середина слова: несколько букв или дефисов.
- Буква «а».
- То, что идет после слова: конец строки или НЕ буква.
- Буква: [а-яё].
- Буква или дефис: [а-яё-].
- Антибуква: [^а-яё].
- Антибуква или начало строки: (^|[^а-яё]).
- Антибуква или конец строки: ($|[^а-яё]).
Собираем все вместе, и получаем типичную запись в паспорте Джейсона Борна:
(^|[^а-яё])к[а-яё-]+а($|[^а-яё])
Вот так это выглядит на графе:
Почти работает! Единственная оставшаяся проблема: выражение захватывает не только слово, но и символ перед и после него. Как это исправить, обсудим в последующих статьях.
А на этом пока все. Квадратное обсудили, в следующей части поговорим о круглом.