JavaScript · Синтаксис · Промежуточный

Регулярные выражения

10 задач

Поиск по шаблону с помощью RegExp. Охватывает test, match, replace и именованные группы захвата.

Основы regex в JavaScript

#
Регулярное выражение в JavaScript — это объект, описывающий текстовый шаблон. Создаётся с помощью литерала `/pattern/flags` или `new RegExp('pattern', 'flags')`. **Три самых используемых метода** ```javascript const text = 'User: alice, age 30'; /\d+/.test(text) // true - есть ли паттерн где-нибудь в строке? text.match(/\d+/) // ['30'] - первое совпадение как массив (или null) text.replace(/\d+/, '??') // 'User: alice, age ??' - заменить первое совпадение text.replace(/\d+/g, '??')// 'User: alice, age ??' - заменить ВСЕ (флаг g) ``` **Читаем паттерн посимвольно** Возьмём `/([\w.]+)@([\w.]+\.\w{2,})/` (простой паттерн email): ``` ( открыть группу 1 [\w.]+ один или более: символы слова или литеральная точка ) закрыть группу 1 @ литеральный "@" ( открыть группу 2 [\w.]+ один или более: символы слова или литеральная точка \. литеральная точка (экранированная - обычная . означает "любой символ") \w{2,} 2 или более символов слова (домен верхнего уровня) ) закрыть группу 2 ``` ```javascript const m = '[email protected]'.match(/([\w.]+)@([\w.]+\.\w{2,})/); m[0] // '[email protected]' - полное совпадение m[1] // 'bob' - группа 1 m[2] // 'example.com' - группа 2 ``` **Литерал regex vs new RegExp** Предпочитай литерал `/pattern/` — он компилируется один раз и не требует экранирования строк. Используй `new RegExp(str)` только когда паттерн строится во время выполнения: ```javascript // Хорошо: литерал const valid = /^\d{3}-\d{4}$/; // Когда паттерн из переменной: function buildSearch(word) { const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(escaped, 'gi'); } ``` **Ловушка `lastIndex` с сохранением состояния** Когда используешь флаг `g` или `y` и вызываешь `.test()` или `.exec()` на *том же* объекте regex несколько раз, движок запоминает позицию через `regex.lastIndex`. Это приводит к удивительному чередованию true/false: ```javascript const re = /\d+/g; // с сохранением состояния из-за 'g' re.test('abc 42 def'); // true (нашёл '42', lastIndex -> 6) re.test('abc 42 def'); // false (продолжает с индекса 6, ничего не находит) re.test('abc 42 def'); // true (lastIndex сбрасывается после неудачи) // Безопасный паттерн: создавай новый regex каждый раз или сбрасывай вручную re.lastIndex = 0; re.test('abc 42 def'); // true (всегда) ``` Самое безопасное правило: используй флаг `g` только с `str.match()`, `str.matchAll()` или `str.replaceAll()`, а не с повторными вызовами `.test()` на том же объекте regex.

Захватывающие группы, именованные группы и matchAll

#
**Захватывающие группы** позволяют извлекать части совпадения. Результат `.match()` без флага `g` содержит полное совпадение по индексу 0, затем каждую группу. ```javascript const log = '2024-03-15 ERROR disk full'; const m = log.match(/(\d{4}-\d{2}-\d{2}) (\w+) (.+)/); m[1] // '2024-03-15' m[2] // 'ERROR' m[3] // 'disk full' ``` **Именованные группы** `(?<name>...)` — доступ через `m.groups.name`: ```javascript const m = log.match(/(?<date>\d{4}-\d{2}-\d{2}) (?<level>\w+) (?<msg>.+)/); m.groups.date // '2024-03-15' m.groups.level // 'ERROR' m.groups.msg // 'disk full' ``` Именованные группы также доступны в `replace` через `$<name>`: ```javascript '2024-03-15'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>/$<m>/$<y>') // '15/03/2024' ``` **replace с функцией** Когда замена является функцией, она получает `(fullMatch, ...groups, offset, originalStr)`: ```javascript 'hello world'.replace(/(\w+)/g, (match) => match.toUpperCase()) // 'HELLO WORLD' 'a=3, b=7'.replace(/(\w+)=(\d+)/g, (_, key, val) => `${key}=${+val * 2}`) // 'a=6, b=14' ``` **matchAll — итерация по всем совпадениям с группами** `.match()` с флагом `g` возвращает только полные совпадения (группы отбрасываются). Используй `str.matchAll(regex)`, чтобы получить все совпадения *вместе* с их группами: ```javascript const text = 'alice=30, bob=25'; const re = /(\w+)=(\d+)/g; for (const m of text.matchAll(re)) { console.log(m[1], m[2]); // 'alice' '30' затем 'bob' '25' } // или собрать результаты: const pairs = [...text.matchAll(re)].map(m => ({ name: m[1], age: +m[2] })); // [{ name: 'alice', age: 30 }, { name: 'bob', age: 25 }] ``` `matchAll` требует флаг `g` и возвращает итератор — разворачивай или итерируй с `for...of`. **Не-захватывающие группы** `(?:...)` группируют без индекса захвата — удобны для чередования: ```javascript // Найти 'colour' или 'color' 'colour and color'.match(/colou?r/g) // ['colour', 'color'] // Чередование без захватывающей группы 'cats and dogs'.match(/(?:cat|dog)s?/g) // ['cats', 'dogs'] ```

Lookahead, lookbehind, жадные/ленивые и динамический regex

#
**Lookahead** проверяет, что идёт *после* текущей позиции, не поглощая символов. ```javascript // Позитивный lookahead (?=...) - совпадение X, только если после него Y '100px 200em 300px'.match(/\d+(?=px)/g) // ['100', '300'] (числа перед 'px') // Негативный lookahead (?!...) - совпадение X, только если после него НЕ Y '100px 200em'.match(/\d+(?!px)/g) // ['200'] (числа не перед 'px') ``` **Lookbehind** (ES2018) проверяет, что идёт *перед*: ```javascript // Позитивный lookbehind (?<=...) - совпадение X, только если перед ним Y '$100 EUR200'.match(/(?<=\$)\d+/g) // ['100'] (числа после '$') // Негативный lookbehind (?<!...) - совпадение X, только если перед ним НЕ Y '$100 EUR200'.match(/(?<!\$)\d+/g) // ['200'] (числа не после '$') ``` **Жадные vs ленивые** Квантификаторы по умолчанию жадные. Добавь `?`, чтобы сделать их ленивыми: ```javascript const html = '<b>bold</b> and <i>italic</i>'; html.match(/<.+>/g) // ['<b>bold</b> and <i>italic</i>'] жадный html.match(/<.+?>/g) // ['<b>', '</b>', '<i>', '</i>'] ленивый ``` **Экранирование ввода пользователя для динамического regex** Если строишь regex из строки, введённой пользователем, сначала обязательно экранируй все специальные символы regex — иначе точка или звёздочка сломают паттерн: ```javascript function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlight(text, query) { const re = new RegExp(escapeRegex(query), 'gi'); return text.replace(re, '<mark>$&</mark>'); } highlight('Цена: 2.50', '2.50') // 'Цена: <mark>2.50</mark>' // без экранирования '.' совпала бы с любым символом ``` **Типичные ловушки** - Забыть флаг `g`: `'aaa'.replace(/a/, 'b')` даёт `'baa'`, а не `'bbb'` - `.` не совпадает с переносами строк по умолчанию — используй `[\s\S]` или флаг `s` - `^` и `$` совпадают с границами строки, а не строки текста — если не используется флаг `m` - `.test()` в цикле с тем же `g` regex вызывает смещение `lastIndex` (см. Блок 1) - Regex не может парсить HTML — используй DOM

Кулинарная книга regex-паттернов (JavaScript)

#
Готовая коллекция паттернов для распространённых задач в JavaScript. **Email (простой)** ```javascript const EMAIL = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g; '[email protected], [email protected]'.match(EMAIL) // ['[email protected]', '[email protected]'] ``` **ISO дата** ```javascript const ISO = /\b(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b/g; 'Мероприятие 2024-03-15, дедлайн 2024-12-31'.match(ISO) // ['2024-03-15', '2024-12-31'] ``` **URL** ```javascript const URL_RE = /https?:\/\/[^\s<>'"]+/g; 'Смотри https://developer.mozilla.org для деталей'.match(URL_RE) // ['https://developer.mozilla.org'] ``` **Slug (нижний регистр с дефисами)** ```javascript function toSlug(str) { return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } toSlug('Hello World! 42') // 'hello-world-42' ``` **Camel в snake** ```javascript function camelToSnake(str) { return str.replace(/([A-Z])/g, '_$1').toLowerCase(); } camelToSnake('myVariableName') // 'my_variable_name' ``` **Маска кредитной карты (показать последние 4)** ```javascript '4111 1111 1111 1234'.replace(/\d(?=\d{4})/g, '*') // '**** **** **** 1234' ``` **Справочник флагов** ``` g global - найти все совпадения (не только первое) i без учёта регистра m multiline - ^ и $ совпадают с границами строк s dotAll - . совпадает и с символами новой строки u unicode - включает \u{...} и классы Unicode d hasIndices - match.indices[n] даёт [start, end] для каждой группы ``` **Когда НЕ использовать regex в JavaScript** - Парсинг HTML - используй `document.querySelector`, `DOMParser` или библиотеку - Парсинг URL - используй `new URL(str)` - Парсинг JSON - используй `JSON.parse` - Строгая валидация email - regex не может обеспечить RFC 5322; используй библиотеку Regex блестяще подходит для сканирования и преобразования обычного текста. Для структурированных форматов всегда предпочитай специализированный парсер.
01

#

Напишите функцию, которая проверяет, является ли строка допустимым адресом электронной почты (содержит @, домен и точку в части домена).

function isValidEmail(str) {
    // ваш код здесь
}

console.log(isValidEmail('[email protected]'));   // true
console.log(isValidEmail('user@example'));        // false
console.log(isValidEmail('notanemail'));          // false
console.log(isValidEmail('[email protected]'));              // true
Решение
function isValidEmail(str) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
02

#

Извлеките все слова из строки, начинающиеся с заглавной буквы. Верните их как массив.

function capitalWords(str) {
    // ваш код здесь
}

console.log(capitalWords('Hello world, My Name is Alice'));  // ['Hello', 'My', 'Name', 'Alice']
console.log(capitalWords('no capitals here'));               // []
Решение
function capitalWords(str) {
    return str.match(/\b[A-Z][a-zA-Z]*/g) || [];
}
03

#

Замените все последовательности пробельных символов (пробелы, табуляции, переводы строки) в строке одним пробелом и обрежьте результат.

function normalizeSpaces(str) {
    // ваш код здесь
}

console.log(normalizeSpaces('  hello   world  '));         // 'hello world'
console.log(normalizeSpaces('line1\n\nline2\t  line3')); // 'line1 line2 line3'
Решение
function normalizeSpaces(str) {
    return str.replace(/\s+/g, ' ').trim();
}
04

#

Извлеките все числа (целые и десятичные) из строки и верните их как массив чисел.

function extractNumbers(str) {
    // ваш код здесь
}

console.log(extractNumbers('Price: 12.5, qty: 3, discount: 0.15'));  // [12.5, 3, 0.15]
console.log(extractNumbers('no numbers here'));                       // []
Решение
function extractNumbers(str) {
    const matches = str.match(/\d+\.?\d*/g);
    return matches ? matches.map(Number) : [];
}
05

#

Используйте группу захвата, чтобы извлечь имя пользователя и домен из строки адреса электронной почты.

function parseEmail(email) {
    // Вернуть { username, domain } или null, если недопустимый
}

console.log(parseEmail('[email protected]'));   // { username: 'user', domain: 'example.com' }
console.log(parseEmail('notanemail'));          // null
Решение
function parseEmail(email) {
    const match = email.match(/^([^\s@]+)@([^\s@]+)$/);
    if (!match) return null;
    return { username: match[1], domain: match[2] };
}
06

#

Напишите функцию, преобразующую строку camelCase в snake_case (например, 'myVariableName' → 'my_variable_name').

function camelToSnake(str) {
    // ваш код здесь
}

console.log(camelToSnake('myVariableName'));    // 'my_variable_name'
console.log(camelToSnake('helloWorld'));        // 'hello_world'
console.log(camelToSnake('alreadylower'));      // 'alreadylower'
Решение
function camelToSnake(str) {
    return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}
07

#

Подсчитайте, сколько раз слово встречается в строке (без учёта регистра, только целые слова).

function countWord(str, word) {
    // ваш код здесь
}

console.log(countWord('The cat sat on the mat. The cat is fat.', 'the'));  // 3
console.log(countWord('cats and cat and cat', 'cat'));                      // 3 (not 'cats')
Решение
function countWord(str, word) {
    const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`\\b${escaped}\\b`, 'gi');
    return (str.match(re) || []).length;
}
08

#

Разбейте строку значений, разделённых запятыми/точками с запятой/пробелами, на массив обрезанных непустых токенов.

function splitTokens(str) {
    // ваш код здесь
}

console.log(splitTokens('a, b;  c , d'));    // ['a', 'b', 'c', 'd']
console.log(splitTokens('one;two,three'));   // ['one', 'two', 'three']
console.log(splitTokens('  , ; '));          // []
Решение
function splitTokens(str) {
    return str.split(/[,;\s]+/).map(t => t.trim()).filter(Boolean);
}
09

#

Используйте регулярное выражение для маскировки всех цифр, кроме последних четырёх, в номере кредитной карты (замените на '*').

function maskCard(number) {
    // ваш код здесь
}

console.log(maskCard('1234567890123456'));  // '************3456'
console.log(maskCard('4111 1111 1111 1111')); // '**** **** **** 1111'
Решение
function maskCard(number) {
    return number.replace(/\d(?=\d{4})/g, '*');
}
10

#

Извлеките все URL из строки. URL начинается с http:// или https:// и заканчивается пробельным символом или концом строки.

function extractUrls(str) {
    // ваш код здесь
}

const text = 'Visit https://example.com and http://foo.org/path?q=1 for details.';
console.log(extractUrls(text));
// ['https://example.com', 'http://foo.org/path?q=1']
Решение
function extractUrls(str) {
    return str.match(/https?:\/\/[^\s]+/g) || [];
}