Регулярные выражения в 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/. Движок делает следующее:
- Начинает с первой позиции (символ 'T')
- Проверяет, совпадает ли 'T' с 'c' — нет, двигаемся дальше
- Проверяет 'h' с 'c' — нет
- Проверяет 'e' с 'c' — нет
- Проверяет пробел с 'c' — нет
- Проверяет 'c' с 'c' — да! Проверяем следующий символ
- 'a' с 'a' — да! Проверяем следующий
- '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
Список символов, которые нужно экранировать: . * + ? ^ $ { } (