
Разработчики делятся на два типа: тех, кто уже понимает регулярные выражения и порой решает сложные задачи одной строкой, и тех, кто все еще боится и всячески их избегает. Эта статья специально для вторых, чтобы им было проще стать первыми. Она либо поможет преодолеть «регекспофобию», либо усугубит ее. В любом случае, добро пожаловать под кат.
Используйте навигацию, если не хотите читать текст полностью:
→ Введение
→ Инструменты
→ Hello, world
→ Спецсимволы
→ Заглядываем в бездну
Введение
Регулярное выражение описывает некоторый образец (на английском — pattern), которому текстовые строки могут или соответствовать, или нет. Основные области применения: поиск, валидация, парсинг и устрашение.
-
Поиск. Найти все email-адреса в тексте
, чтобы отправить им письма счастья. - Валидация. Проверить, что введенный в форме email-адрес хотя бы отдаленно похож на настоящий.
- Парсинг. Разбить email-адрес на имя пользователя и домен.
- Устрашение. Наиболее полное регулярное выражение для валидации email-адресов можно посмотреть на этой странице.
С другой стороны, иногда регулярные выражения позволяют решать задачи, для которых они вообще не предназначены. Например, на этой странице описан крайне неэффективный, но работающий способ проверять число на простоту.
Каждый раз писать «регулярное выражение» утомляет. В англоязычном сленге прижились термины regex и regexp. Однако оба звучат как имена злодеев из аниме, поэтому в этой статье я буду время от времени использовать слово «регулярки».
Согласно легенде, «regular expressions» означает «обычные выражения», а регулярными их назвали, потому что поленились при переводе.
Кстати, есть мнение, что термин regex не всегда может быть синонимом regular expression. Но иногда может. Подробности можно прочитать в короткой статье.

Инструменты
Безусловно, из всех своих задач регулярки лучше всего справляются именно с устрашением. Однако благодаря работе ведущих египтологов появились сервисы, помогающие расшифровать эти загадочные письмена. Я чаще всего пользуюсь двумя: один красивый, другой — полезный. На самом деле оба красивые и полезные.
Regexper позволяет превратить регулярку почти любой степени запутанности в красиво оформленный граф. Например, есть такая регулярка для вещественных чисел, изобретенная древними шумерами:
[+-]?(\d*\.)?\d+
И вдруг она обретает смысл в виде интуитивно-понятной инфографики:

Если вы где-то откопали странную регулярку и хотите быстрее понять, что она делает, смело закидывайте в Regexper.
Правда, для особо запутанных случаев и граф будет непростым. Например, попробуйте отправить в сервис вышеупомянутую регулярку для email (правда, сперва нужно будет записать ее в одну строчку). Я не стал прикреплять картинку здесь, потому что она имеет 24 621 пикселей в ширину.
Второй крайне полезный ресурс — regex101. Помогает увидеть, как работает регулярка. А если она не работает или работает не так, то можно понять, в чем дело. В сервисе есть даже пошаговый дебаггер!

Hello, world
Хоть язык регулярных выражений и не является языком программирования, все-таки будем соблюдать традицию. Итак, открываем regex101 и вводим регулярное выражение:
hello, world
Да-да, регулярка может выглядеть как человеческий текст, а не только как клингонский некролог. Регулярному выражению «hello, world» соответствует одна-единственная строка — «hello, world».

(В некоторых|Во многих) языках программирования регулярному выражению можно задавать опции. Если ей будет Case Insensitive (обычно обозначается как i), то подойдут все варианты регистра: «Hello world», «Hello World», «HELLO WORLD», «hElLo wOrLd» и т. д.
В regex101 для этого достаточно кликнуть по буквам с опциями справа от строки ввода.

Ни для кого не секрет, что в слове «hello» две буквы «l». Давайте этот занимательный факт запишем с помощью языка регулярных выражений:
hel{2}o, world
Это то же самое выражение, что и предыдущее, но теперь уже слегка пробуждает Ктулху. Обратите внимание, что {2} работает только на букву «l», а не на все буквы, стоящее ранее.
Зачем нам это нужно? Не проще ли было оставить просто «ll»? Безусловно, в данном случае проще. Однако в реальной практике могут быть более сложные ситуации. Во-первых, символ может повторяться не два раза, а, например, тысячу. Во-вторых, есть возможность применить операцию не на один символ, а на группу, но об этом в другой раз.
Кроме того, можно указать не только конкретное число повторений, но и пределы: минимум и максимум. Например:
hel{2,5}o, world
Такая запись означает, что буква «L» может повторяться от двух до пяти раз.

А если второе число опустить, но запятую оставить, это уже будет означать «до бесконечности».
Например:
hel{2,}o, world
Такая запись уже означает, что в слове может быть любое количество букв «l», но не менее двух.

Оставим пока букву «l» в покое. В нашем примере вариативность по-настоящему пригодится, когда мы будем работать с пробелом. Допустим, нам нужно обрабатывать и те случаи, когда между словами не один пробел, а несколько:
hello, {1,}world

Срочная правка от заказчика: в конце фразы могут стоять восклицательные знаки, может быть даже сразу три. А может быть бесконечность, а может быть ни одного. Так и запишем:
hello, {1,}world!{0,}

Обратите внимание на последние две строчки. Если начинаются символы, не соответветствующие образцу, это не отбраковывает всю строку целиком, а только ту часть, которая не подходит. Иногда это здорово, а иногда это совсем не то, что нам нужно. Что с этим делать, обсудим позже.
Раз уж зашла речь про знаки препинания: а что, если кто-то забудет поставить запятую, а мы все равно хотим найти такие варианты? Значит, запятая у нас должна встречаться или ноль, или один раз:
hello,{0,1} {1,}world!{0,}
Вот теперь это похоже на друидскую бухгалтерию, как и положено хорошей регулярке!{3,}
Она уже достойна того, чтобы посмотреть ее в Regexper:

- Берем слово «hello».
- Запятую или берем, или обходим стороной.
- Пробел один берем точно, затем крутимся по нему любое число раз.
- Берем слово «world».
- Восклицательный знак или обходим стороной, или крутимся по нему.

Обратите внимание, что строка, где вообще нет пробела, не соответствует образцу. Дело в том, что мы указали, что хотя бы один пробел должен быть. Разберем этот случай чуть позже.
Спецсимволы
Чтобы не переполнять регулярку цифрами, ее можно переполнить спецсимволами. Для некоторых наиболее распространенных вариантов есть сокращенный способ записи:
{0,1} |
? |
Символ или есть, или нет |
{0,} |
* |
Символ встречается любое количество раз, включая 0 |
{1,} |
+ |
Символ встречается не менее одного раза |
hello,? +world!*
Теперь выражение выглядит почти как изначальное «hello, world», но насколько функциональнее оно стало!
Кстати, регулярное выражение, которому соответствуют оба имени злодеев англоязычных термина — regexp?А что, если нужно воспринимать спецсимволы как обычные: плюс как плюс, вопросик как вопросик? Мы можем их экранировать с помощью escape-символов (в англоязычной литературе это называется escape character). Косая черта перед спецсимволом означает, что он теряет сверхспособности и становится обычным. Такой криптонит в мире регулярок.
Пример:
hello\?
Соответствует строке «hello?», а вовсе не «hello\» и «hello».

Косая черта может экранировать и саму себя. Чтобы это сработало, ее нужно записать дважды: \\
В некоторых языках программирования косая черта уже является escape-символом в строке, поэтому, чтобы она стала escape-символом регулярного выражения, нужно написать \\. А чтобы она стала просто косой чертой, нужно написать \\\\. Да, чтобы косая черта осталась самой собой, ее нужно повторить четыре раза. Расскажите это людям, которые не понимают, за что айтишникам столько платят.
Возвращаемся к «hello, world». Кто-то может случайно написать не «hello», а «hallo». Давайте этот случай тоже обработаем.
Чтобы выбрать один символ из нескольких, нужно перечислить их внутри квадратных скобок. Выражению h[ea]llo соответствуют и «hello», и «hallo».

Раз уж мы заговорили о других языках, давайте и о русском подумаем. Как сделать, чтобы подходило и «hello», и «привет»?
Если написать [helloпривет], это выражение будет соответствовать просто одному символу из набора h,e,l,o, п, р, и, в, е, т. То есть одна буква подходит, а все слово — уже нет. Как быть?
Для этого есть спецсимвол |. Запишем hello|привет.

Стоп, а в регулярках можно использовать кириллицу? Конечно! Хоть емоджи. Хотя, конечно, это зависит от приложения или языка программирования, в которым вы будете их использовать, но прямого запрета на этот счет нет.
Если написать hello|привет, world, то слово «world» останется в правой части. То есть мы получим или «hello», или «привет, world».

А можно «world» как-то оставить общим (хоть это и странно будет смотреться в сочетании с приветом)? Чтобы ограничить действие вертикальной черты, можно использовать круглые скобки. Вообще, это далеко не единственная, и более того — не главная задача круглых скобок, но об этом позже. А пока:
(hello|привет), world
Можно писать сколько угодно вариантов:
(hello|привет|bonjour), world

Обратите внимание, что можно поставить пробелы вокруг прямой черты, чтобы было красиво:
(hello | привет | bonjour), world
Но в таком случае изменится и выражение, поскольку пробелы не просто для красоты, а являются частью выражения. Поэтому не вставляйте пробелы, если не уверены, что они нужны.


Кстати, по поводу пробелов. Есть люди, которые не ставят пробел после запятой, а есть те, кто ставят перед ней. Как сказал бы Шелдон Купер, в мире не хватит ромашкового чая, чтобы унять ярость в моей груди. Но что поделать? Такие случаи тоже возможно нужно обрабатывать.
Попробуем такой вариант:
hello *,? *world

Работает.

Но есть две проблемы. Первая — строчка «helloworld». Так как у нас пробелы со звездочкой и запятая под вопросом, то подходят даже те варианты, в которых вообще нет разделителей. В тренировочном примере ничего страшного, но в реальной практике это распространенная проблема: как сделать, чтобы среди множества опциональных вариантов хотя бы один все-таки присутствовал? Это можно сделать разными способами: квадратным и круглым.
Квадратный способ
hello *[, ] *world

После «hello» могут идти пробелы (возможно, ни одного), затем точно должен быть какой-то разделитель (запятая или пробел), затем — еще пачка пробелов, возможно пустая.
Круглый способ
hello( *, *| +)world

Здесь у нас есть два варианта на выбор:
- запятая, с которой могут быть пробелы (а могут и не быть);
- пробелы, не менее одного.
Первую проблему решили, но есть вторая: в самом выражении пробел недостаточно ярко выражен. Не сразу очевидно, что звездочка относится именно к нему.
Помните, мы говорили, что косая черта превращает спецсимволы в простых смертных? Так вот, наоборот тоже работает. Если поставить косую черту перед простым магловским символом, он обретет сверхспособности. Конечно, не с каждым это работает, только с теми, у кого достаточно мидихлориан.
Например, буква s прекрасна тем, что с нее начинается слово space, поэтому \s — это спецсимвол, обозначающий пробел. И не только пробел, а вообще все пробелообразное: Tab и при определенных опциях перенос строк.
Перепишем наше выражение:
hello(\s*,\s*|\s+)world
Уже начинает походить на древнеегипетскую глаголицу, как и положено достойному регулярному выражению. Но при этом мы все еще примерно понимаем, что оно делает! В этом главный секрет регулярок — кто их пишет, тот их и понимает.
Заглядываем в бездну
Теперь соберем все вместе:
(h[ea]llo|привет|bonjour)(\s*,\s*|\s+)world!*

Уровень оккультизма достиг приемлемого.
В статье присутствует от нуля до бесконечности неточностей и упрощений. Некоторые из них допущены умышленно, чтобы сохранить развлекательный дух статьи и не отпугнуть вас излишней педантичностью и академичностью.
В следующей публикации рассмотрим различные возможности регулярных выражений более подробно.
Комментарии (46)
lost_embedder
07.10.2024 16:49+11Главная проблема регулярок для меня - изучать их в отрыве от конкретной задачи ленишься, а при решении конкретной задачи они эмбеддеру нужны раз в год. Поэтому каждый раз открываешь их для себя с нуля :)
За мануал спасибо, было наглядно. Увижусь с ним, по моим расчетам, месяца через четыре :)
DandyDan Автор
07.10.2024 16:49+5Ну, прямо ембедить наверно редко надо, но наверняка возникают задачи, связанные с процессом разработки: логи распарсить, найти и заменить что-то в коде и т.д. В конечном итоге время, потраченное на изучение, окупается.
pelepelin
07.10.2024 16:49+4Разработчики делятся на два типа: тех, кто уже понимает регулярные выражения
А разработчики, которые используют регулярные выражения, делятся на тех, кто уже решал проблемы с их производительностью, и тех, кто ещё нет ;)
hello *,? *world
оно
Grey83
07.10.2024 16:49+3Вообще регулярных выражений существует больше одного стандарта: https://ru.wikipedia.org/wiki/Регулярные_выражения#Разновидности_регулярных_выражений
И про регулярки раз сто на хабре писали: https://habr.com/ru/search/?target_type=posts&order=relevance&q=[regexp]
DandyDan Автор
07.10.2024 16:49+2Главная цель и отличительная черта этой серии статей — максимальная простота. Нюансов очень много, и если бросать их на читателя все разом, получится просто перегруз информацией.
В том числе и про различные "наречия" планировал упомянуть в следующей главе. Без конкретных примеров, чем именно отличаются, просто как сам факт.
Grey83
07.10.2024 16:49+1Вот только от этой «незначительной» детали обычно и возникает куча проблем.
В первую очередь нужно определить какая система используется, а уже потом составлять регулярку по конкретным правилам этой системы. Потому что, в лучшем случае, составленая для другой системы регулярка не сработает.
unreal_undead2
07.10.2024 16:49+1Угу - писать про регулярки, не уточняя диалект, всё равно что играть в преферанс, не договорившись о системе.
DandyDan Автор
07.10.2024 16:49В любом преферансе 32 карты в колоде, даже в "гусарике". В регулярках тоже есть общая база, а диалекты в основном меняют/добавляют advanced фичи.
unreal_undead2
07.10.2024 16:49+1Да какие advanced, периодически путаюсь, где какие скобки надо экранировать.
SadOcean
07.10.2024 16:49+6Я признаю мощность и красоту регулярок, но вместе с тем есть и пару проблем:
- Они очень информационно плотные, их тяжело читать и разбирать, получается writeonly подход.
- Мне они нужны довольно редко, в лучшем случае раз в пару месяцев. Соответственно каждый раз приходится гуглить, особенно если задача нетипичная. Python, С# и прочие Java имеют какие то маленькие нюансики в синтаксисе стандартного средства регулярок, поэтому без гуглежа можно не понять, что конкретно не завелось.
А так хорошая штука конечно.
Для слабых духом есть библиотеки, собирающие регулярки из человекочитаемых конструкций наподобие LINQ, к примеру вот такая:
https://github.com/ricoapon/readable-regexDandyDan Автор
07.10.2024 16:49+1Спасибо за ссылку на библиотеку!
Тоже когда-то задумывался о создании чего-то подобного, чтобы вместо иероглифов были простые человеческие конструкции. Однако, когда натолкнулся на regexper, понял, что графический язык лучше помогает читать регулярки, чем даже самый многословный текст. По крайней мере, для меня так.
SadOcean
07.10.2024 16:49Я, если честно, тоже не уверен, что это оптимально в общем случае (когда пишешь код для других)
eyeDM
07.10.2024 16:49+4ИМХО, самое страшное в регулярках то, что в разном софте слегка разный синтаксис. grep, sed, nginx, apache, php, javascript... Маски и "полноценные regex", PCRE и POSIX...
Вот и получается, что, кажется, знаешь общие принципы, подстановки... Но всё-равно приходится гуглить, например, как конкретно в этом случае организовать именованную группу, как отключить "жадный" режим" и т.п.
Fedorkov
07.10.2024 16:49+3В прошлом году сдавал ЕГЭ по информатике (чтобы поступить в вуз), где-то 4 или 5 задач решил одной строчкой регекса, потратив на каждую пару минут.
sibel
07.10.2024 16:49+2В реальной разработке, если вижу регулярку, там где можно обойтись парой-тройкой ифчиков, то мне хочется убивать. Как правило регулярки сильно ухудшают читаемость кода, и крайне плохо модифицируются кем либо, кроме автора регулярки. Поэтому, если можно не писать регулярку, то ни в коем случае ее писать не следует
alamat42
07.10.2024 16:49Имхо, регулярки в первую очередь полезны не как вставки в продакшн коде, а для ситуативных задач вроде поиска/замены в коде/логах, ну и для скриптов, которые читать будет только автор.
eulampius
07.10.2024 16:49У регулярных выражений есть огромный плюс - они могут быть параметром, который выводится в настройки.
И мне, например, наоборот не нравится, когда потенциально изменяющуюся логику жёстко прописывают ифчиками или городят стратегии и фабрики валидаторов, вместо регулярки. Естественно, при условии, что использование регулярных выражений не спровоцирует кризис производительности. А то с другой стороны мне доводилось видеть, как регулярки использовали для добывания значений параметров из файла, каждый раз скармливая весь файл для каждого искомого параметра )
unreal_undead2
07.10.2024 16:49+2Главное не переборщить, а то получится что-то типа конфигов sendmail .
venanen
07.10.2024 16:49Для меня регулярки бывают двух видов - распространённые (валидация email, телефона, адреса и т.д.), и локальные - массово что-то в проекте заменить.
Вторые пишутся обычно за 2 минуты по шпаргалке, а первые лучше всего взять с гитхаба или СО.
P.S. Если вы используете сложную регулярку, и ее нельзя заменить - оберните ее хотя бы в читаемый вид - или в переменную типа phoneValidationRegExp, или в функцию с читаемым названием. Иначе потом будет больно.iroln
07.10.2024 16:49валидация email
Всегда вспоминается это, когда-то кто говорит про валидацию email в контексте регулярных выражений. :)
https://pdw.ex-parrot.com/Mail-RFC822-Address.htmlРегулярные выражения - это типичный write only код, отлично подходит для одноразовых скриптов, но в продакшен коде сложные регулярки - это почти всегда боль, особенно если их нужно поддерживать и дописывать/переписывать под новые требования. Без тулов, раскладывающих регулярки на понятные читаемые блоки, не обойтись.
unreal_undead2
07.10.2024 16:49Кстати, нет ли (проверенных в промышленных условиях) библиотек, оперирующих синтаксисом регулярных выражений в виде объектов? Что-нибудь типа one_or_more(one_of("abc"))+zero_or_more(whitespace)+...
WayMax
07.10.2024 16:49Зачем? Зачем эти два с половиной листа в Ворде?
Разве валидный email это не: "строка не нулевой длины" + "собачка" + "строка не нулевой длины" + "точка" + "строка не нулевой длины" ?
unreal_undead2
07.10.2024 16:49Читайте мануал . Конкретно формат адреса расписывается в п.6, но остальные части тоже релевантны. Например, в e-mail адресе допускаются комментарии в определённом формате.
WayMax
07.10.2024 16:49Ок, допустим "точек" может быть несколько, но это полностью подпадает под правило "строка не нулевой длины ". Зачем городить для этого что-то монструозное?
adante
07.10.2024 16:49+1Это очень интересно, но вот ИМХО любую регулярку уже можно смело отдавать чатгпт и забыть про это.
Все равно если ты их не пишешь каждый день, ты их пишешь с гуглом и справочником. При этом практическая польза от скилла «умею читать/писать регулярки» сомнительная.
avaava
07.10.2024 16:49Умею читать от умею писать отличается ОЧЕНЬ сильно.
Мне они практически не нужны, использую ОЧЕНЬ редко, и, как мне кажется, для ситуативных случаев они хороши. Но вот разбираться с чужой (или старой своей, которой более полугода) - боль. Проще новую накидать.
И отдать её написать кому-то (той же нейронке) это равносильно тому, что разбираться в чужой (её же надо проверить). Спасибо, ни за какие деньги.
unreal_undead2
07.10.2024 16:49+2Очень удобны просто для разовой замены в редакторе. Для кода, который надо поддерживать, часто прав jwz:
Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems.
Spearance
07.10.2024 16:49Сам автор курса по регуляркам, что могу сказать на последний комментарий, если в вашей работе нет задач в которых они нужны — вы просто мало и не глубоко работаете с текстом. Регулярки не панацея, но некоторые задачи позволяют решать в разы быстрее, чем то же самое описать обычными условиями. Сам ими пользуюсь везде, при написании кода, в терминальной строке, в корректуре приходящих извне объемных данных...
По сути статьи, лучше примеры брать более жизненные. И раз уж примеры синтетические, то не нужна очень большая точность совпадения, а потому: `hello[ ,]*world` вполне достаточно
DandyDan Автор
07.10.2024 16:49hello,,,,,world
helloworld
hello , , , , ,world
Spearance
07.10.2024 16:49Нормально для простого примера, аналогично !*, иначе получается где-то мы точно ищем, а где-то допускаем вольности. Иначе:
(h[ea]llo|привет|bonjour)(\s*,\s*|\s+)world!*
Лучше писать без вторых круглых скобок, нам не нужно сохраняющее состояние, не нужен вариант \s+, его покрывает \s* только нужно исключить запятую ,?
(h[ea]llo|привет|bonjour)\s*,?\s*world!*
DandyDan Автор
07.10.2024 16:49Это чтобы не допустить вариант helloworld.
Да, я понимаю, что примеры надуманные и нереалистичные. Но для меня главное была простота и последовательность изложения. Любой реалистичный пример ломает последовательность.
Spearance
07.10.2024 16:49проходил это, когда курс писал, примеры приходилось из пальца высасывать, но, после объяснения скобок и квантификаторов, пффф... и тебе разбор CSV, и плавающие переносы в тексте, и триады в числах... желаю не перегореть ;)
unreal_undead2
07.10.2024 16:49при написании кода, в терминальной строке, в корректуре приходящих извне объемных данных...
Хорошие примеры, но всё это скорее разовые задачи, чем часть production кода - и подходы могут быть разные. Скажем, если надо быстренько посмотреть на какие-то паттерны в бинарном коде - наверное, пройдусь регулярками по выхлопу objdump -d. Если надо добавить такую фичу в продукт - скорее возьму для парсинга готовую библиотеку (скажем, из llvm), даже если на входе именно дизассемблированный текст.
radioxoma
07.10.2024 16:49Регулярки приятны тем, что не нужно уметь программировать, чтобы пользоваться sed/grep и обрабатывать массивы текста. Ну и поиск в Sublime, Libreoffice.
andkartsev
Пойду попью ромашкового чаю.