Группы, альтернация и lookahead

Группы, альтернация и lookahead #

Группы в regex изолируют фрагмент шаблона и сохраняют совпавший текст для последующего использования. Альтернация | выбирает один из вариантов; lookahead и lookbehind проверяют контекст без включения его в результат совпадения. Эти приёмы нужны при разборе статусов заказа, составных адресов и метаданных плейлиста.


Захватывающие группы (...) #

Скобки группируют часть шаблона и нумеруют захваты слева направо:

const line = 'ord-2026-7f3a | delivered'
const re = /^(ord-\d{4}-[a-f0-9]{4})\s*\|\s*(\w+)$/

const m = line.match(re)
m[0] // полное совпадение
m[1] // "ord-2026-7f3a"
m[2] // "delivered"

В replace группы доступны как $1, $2 или через функцию:

'status: pending'.replace(/status:\s*(\w+)/, 'Статус: $1')
// "Статус: pending"

'1500 RUB'.replace(/(\d+)\s*(\w+)/, (_, sum, cur) => `${sum} ${cur}`)

Вложенные группы увеличивают номера; незахватывающие (?:...) не занимают номер.


Именованные группы (?<name>...) #

ES2018 добавил именованный захват — читаемее, чем индексы:

const receipt = 'Итого: 2490 RUB, доставка: 300 RUB'
const re = /Итого:\s*(?<total>\d+)\s*(?<currency>\w+)/

const { groups } = receipt.match(re)
groups.total // "2490"
groups.currency // "RUB"

В replace — синтаксис $<name>:

'track: night-drive | 245s'.replace(
  /track:\s*(?<slug>[\w-]+)\s*\|\s*(?<dur>\d+)s/,
  '$<slug> — $<dur> сек'
)
// "night-drive — 245 сек"

Незахватывающие группы (?:...) #

Когда группировка нужна только для квантификатора или альтернации, без извлечения:

const tags = 'focus|work|chill'
tags.split(/\|(?:\s*)?/) // ["focus", "work", "chill"]

Сравните с захватывающей группой — в split пустые элементы или лишние части в результате.


Альтернация | #

Означает «или» — пробует варианты слева направо:

const STATUS = /^(pending|paid|shipped|delivered)$/

STATUS.test('shipped') // true
STATUS.test('cancelled') // false

Приоритет: альтернация имеет низкий приоритет; группируйте скобками:

// совпадение "express" или "standard" после слова delivery
/delivery-(express|standard)/.test('delivery-express') // true

// без скобок — иначе интерпретация иная
/delivery-express|standard/.test('standard') // true — только второй вариант целиком

Практика — способ оплаты на чекауте:

const PAYMENT = /^(card|sbp|cash_on_delivery)$/

function normalizePayment(method) {
  const key = method.trim().toLowerCase().replace(/\s+/g, '_')
  return PAYMENT.test(key) ? key : null
}

normalizePayment('Cash On Delivery') // "cash_on_delivery"

Lookahead (?=...) и (?!...) #

Positive lookahead — после текущей позиции должно совпасть ..., но это не входит в результат:

const prices = '1500 RUB 300 USD 90 EUR'
prices.match(/\d+(?=\s*RUB)/g) // ["1500"] — суммы только в рублях

Negative lookahead — далее не должно совпасть:

'товар 1 шт 2 кг'.match(/\d+(?!\s*кг)/g) // ["1"] — число не перед "кг"

Типичный приём — пароль с требованиями без отдельных проходов:

const HAS_DIGIT = /(?=.*\d)/
const HAS_LETTER = /(?=.*[a-zа-я])/i

function passwordOk(pw) {
  return pw.length >= 8 && HAS_DIGIT.test(pw) && HAS_LETTER.test(pw)
}

Lookbehind (?<=...) и (?<!...) #

Positive lookbehind — перед позицией должен быть фрагмент (ES2018):

const log = 'cart:add cart:remove checkout:pay'
log.match(/(?<=cart:)\w+/g) // ["add", "remove"]

Negative lookbehind — перед позицией фрагмента быть не должно:

'цена 1500, скидка 1500'.match(/(?<!скидка\s)\d+/g)
// ["1500", "1500"] — оба числа; уточните шаблон под задачу

Lookbehind требует фиксированной или ограниченной длины слева в части движков; в V8/JavaScript поддерживаются variable-length lookbehind.


Обратные ссылки \1, \k<name> #

Внутри того же шаблона можно сослаться на ранее захваченную группу:

const dup = 'SAVE-SAVE'
/^(\w+)-\1$/.test(dup) // true — повтор того же слова

const quoted = '"focus"'
/^"(?<w>\w+)"\s*,\s*"\k<w>"$/.test('"focus", "focus"') // false — разные слова

В replace обратные ссылки в шаблоне замены — $1 или $<name>.


Практика: разбор строки доставки #

const DELIVERY_LINE =
  /^(?<carrier>express|standard):\s*(?<eta>\d{1,2})\s*мин(?:,\s*трек\s*(?<track>[A-Z0-9-]+))?$/i

function parseDeliveryStatus(line) {
  const m = line.trim().match(DELIVERY_LINE)
  if (!m) return null
  return {
    carrier: m.groups.carrier.toLowerCase(),
    etaMinutes: Number(m.groups.eta),
    track: m.groups.track ?? null,
  }
}

parseDeliveryStatus('Express: 45 мин, трек TRK-9X2')
// { carrier: 'express', etaMinutes: 45, track: 'TRK-9X2' }
const PLAYLIST_ENTRY =
  /^(?<artist>[^—]+)—\s*(?<title>.+?)\s*\((?<sec>\d{2}):(?<min>\d{2})\)$/

function parseTrack(line) {
  const m = line.match(PLAYLIST_ENTRY)
  if (!m) return null
  const durationSec = Number(m.groups.sec) * 60 + Number(m.groups.min)
  return {
    artist: m.groups.artist.trim(),
    title: m.groups.title.trim(),
    durationSec,
  }
}

parseTrack('Lumen — Night Drive (04:05)')
// { artist: 'Lumen', title: 'Night Drive', durationSec: 245 }

Итог #

  • Захватывающие группы (...) нумеруются с $1; именованные (?<name>...) читаемее в match и replace.
  • (?:...) группирует без захвата — используйте при альтернации и квантификаторах.
  • Альтернация | требует скобок, когда «или» относится только к части шаблона.
  • Lookahead и lookbehind проверяют контекст без расширения совпадения — удобно для цен в нужной валюте и префиксов в логах.

См. также #