⚡ JavaScript

Регулярные выражения в JavaScript (Node.js): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К вам приходит читатель и говорит: «Найдите мне все книги, в названии которых есть слово "приключения", но только те, что написаны после 2000 года, и чтобы на обложке была картинка с драконом». Без специальных инструментов вам пришлось бы перебирать каждую книгу вручную — это заняло бы недели.

Регулярные выражения (Regular Expressions, или сокращённо regexp) — это как раз такой волшебный инструмент для поиска в тексте. Только вместо книг мы ищем в строках (тексте), а вместо словесных описаний используем специальный язык шаблонов.

Вот ещё одна аналогия. Представьте, что вы хотите найти все яблоки в корзине с фруктами. Вы можете перебрать каждый фрукт руками — это долго. А можете сказать: «Дай мне все круглые, красные или зелёные, размером с кулак». Это и есть регулярное выражение — описание того, что вы ищете, на специальном языке.

Важно: Регулярные выражения — это не особенность JavaScript. Они есть почти во всех языках программирования: Python, Java, C++, PHP и даже в текстовых редакторах вроде VS Code или Notepad++. Но синтаксис может немного отличаться.

В программировании регулярные выражения используются для:

  • Проверки ввода — например, является ли строка email-адресом или номером телефона
  • Поиска — найти все даты в тексте, все ссылки, все упоминания чего-либо
  • Замены — заменить все "кот" на "собака", но только если это целое слово
  • Разделения — разбить строку на части по сложному шаблону

В Node.js (JavaScript на сервере) регулярные выражения особенно полезны при обработке данных из интернета, парсинге HTML-страниц, валидации форм и работе с базами данных.

Теория: как это работает изнутри

Чтобы понять, как работают регулярные выражения, нужно представить, что у нас есть строка текста и «поисковый автомат». Этот автомат движется по строке символ за символом и проверяет, совпадает ли текущий участок с нашим шаблоном.

Регулярное выражение состоит из обычных символов (которые ищут сами себя) и специальных символов (метасимволов), которые имеют особое значение.

Основные понятия

Шаблон (Pattern) — это то, что мы ищем. Например, шаблон cat найдёт в строке все вхождения "cat".

Флаги (Flags) — это настройки поиска. Например, флаг i делает поиск нечувствительным к регистру (будет искать и "Cat", и "cat", и "CAT").

Методы — это способы применить регулярное выражение к строке. В JavaScript есть несколько методов: test(), exec(), match(), replace(), search(), split().

Как движок регулярных выражений обрабатывает текст

Представьте, что у нас есть строка "The cat sat on the mat" и шаблон /cat/. Движок делает следующее:

  1. Начинает с первой позиции (символ 'T')
  2. Проверяет, совпадает ли 'T' с 'c' — нет, двигаемся дальше
  3. Проверяет 'h' с 'c' — нет
  4. Проверяет 'e' с 'c' — нет
  5. Проверяет пробел с 'c' — нет
  6. Проверяет 'c' с 'c' — да! Проверяем следующий символ
  7. 'a' с 'a' — да! Проверяем следующий
  8. 't' с 't' — да! Нашли совпадение!

Если бы мы искали /c.t/ (где точка — любой символ), то на шаге 6-8 мы бы нашли "cat", но также нашли бы "cot" если бы оно было, или "c!t".

Важно понимать: Регулярные выражения по умолчанию ищут первое совпадение. Если нужно найти все, используется флаг g (global).

Синтаксис в JavaScript (Node.js): все формы и варианты

В JavaScript есть два способа создать регулярное выражение:

1. Литеральный синтаксис (самый распространённый)

// Шаблон заключается в слеши, флаги — после второго слеша
const regexp = /pattern/flags;

// Пример: найти слово "hello" без учёта регистра
const regexp1 = /hello/i;

2. Конструктор RegExp (когда шаблон динамический)

// Первый аргумент — строка с шаблоном, второй — строка с флагами
const regexp = new RegExp('pattern', 'flags');

// Пример: найти слово, которое приходит из переменной
const searchWord = 'hello';
const regexp2 = new RegExp(searchWord, 'i');

Таблица флагов (Flags)

Флаг Название Описание Пример
g Global Ищет все совпадения, а не только первое /cat/g найдёт все "cat"
i Case-insensitive Игнорирует регистр букв /cat/i найдёт "Cat", "CAT", "cat"
m Multiline Многострочный режим. Якоря ^ и $ работают для каждой строки /^hello/m найдёт "hello" в начале каждой строки
s Dotall Точка . теперь включает символ новой строки /hello.world/s найдёт "hello world"
u Unicode Включает полную поддержку Unicode (эмодзи, кириллица) /p{Emoji}/u найдёт все эмодзи
y Sticky Поиск только с позиции lastIndex (прилипающий) /cat/y найдёт "cat" только если он начинается с lastIndex

Таблица специальных символов (метасимволов)

Символ Значение Пример Найдёт
. Любой символ, кроме новой строки /c.t/ "cat", "cot", "c!t"
d Любая цифра (0-9) /d+/ "123", "7", "42"
D Не цифра /D+/ "abc", "!@#"
w Буква, цифра или подчёркивание /w+/ "hello", "test_123"
W Не буква, не цифра и не подчёркивание /W+/ "!@#", " "
s Пробельный символ (пробел, табуляция, новая строка) /s+/ " ", " ", " "
S Не пробельный символ /S+/ "hello", "123"
^ Начало строки /^hello/ "hello" только в начале строки
$ Конец строки /world$/ "world" только в конце строки
* 0 или более повторений /ab*c/ "ac", "abc", "abbc"
+ 1 или более повторений /ab+c/ "abc", "abbc", но не "ac"
? 0 или 1 повторение (необязательно) /colou?r/ "color", "colour"
{n} Ровно n повторений /d{3}/ "123", "456", но не "12"
{n,} n или более повторений /d{2,}/ "12", "123", "1234"
{n,m} От n до m повторений /d{2,4}/ "12", "123", "1234"
[] Класс символов (любой из перечисленных) /[aeiou]/ Любая гласная буква
[^] Отрицание класса /[^aeiou]/ Любой символ, кроме гласных
| ИЛИ (альтернатива) /cat|dog/ "cat" или "dog"
() Группировка и захват /(ab)+/ "ab", "abab", "ababab"
 Граница слова /cat/ "cat" как отдельное слово, но не в "catalog"
B Не граница слова /BcatB/ "cat" внутри слова, например в "catalog"

Методы для работы с регулярными выражениями в JavaScript

Метод Вызывается на Что возвращает Пример
test() RegExp true или false /cat/.test('cat') → true
exec() RegExp Массив с деталями или null /cat/.exec('cat') → ['cat']
match() String Массив совпадений или null 'cat'.match(/cat/) → ['cat']
matchAll() String Итератор со всеми совпадениями 'cat cat'.matchAll(/cat/g)
replace() String Новую строку с заменами 'cat'.replace(/c/, 'b') → 'bat'
replaceAll() String Новую строку со всеми заменами 'cat cat'.replaceAll(/cat/g, 'dog') → 'dog dog'
search() String Индекс первого совпадения или -1 'cat'.search(/c/) → 0
split() String Массив строк, разделённых по шаблону 'a,b,c'.split(/,/) → ['a','b','c']

Примеры кода — от простого к сложному

Пример 1: Проверка, содержит ли строка слово

// Самый простой случай: проверяем, есть ли в строке слово "JavaScript"
const text = "Я изучаю JavaScript и Node.js";
const pattern = /JavaScript/;

// Метод test() возвращает true или false
const hasJavaScript = pattern.test(text);
console.log(hasJavaScript); // true

// Объяснение:
// 1. Создаём регулярное выражение /JavaScript/ — ищем точное совпадение
// 2. Вызываем метод test() на нашем шаблоне, передавая строку
// 3. Метод проверяет, есть ли в строке подстрока "JavaScript"
// 4. Возвращает true, потому что "JavaScript" есть в тексте

Пример 2: Поиск с игнорированием регистра

const text = "JavaScript — это круто! А вы знаете javascript?";
const pattern = /javascript/i; // флаг i — игнорируем регистр

const result = pattern.test(text);
console.log(result); // true

// А теперь найдём все вхождения с флагом g
const patternGlobal = /javascript/gi; // g — все вхождения, i — без учёта регистра
const matches = text.match(patternGlobal);
console.log(matches); // ['JavaScript', 'javascript']

// Объяснение:
// 1. Флаг i позволяет найти "JavaScript" даже если написано "javascript"
// 2. Флаг g заставляет искать все совпадения, а не только первое
// 3. Метод match() возвращает массив всех найденных совпадений

Пример 3: Проверка, является ли строка числом

// Проверяем, состоит ли строка только из цифр
function isNumber(str) {
    // ^ — начало строки, $ — конец строки
    // d+ — одна или более цифр
    const pattern = /^d+$/;
    return pattern.test(str);
}

console.log(isNumber("12345"));   // true — только цифры
console.log(isNumber("12a45"));   // false — есть буква
console.log(isNumber(""));        // false — пустая строка
console.log(isNumber("  123"));   // false — есть пробелы

// Объяснение:
// 1. ^ — якорь начала строки. Без него мы бы нашли цифры в середине строки
// 2. $ — якорь конца строки. Гарантирует, что после цифр ничего нет
// 3. d+ — одна или несколько цифр
// 4. Вместе ^d+$ означает: "строка от начала до конца состоит из цифр"

Пример 4: Извлечение email-адресов из текста

const text = "Свяжитесь с нами: support@example.com или admin@test.org. Также пишите на info@company.co.uk";

// Простой шаблон для email (упрощённый, но рабочий)
const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}/g;

const emails = text.match(emailPattern);
console.log(emails);
// ['support@example.com', 'admin@test.org', 'info@company.co.uk']

// Объяснение:
// 1. [a-zA-Z0-9._%+-]+ — имя пользователя: буквы, цифры и некоторые символы
// 2. @ — символ "собака"
// 3. [a-zA-Z0-9.-]+ — домен: буквы, цифры, точки и дефисы
// 4. . — точка (экранированная, потому что точка — спецсимвол)
// 5. [a-zA-Z]{2,} — доменная зона: минимум 2 буквы (ru, com, org...)
// 6. Флаг g — найти все email'ы в тексте

Пример 5: Замена всех вхождений с использованием групп

// Задача: поменять местами имя и фамилию
// "Иванов Иван" → "Иван Иванов"

const fullName = "Петров Пётр";

// Используем группы захвата (скобки)
// (w+) — первая группа: слово (фамилия)
// s — пробел
// (w+) — вторая группа: слово (имя)
const pattern = /(w+)s(w+)/;

// В replace можно ссылаться на группы через $1, $2 и т.д.
const reversed = fullName.replace(pattern, "$2 $1");
console.log(reversed); // "Пётр Петров"

// Объяснение:
// 1. (w+) — захватываем первое слово в группу 1
// 2. s — пробел между словами
// 3. (w+) — захватываем второе слово в группу 2
// 4. "$2 $1" — меняем порядок: сначала группа 2, потом пробел, потом группа 1

Пример 6: Валидация номера телефона

// Проверяем российский номер телефона в формате +7 (XXX) XXX-XX-XX
function validatePhone(phone) {
    // Экранируем скобки и дефисы, так как это спецсимволы
    const pattern = /^+7 (d{3}) d{3}-d{2}-d{2}$/;
    return pattern.test(phone);
}

console.log(validatePhone("+7 (999) 123-45-67")); // true
console.log(validatePhone("8 (999) 123-45-67"));  // false — начинается с 8, а не +7
console.log(validatePhone("+7 999 123-45-67"));   // false — нет скобок
console.log(validatePhone("+7 (999) 123-45-6"));  // false — не хватает цифры

// Объяснение:
// 1. +7 — символ + экранирован, так как + — спецсимвол
// 2. ( и ) — скобки экранированы
// 3. d{3} — ровно 3 цифры
// 4. d{3}-d{2}-d{2} — три цифры, дефис, две цифры, дефис, две цифры
// 5. ^ и $ — строка должна полностью соответствовать шаблону

Пример 7: Парсинг HTML-тегов (извлечение ссылок)

// Извлекаем все ссылки из HTML-кода
const html = `
    Пример
    О нас
    Тестовая страница
`;

// Ищем href="..." — значение атрибута href
// Используем ленивый квантификатор *? чтобы не захватить лишнего
const linkPattern = /href="([^"]+)"/g;

let match;
const links = [];

// Используем exec() в цикле для получения всех совпадений с группами
while ((match = linkPattern.exec(html)) !== null) {
    // match[0] — полное совпадение (href="...")
    // match[1] — первая группа (то, что внутри кавычек)
    links.push(match[1]);
}

console.log(links);
// ['https://example.com', '/about', 'https://test.org/page']

// Объяснение:
// 1. href=" — ищем буквально строку href="
// 2. ([^"]+) — группа захвата: один или более символов, не являющихся кавычкой
// 3. " — закрывающая кавычка
// 4. Флаг g — ищем все совпадения
// 5. exec() в цикле — получаем каждое совпадение с деталями
// 6. [^"]+ — это "отрицание класса": любой символ, кроме "

Пример 8: Сложный парсинг с именованными группами (ES2018+)

// Разбираем строку с датой в формате "2024-01-15"
const dateStr = "Сегодня 2024-01-15, а завтра будет 2024-01-16";

// Используем именованные группы (?<name>...)
const datePattern = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/g;

// matchAll возвращает итератор, преобразуем в массив
const dates = [...dateStr.matchAll(datePattern)];

dates.forEach((date, index) => {
    console.log(`Дата ${index + 1}:`);
    console.log(`  Год: ${date.groups.year}`);
    console.log(`  Месяц: ${date.groups.month}`);
    console.log(`  День: ${date.groups.day}`);
});

// Вывод:
// Дата 1:
//   Год: 2024
//   Месяц: 01
//   День: 15
// Дата 2:
//   Год: 2024
//   Месяц: 01
//   День: 16

// Объяснение:
// 1. (?<year>d{4}) — именованная группа "year" с 4 цифрами
// 2. (?<month>d{2}) — именованная группа "month" с 2 цифрами
// 3. (?<day>d{2}) — именованная группа "day" с 2 цифрами
// 4. matchAll() — возвращает итератор, который мы разворачиваем в массив через [...]
// 5. date.groups — объект с именованными группами

Сравнение с Python: чем похоже и чем отличается

Если вы уже знаете Python или планируете его изучать, вот ключевые различия в работе с регулярными выражениями:

Аспект JavaScript (Node.js) Python
Импорт Не требуется (встроено в язык) import re
Создание шаблона /pattern/flags или new RegExp('pattern', 'flags') re.compile(r'pattern', flags) или re.search('pattern', text)
Сырые строки Не нужны (обратные слеши работают как обычно) Рекомендуются r'd+' чтобы избежать экранирования
Флаг global g — ищет все совпадения Нет флага global, вместо него re.findall() или re.finditer()
Игнорирование регистра i re.IGNORECASE или re.I
Многострочный режим m re.MULTILINE или re.M
Именованные группы (?<name>...) (?P<name>...)
Доступ к группам match.groups.name или match[1] match.group('name') или match.group(1)
Поиск всех совпадений str.match(/pattern/g) re.findall(pattern, str)
Замена str.replace(/pattern/, replacement) re.sub(pattern, replacement, str)
Разделение str.split(/pattern/) re.split(pattern, str)
Lookahead (опережающая проверка) (?=...) и (?!...) То же самое: (?=...) и (?!...)
Lookbehind (ретроспективная проверка) (?<=...) и (?<!...) (с ES2018) (?<=...) и (?<!...)
Важное отличие: В JavaScript флаг g меняет поведение методов exec() и test() — они запоминают позицию последнего совпадения в свойстве lastIndex. В Python такого нет, и каждый поиск начинается сначала.

Типичные ошибки и подводные камни

Ошибка 1: Забыли экранировать специальные символы

// ❌ Неправильно: точка — спецсимвол, ищет любой символ
const wrong = /example.com/;
console.log(wrong.test('exampleXcom')); // true — не то, что хотели!

// ✅ Правильно: экранируем точку
const correct = /example.com/;
console.log(correct.test('example.com')); // true
console.log(correct.test('exampleXcom')); // false

Список символов, которые нужно экранировать: . * + ? ^ $ { } (