Регулярные выражения в 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.