Регулярные выражения в JavaScript: шаблоны для поиска по тексту
Регулярные выражения (или просто regex) описывают форму строки: «четыре цифры подряд», «слово с запятой в конце», «что-то похожее на email». В JavaScript регулярку можно создать двумя способами:
Литеральная запись - со слэшами вокруг шаблона и флагами после закрывающего слэша - это то, что вы будете использовать чаще всего. А вот new RegExp(...) пригодится, когда сам шаблон динамический: например, собирается из пользовательского ввода или переменной.
Буква i в конце - это флаг. i означает поиск без учёта регистра. Про флаги расскажу чуть ниже.
test: есть ли совпадение?
Самый простой вопрос, который можно задать регулярке - «а есть ли в строке совпадение?». Для этого и нужен test:
\d - это «любая цифра». Метод test возвращает строго true или false, больше ничего. Если вам нужен ответ в духе «да/нет» - проверить поле формы, отфильтровать массив - test подойдёт как нельзя лучше.
Метод match: достаём найденный текст
Когда нужен сам кусок текста, который совпал, пригодится метод строки match:
Без флага g метод match возвращает массив, где лежит первое совпадение вместе с метаданными (index, input). С флагом g на выходе - обычный массив строк со всеми найденными совпадениями. Если же ничего не нашлось, вернётся null, а не пустой массив - так что не забудьте это проверить:
Флаги меняют поведение паттерна
Флаги ставятся после закрывающего слэша и управляют тем, как работает поиск. Вот те, что пригождаются чаще всего:
g- глобальный поиск: находит все совпадения, а не только первое.i- без учёта регистра.m- многострочный режим:^и$совпадают с началом и концом каждой строки, а не всего текста.s- режим dotall: точка.начинает матчить и переводы строк.u- поддержка Unicode, без неё многие эмодзи и не-ASCII символы в паттернах работать не будут.
Без флага m символ ^ цепляется только к самому началу строки. А с флагом m - к началу каждой строки, поэтому в выдачу попадут и Roses, и Violets.
Классы символов и квантификаторы
Кирпичики, из которых собирается большинство regex-шаблонов в JavaScript:
\d- цифра,\w- символ слова (буква, цифра или подчёркивание),\s- пробельный символ.[abc]- один из символов a, b или c.[^abc]- что угодно, кроме перечисленного.[a-z]- диапазон..- любой символ, кроме перевода строки.*- ноль или больше,+- один или больше,?- ноль или один.{3}- ровно три,{2,5}- от двух до пяти,{2,}- два и больше.^- начало,$- конец.
Собираем всё вместе:
\b - это граница слова, невидимая черта между символом слова и символом не-слова. Пригождается, когда нужно искать «слово целиком».
Группы захвата в regex: запоминаем части совпадения
Круглые скобки создают группу и запоминают то, что в неё попало. Методы exec и match возвращают эти захваты вместе с самим совпадением:
Индекс 0 - это полное совпадение, а каждая группа захвата идёт после него по своему индексу. Считать группы по номерам становится неудобно, как только их больше двух, поэтому лучше давать им имена:
Именованные группы захвата делают код на стороне вызова понятнее и не ломаются, если порядок групп в шаблоне поменяется.
replace: переписываем найденный текст
Метод replace принимает шаблон и замену. В роли замены может выступать как строка, так и функция:
Без флага g заменится только первое совпадение. Классическая ошибка - забыть этот флаг, а потом ломать голову, почему второй email всё ещё выглядит криво.
В строке замены можно использовать обратные ссылки. $1, $2 и так далее ссылаются на группы захвата, а $<имя> - на именованные группы:
Если задача сложнее обычной замены, передавайте функцию. На вход она получает само совпадение и захваченные группы:
Подчёркивание - это полное совпадение (оно нам не нужно), а n - первая группа захвата. Связка «регулярка + функция-заменитель» закрывает большинство реальных задач по обработке текста.
matchAll: все совпадения вместе с группами
String.prototype.matchAll возвращает итератор по всем совпадениям вместе с их группами захвата - то, что обычный match с флагом g сделать не умеет:
matchAll работает только с флагом g. Без него получите TypeError. Если нужен произвольный доступ к элементам, а не перебор в цикле, разверните результат в массив: [...text.matchAll(email)].
Экранирование специальных символов
Символы . * + ? ( ) [ ] { } | \ ^ $ имеют особое значение в регулярных выражениях. Чтобы сопоставить их буквально, экранируйте обратным слэшем:
Вариант без экранирования спокойно подхватит examplexcom, потому что . означает «любой символ». Такой баг встречается сплошь и рядом - и он молчаливый. Если регулярка цепляет лишнее, первым делом ищите неэкранированную ..
Когда вы собираете паттерн из пользовательского ввода, его обязательно нужно экранировать, иначе пользователь сможет подсунуть свой regex-синтаксис:
$& в строке замены - это короткая запись для «всего совпадения».
Опережающие и ретроспективные проверки в regex
Иногда нужно найти совпадение только в том случае, если после него (или перед ним) идёт что-то определённое, - но при этом само это «что-то» в результат не включать. Ровно для таких ситуаций и придуманы lookaround-конструкции:
(?= ...)- позитивный lookahead: «за чем следует».(?<= ...)- позитивный lookbehind: «чему предшествует».(?! ...)и(?<! ...)- их негативные варианты.
Lookaround'ы не «съедают» символы, поэтому то, на что мы «подсматриваем», остаётся доступным для следующей части паттерна.
Пара слов о валидации email
Этот вопрос всплывает постоянно: «дайте мне regex, который валидирует email». Честный ответ - не надо. Настоящая грамматика email'ов - это адский зверь, и любое регулярное выражение, достаточно короткое, чтобы его можно было прочитать, в чём-нибудь да ошибётся. Для валидации формы вполне сойдёт прагматичный паттерн:
Читается это так: «несколько символов, которые не являются пробелами и не @, потом @, снова то же самое, точка, и опять то же самое». Такой шаблон ловит очевидные опечатки, но не претендует на соответствие RFC 5322. Если нужна настоящая проверка - отправьте письмо с подтверждением.
Типичные подводные камни
Несколько граблей, на которые стоит наступить в уме, а не в проде:
- Забыли флаг
gприreplaceилиmatchAll. В первом случае заменится только одно совпадение, во втором прилетитTypeError. - Состояние
lastIndexу глобальных регулярок. Регулярное выражение с флагомgилиyзапоминает, где остановилось, между вызовамиtest/exec. Не переиспользуйте один и тот же объект на несвязанных строках - создавайте новый либо беритеmatchAll. - Неэкранированные точки и слэши в динамических шаблонах. Всегда экранируйте пользовательский ввод, прежде чем пихать его в
new RegExp(...). - Катастрофический бэктрекинг. Вложенные квантификаторы вроде
(a+)+на «злой» входной строке способны намертво повесить вкладку. Если регулярка тормозит - упрощайте.
Дальше: даты и время
Регулярки отвечают за форму текста, но в реальных данных ещё есть временные метки - их нужно парсить, форматировать и складывать. На следующей странице разберём Date, Intl.DateTimeFormat и ту самую ментальную модель, благодаря которой баги с часовыми поясами обходят вас стороной.
Часто задаваемые вопросы
Как создать регулярное выражение в JavaScript?
Есть два способа. Литерал через слэши: /hello/i. И конструктор, который принимает строку: new RegExp('hello', 'i'). Литерал удобен, когда паттерн фиксированный, а конструктор пригодится, если нужно собрать выражение из переменной на лету.
Чем отличаются test, match и exec?
regex.test(str) возвращает булево - самый быстрый вариант, когда нужно просто узнать, есть совпадение или нет. str.match(regex) отдаёт массив совпадений (или null). regex.exec(str) возвращает совпадения по одному, вместе с группами захвата, и при флаге g запоминает позицию между вызовами через lastIndex.
Как заменить все вхождения через regex?
Нужен флаг g: str.replace(/foo/g, 'bar'). Без g заменится только первое совпадение. Ещё есть str.replaceAll(/foo/g, 'bar'), но учтите: если в replaceAll передаётся регулярка, флаг g обязателен - иначе будет ошибка.
Что такое группы захвата в регулярных выражениях?
Это скобки в паттерне, которые запоминают то, что в них попало. Например, /(\d{4})-(\d{2})/.exec('2024-11') вернёт массив, где под индексом 1 лежит '2024', а под индексом 2 - '11'. Группы можно именовать: (?<year>\d{4}), и тогда доступ будет через match.groups.year.