Перейти к содержимому
Toova
Все инструменты

10 приёмов регулярных выражений, которые должен знать каждый разработчик

Toova

Регулярные выражения — один из тех инструментов, которые поначалу кажутся непреодолимыми, а потом вдруг встают на место. Когда это происходит, вы начинаете видеть их повсюду: валидация ввода, разбор логов, пайплайны поиска и замены, маршрутизация URL. Но большинство разработчиков используют лишь горстку возможностей — символьные классы, квантификаторы, якоря — оставляя остальную спецификацию нетронутой.

Это руководство охватывает десять возможностей регулярных выражений, выходящих за рамки базовых. Каждая из них решает реальную задачу, с которой простые шаблоны не справляются. Все примеры написаны на JavaScript, что также применимо к любой среде, совместимой с ECMAScript.

Вы можете протестировать каждый шаблон из этой статьи в Toova Regex Tester без единой строки настроечного кода.

1. Просмотр вперёд: совпадение без захвата

Просмотр вперёд проверяет, что шаблон должен (или не должен) следовать после текущей позиции, не продвигая движок через эти символы. Совпавший текст не включает то, что проверяется просмотром вперёд.

Позитивный просмотр вперёд — синтаксис: (?=...)

// Позитивный просмотр вперёд: совпадение "foo" только если за ним следует "bar"
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false

Негативный просмотр вперёд — синтаксис: (?!...)

// Негативный просмотр вперёд: совпадение "foo" если за ним НЕ следует "bar"
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false

Практический пример: совпасть с числом цены только если за ним следует символ валюты, не включая этот символ в захваченное значение. Или проверить, что пароль содержит хотя бы одну цифру через (?=.*\d), не указывая, где именно должна быть цифра.

Просмотры вперёд имеют нулевую ширину — они не захватывают символы. Можно накладывать несколько просмотров вперёд на одну позицию, чтобы одновременно проверять несколько независимых условий.

2. Просмотр назад: проверка предшествующего текста

Просмотр назад — зеркальное отражение просмотра вперёд: он проверяет текст перед текущей позицией, не включая его в совпадение.

Позитивный просмотр назад — синтаксис: (?<=...)

// Позитивный просмотр назад: совпадение "bar" только если перед ним "foo"
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false

Негативный просмотр назад — синтаксис: (?<!...)

// Негативный просмотр назад: совпадение "bar" если перед ним НЕТ "foo"
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false

Просмотры назад появились в ECMAScript 2018 и поддерживаются во всех современных браузерах и Node.js 10+. Типичный случай применения: извлечь значение из пары ключ-значение вида name=Alice, захватив всё после name= без включения ключа в совпадение.

Важно: в отличие от просмотров вперёд, выражения просмотра назад в JavaScript не могут содержать шаблоны переменной длины — выражение внутри должно иметь фиксированную или ограниченную максимальную длину.

3. Именованные группы захвата: самодокументируемые шаблоны

Стандартные группы захвата адресуются по номеру: $1, $2 и так далее. При добавлении или удалении группы все ссылки на неё ломаются. Именованные группы решают эту проблему, позволяя прикрепить метку к каждой группе.

const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year);  // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day);   // "10"

Именованные группы также доступны в строках замены через $<name>:

// Обратная ссылка на именованную группу в шаблоне
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false

Имя группы должно быть допустимым идентификатором JavaScript. Используйте описательные имена, отражающие суть захватываемого — year, port, protocol — и ваши шаблоны станут почти самодокументируемыми. Внутри самого шаблона можно использовать \k<name> для обратной ссылки на именованную группу, как показано выше.

4. Нежадные квантификаторы: минимальное совпадение

По умолчанию квантификаторы (*, +, ?) жадные — они совпадают с максимально возможным количеством символов. Добавление ? после квантификатора делает его нежадным (ленивым), совпадающим с минимально возможным количеством символов.

const html = '<a>click</a>';

// Жадный (по умолчанию) — совпадает с максимально длинной строкой
/<.+>/.exec(html)?.[0]; // '<a>click</a>'

// Нежадный — совпадает с минимально короткой строкой
/<.+?>/.exec(html)?.[0]; // '<a>'

Это важно при разборе HTML, XML или любого формата, где один и тот же разделитель может встречаться несколько раз. Жадная версия захватывает всё от первого открывающего тега до последнего закрывающего во всей строке. Нежадная версия останавливается на первом допустимом закрывающем совпадении.

То же самое применимо к +? (один или более, ленивый) и ?? (ноль или один, ленивый). Нежадные квантификаторы не меняют то, что может быть найдено — они меняют, какое именно допустимое совпадение выбирается, когда существует несколько вариантов.

5. Предотвращение катастрофического отката

Откат — это то, как движки регулярных выражений восстанавливаются после неудачной попытки совпадения: они пробуют другой путь через шаблон. В большинстве случаев это незаметно и быстро. Но некоторые шаблоны заставляют движок перебирать экспоненциально растущее число путей, превращая процесс Node.js в нечто малопригодное даже на небольшой входной строке.

Классический опасный шаблон — вложенные квантификаторы вида (a+)+ применительно к строке типа aaaaab. Движок перебирает все возможные способы распределить символы a между внутренней и внешней группами, прежде чем прийти к выводу об отсутствии совпадения.

Атомарные группы ((?>...)) предотвращают это, запрещая движку откатываться в группу после успешного совпадения. В JavaScript атомарные группы не поддерживаются нативно, но притяжательное поведение можно эмулировать через просмотр вперёд:

// Без атомарной группы — движок откатывается внутрь (\d+)
// С атомарной группой — после совпадения (\d+) откат запрещён
// JavaScript не поддерживает атомарные группы нативно,
// но их можно эмулировать через просмотр вперёд:
const re5 = /(?=(\d+))\1(?!\d)/; // эмуляция притяжательного \d++

Более безопасное правило: избегайте квантификаторов, вложенных прямо в другие квантификаторы, без веской причины. Переформулируйте шаблоны так, чтобы они были точнее. Также можно использовать инструмент Text Diff, чтобы сравнить вывод двух эквивалентных шаблонов при рефакторинге.

6. Операции с символьными классами (флаг Unicode v)

ECMAScript 2024 добавил флаг v, который включает операции с множествами внутри символьных классов. Это позволяет выразить «все буквы кроме гласных» или «заглавные буквы только из ASCII» как чёткое определение класса вместо громоздкого чередования.

// Вычитание символьного класса недоступна в JS напрямую,
// но флаг Unicode sets (v) добавляет операции с множествами:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false

Флаг v поддерживает три операции внутри символьных классов:

  • Вычитание: [A--B] — символы в A, но не в B
  • Пересечение: [A&&B] — символы и в A, и в B
  • Объединение: [AB] — символы в A или в B (как в стандартных классах)

Node.js 20+ и все вечнозелёные браузеры поддерживают флаг v. Он является надмножеством флага u — не комбинируйте оба; используйте только v, когда нужны его возможности.

7. Границы слова: совпадение с целым словом

Якорь \b совпадает с позицией между символом слова (\w) и несловесным символом. Он не захватывает символы — только проверяет позицию. Инверсия \B совпадает с любой позицией, которая не является границей слова.

const sentence = 'cat concatenate';

// Без \b — "cat" находится и внутри "concatenate"
/cat/g.exec(sentence); // совпадает "cat" в "cat" И в "concatenate"

// С \b — только целое слово "cat"
/\bcat\b/g.exec(sentence); // совпадает только отдельно стоящий "cat"
// \B — инверсия: совпадение внутри слова, не на границе
/\Bcat\B/.test('concatenate'); // true — "cat" находится внутри слова

Границы слова необходимы при поиске идентификаторов в коде или тексте. Без них поиск переменной с именем id также найдёт indexOf, invalid и grid. Используйте \bterm\b, чтобы ограничить совпадения только отдельно стоящими вхождениями.

Важная оговорка: \b использует JavaScript-определение словесного символа ([a-zA-Z0-9_]). Символы с диакритикой и буквы не из латиницы считаются несловесными. Для Unicode-корректных границ слова комбинируйте флаг v с классами свойств Unicode.

8. Многострочный режим: якоря для каждой строки

По умолчанию ^ совпадает только с самым началом строки, а $ — только с самым концом. Флаг m (multiline) меняет это: ^ совпадает с началом каждой строки, а $ — с концом каждой строки.

const text = 'line one\nline two\nline three';

// Без флага m — ^ совпадает только с началом всей строки
/^line/.test(text); // true (только первая строка)

// С флагом m — ^ совпадает с началом КАЖДОЙ строки
const matches = text.match(/^line/gm);
console.log(matches); // ['line', 'line', 'line']

Это незаменимо при обработке многострочного текста — лог-файлов, конфигурационных файлов или кода. Типичные применения: извлечение строк, начинающихся с ключевого слова, замена маркеров конца строки или проверка того, что каждая строка в блоке соответствует шаблону.

Не путайте флаг m с флагом s (dotAll). Флаг s позволяет . совпадать с символами новой строки. Флаг m вообще не влияет на . — только на поведение ^ и $.

9. Экранирование свойств Unicode: интернациональное совпадение символов

Флаг u включает экранирование свойств Unicode, позволяя совпадать с символами на основе их категории Unicode, письменности или иного свойства. Это правильный способ совпадать с буквами, цифрами или знаками пунктуации во всех системах письма — не только в ASCII.

// Флаг u включает экранирование свойств Unicode
const letters = /\p{L}+/u;
letters.test('Héllo');   // true
letters.test('你好');    // true
letters.test('12345');   // false

// Совпадение только с заглавными буквами всех письменностей
const upper = /\p{Lu}+/u;
upper.test('ABC');  // true
upper.test('abc');  // false
// Совпадение с эмодзи (категория Unicode: Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true

Наиболее часто используемые свойства Unicode:

  • \p{L} — любая буква (все письменности)
  • \p{Lu} — заглавные буквы
  • \p{Ll} — строчные буквы
  • \p{N} — любое число
  • \p{Nd} — десятичные цифры
  • \p{P} — знаки пунктуации
  • \p{Script=Latin} — символы латинской письменности
  • \p{Emoji} — символы эмодзи

Используйте \P{...} (заглавная P) для отрицания — совпадение с тем, что не обладает указанным свойством. Полный список поддерживаемых свойств Unicode приведён в документации MDN.

10. Подробные шаблоны через сборку строк

Многие реализации регулярных выражений (Python, Ruby, .NET, PCRE) поддерживают подробный или расширенный режим (флаг x), допускающий пробелы и комментарии внутри шаблонов. В JavaScript этого флага нет — флаг x недопустим в ECMAScript.

Стандартный обходной путь — собирать шаблоны из именованных строковых констант и объединять их через new RegExp():

// JavaScript не имеет нативного флага x,
// но можно строить читаемые шаблоны из строковых констант:
const YEAR  = '(?<year>\\d{4})';
const SEP   = '-';
const MONTH = '(?<month>\\d{2})';
const DAY   = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY);

Каждая константа описывает, что именно она находит, и итоговый шаблон читается как предложение. Такой подход также облегчает переиспользование общих подшаблонов в нескольких определениях регулярных выражений в кодовой базе и независимое тестирование каждого компонента.

Для менее сложных шаблонов часто достаточно записать всё регулярное выражение в одну строку с комментарием в соседнем блоке. Цель — убедиться, что следующий разработчик (включая вас самих в будущем) сможет понять шаблон, не прогоняя его через декодер.

Всё вместе

Эти десять техник охватывают значительную часть поверхности «почему это регулярное выражение не работает на граничных случаях?». Просмотры вперёд и назад позволяют проверять контекст, не захватывая его. Именованные группы сохраняют читаемость шаблонов при рефакторинге. Нежадные квантификаторы предотвращают случайное избыточное совпадение. Экранирование свойств Unicode работает с вводом за пределами ASCII.

Лучший способ выработать беглость — экспериментировать с реальными шаблонами на реальных данных. Используйте Regex Tester для быстрого итерирования. Когда шаблон выдаёт результат, который нужно сравнить или подчистить, инструмент Text Diff покажет, что именно изменилось между запусками. А если ваше регулярное выражение разбирает JSON, JSON Formatter позволит изучить структурированный результат прямо в браузере.

Полный справочник по синтаксису и флагам регулярных выражений JavaScript вы найдёте в MDN Regex Cheatsheet. Для интерактивной отладки шаблонов с полной визуализацией совпадений regex101.com поддерживает режим JavaScript с описанием каждого компонента шаблона.