← JavaScript/RegExp: lookahead, lookbehind и группы#172 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

RegExp: lookahead, lookbehind и группы

Тебе нужно извлечь все суммы из строки «Итого: 1500₽, скидка 10%, налог 150₽» — только рублёвые, не проценты. Или найти версию в строке «node v18.12, npm v9.5» — числа после «v», но не все числа. Или распарсить конфиг-файл по строкам «key=value». Для всего этого нужны lookahead, lookbehind и именованные группы.

Какую проблему решает

Обычные regex-паттерны не умеют «смотреть вперёд» или «назад» без захвата этого контекста. Lookahead/lookbehind позволяют добавить условия на окружение без включения его в результат. Именованные группы делают код читаемым.

На основе предыдущих уроков

  • RegExp основы: базовые паттерны, flags, .match(), .test()
  • Строки: .replace(), .split() с регулярными выражениями
  • RegExp продвинутый: группы захвата, .exec() в цикле
  • Lookahead — просмотр вперёд

    // X(?=Y) — позитивный: X только если ЗА НИМ следует Y
    // X(?!Y) — негативный: X только если ЗА НИМ НЕ следует Y
    
    // Только числа перед ₽ или $:
    const priceRe = /\d+(?=[₽$])/g
    '100₽ 200$ 50% 300руб'.match(priceRe)  // ['100', '200']  — без 50 и 300!
    
    // Слова не перед двоеточием (не ключи объекта):
    const wordRe = /\w+(?!:)/g

    Lookbehind — просмотр назад

    // (?<=Y)X — позитивный: X только если ПЕРЕД НИМ стоит Y
    // (?<!Y)X — негативный: X только если ПЕРЕД НИМ НЕ стоит Y
    
    // Числа после знака валюты $:
    const afterDollarRe = /(?<=\$)\d+(\.\d{2})?/g
    '$99.99 €200 $150'.match(afterDollarRe)  // ['99.99', '150']
    
    // Версии после "v":
    const versionRe = /(?<=v)\d+\.\d+/g
    'node v18.12 npm v9.5'.match(versionRe)  // ['18.12', '9.5']

    Именованные группы

    // (?<name>...) — захват с именем, доступен через match.groups.name
    // (?:...)      — группа без захвата (только для группировки)
    
    // Парсинг даты ISO
    const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
    const match  = '2024-03-15'.match(dateRe)
    const { year, month, day } = match.groups
    // year='2024', month='03', day='15'
    
    // Деструктуризация прямо из groups

    Обратные ссылки (backreferences)

    // \1 — ссылка на первую группу захвата (в самом regex)
    // \k<name> — ссылка на именованную группу
    
    // Поиск удвоенных слов:
    const doubled = /\b(\w+) \1\b/gi
    'the the cat sat sat'.match(doubled)  // ['the the', 'sat sat']
    
    // Совпадающие кавычки:
    const quoted = /(['"]).*?\1/g

    Нежадные квантификаторы

    // Жадный   * : захватывает максимально
    // Нежадный *?: захватывает минимально
    
    const html = '<b>жирный</b> и <i>курсив</i>'
    
    html.match(/<.+>/g)    // ['<b>жирный</b> и <i>курсив</i>'] — ВСЁ!
    html.match(/<.+?>/g)   // ['<b>', '</b>', '<i>', '</i>']   — отдельно
    
    // Аналогично: +? ?? {n,m}?

    Флаги в regex

    // g — global: все совпадения (не только первое)
    // i — case-insensitive
    // m — multiline: ^ и $ работают с переносами строк
    // s — dotAll: . соответствует переносам строк
    // u — unicode: поддержка Unicode (нужен для эмодзи)
    // d — indices: добавляет .indices с позициями групп

    Важно: двойной слеш в шаблонных строках

    // В регулярном выражении /\d+/ — один слеш
    // В строке (шаблонной или обычной) для передачи в RegExp():
    const re = new RegExp('\\d+')  // \\d → \d в строке → \d в regex

    Типичные ошибки

    Ошибка 1: Флаг g без .exec() в цикле

    // НЕВЕРНО — с флагом g и .match() именованные группы теряются
    const re = /(?<year>\d{4})/g
    '2023 2024'.match(re)  // ['2023', '2024'] — groups нет!
    
    // ВЕРНО — .exec() в цикле сохраняет groups
    let m
    while ((m = re.exec('2023 2024')) !== null) {
      console.log(m.groups.year)  // '2023', '2024'
    }

    Ошибка 2: Забыть сбросить .lastIndex

    const re = /\d+/g
    re.exec('hello 42')  // { index: 6, ... }
    re.exec('1 2 3')     // null! lastIndex=8 не совпал в новой строке
    
    // ВЕРНО — создавай новый regex или сбрасывай:
    re.lastIndex = 0

    Ошибка 3: Жадный квантификатор для HTML

    // НЕВЕРНО — захватывает слишком много
    '<div>раз</div> <div>два</div>'.match(/<div>.*<\/div>/g)
    // ['<div>раз</div> <div>два</div>'] — ВСЁ как одно совпадение!
    
    // ВЕРНО — нежадный
    '<div>раз</div> <div>два</div>'.match(/<div>.*?<\/div>/g)
    // ['<div>раз</div>', '<div>два</div>']

    В реальных проектах

  • Парсинг логов: извлечение timestamp, уровня, сообщения через именованные группы
  • Валидация: пароли, email, телефоны, URL
  • Template engines: поиск {{переменных}} в шаблонах
  • Конфиг-файлы: парсинг .env, nginx.conf, package.json scripts
  • Code generation: поиск и замена паттернов при рефакторинге
  • Примеры

    Lookahead/lookbehind: извлечение цен и версий, именованные группы для дат и URL, парсинг конфига

    // Продвинутые регулярные выражения
    
    // ===== Lookahead =====
    console.log('=== Lookahead: числа перед символом =====')
    
    // Числа перед ₽ или $
    const priceRe = /\d+(\.\d+)?(?=[₽$])/g
    const priceStr = 'Яблоки 45₽, скидка 10%, USB-кабель $12, итого 57₽'
    console.log('Строка:', priceStr)
    console.log('Цены (₽/$):', priceStr.match(priceRe))  // ['45', '12', '57']
    
    // Негативный lookahead: числа НЕ перед %
    const noPercent = /\d+(?!%)/g
    const discountStr = '20% скидка на 500₽ товары, 3% кэшбек'
    const amounts = discountStr.match(/\d+(?=[₽$])/g)
    console.log('Суммы (не проценты):', amounts)  // ['500']
    
    // ===== Lookbehind =====
    console.log('\n=== Lookbehind: числа после символа ===')
    
    // Суммы после $
    const afterDollar = /(?<=\$)\d+(\.\d{2})?/g
    const invoice = 'Subtotal: $99.99, Tax: $8.50, Total: $108.49'
    console.log('Инвойс:', invoice)
    console.log('Суммы:', invoice.match(afterDollar))  // ['99.99', '8.50', '108.49']
    
    // Версии после "v"
    const versionRe = /(?<=v)\d+\.\d+(\.\d+)?/gi
    const releases = 'Node.js v20.5.0, Python v3.11, Chrome v118.0'
    console.log('\nВерсии:', releases.match(versionRe))  // ['20.5.0', '3.11', '118.0']
    
    // ===== Именованные группы =====
    console.log('\n=== Именованные группы ===')
    
    // ISO дата
    const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
    const dateStr = '2024-03-15'
    const dateMatch = dateStr.match(dateRe)
    if (dateMatch?.groups) {
      const { year, month, day } = dateMatch.groups
      console.log('Дата:', { year, month, day })
    }
    
    // URL разбор
    const urlRe = /(?<protocol>https?):\/\/(?<host>[^/?#]+)(?<path>\/[^?#]*)?(?:\?(?<query>[^#]*))?/
    const urlStr = 'https://api.example.com/users/profile?page=1&limit=20'
    const urlMatch = urlStr.match(urlRe)
    if (urlMatch?.groups) {
      const { protocol, host, path, query } = urlMatch.groups
      console.log('\nURL разбор:')
      console.log('  protocol:', protocol)  // https
      console.log('  host:    ', host)      // api.example.com
      console.log('  path:    ', path)      // /users/profile
      console.log('  query:   ', query)     // page=1&limit=20
    }
    
    // ===== Парсинг конфига ===
    console.log('\n=== Парсинг key=value конфига ===')
    
    function parseConfig(text) {
      const result = {}
      const lineRe = /^(?<key>[\w.]+)\s*=\s*(?<value>.+?)\s*$/gm
      let m
      while ((m = lineRe.exec(text)) !== null) {
        const { key, value } = m.groups
        result[key] = value
      }
      return result
    }
    
    const envText = `
    DB_HOST=localhost
    DB_PORT=5432
    DB_NAME=myapp_prod
    API_KEY=abc123xyz
    API_TIMEOUT=30
    DEBUG=false
    `
    
    const config = parseConfig(envText)
    console.log('DB_HOST:', config.DB_HOST)       // localhost
    console.log('DB_PORT:', config.DB_PORT)       // 5432
    console.log('API_KEY:', config.API_KEY)       // abc123xyz
    console.log('DEBUG:', config.DEBUG)           // false
    
    // ===== Нежадные квантификаторы ===
    console.log('\n=== Жадный vs нежадный ===')
    const html = '<b>жирный</b> и <i>курсив</i>'
    
    const greedy  = html.match(/<.+>/g)
    const lazy    = html.match(/<.+?>/g)
    console.log('Жадный   (<.+>):', greedy)   // 1 матч — всё
    console.log('Нежадный (<.+?>):', lazy)    // 4 тега отдельно
    
    // ===== Обратные ссылки ===
    console.log('\n=== Обратные ссылки: удвоенные слова ===')
    const doubleWord = /\b(\w+)\s+\1\b/gi
    const text = 'Это это тест. Ошибка ошибка часто бывает. All all good here.'
    
    const dups = []
    let m2
    while ((m2 = doubleWord.exec(text)) !== null) {
      dups.push(m2[0])
    }
    console.log('Удвоения:', dups)

    RegExp: lookahead, lookbehind и группы

    Тебе нужно извлечь все суммы из строки «Итого: 1500₽, скидка 10%, налог 150₽» — только рублёвые, не проценты. Или найти версию в строке «node v18.12, npm v9.5» — числа после «v», но не все числа. Или распарсить конфиг-файл по строкам «key=value». Для всего этого нужны lookahead, lookbehind и именованные группы.

    Какую проблему решает

    Обычные regex-паттерны не умеют «смотреть вперёд» или «назад» без захвата этого контекста. Lookahead/lookbehind позволяют добавить условия на окружение без включения его в результат. Именованные группы делают код читаемым.

    На основе предыдущих уроков

  • RegExp основы: базовые паттерны, flags, .match(), .test()
  • Строки: .replace(), .split() с регулярными выражениями
  • RegExp продвинутый: группы захвата, .exec() в цикле
  • Lookahead — просмотр вперёд

    // X(?=Y) — позитивный: X только если ЗА НИМ следует Y
    // X(?!Y) — негативный: X только если ЗА НИМ НЕ следует Y
    
    // Только числа перед ₽ или $:
    const priceRe = /\d+(?=[₽$])/g
    '100₽ 200$ 50% 300руб'.match(priceRe)  // ['100', '200']  — без 50 и 300!
    
    // Слова не перед двоеточием (не ключи объекта):
    const wordRe = /\w+(?!:)/g

    Lookbehind — просмотр назад

    // (?<=Y)X — позитивный: X только если ПЕРЕД НИМ стоит Y
    // (?<!Y)X — негативный: X только если ПЕРЕД НИМ НЕ стоит Y
    
    // Числа после знака валюты $:
    const afterDollarRe = /(?<=\$)\d+(\.\d{2})?/g
    '$99.99 €200 $150'.match(afterDollarRe)  // ['99.99', '150']
    
    // Версии после "v":
    const versionRe = /(?<=v)\d+\.\d+/g
    'node v18.12 npm v9.5'.match(versionRe)  // ['18.12', '9.5']

    Именованные группы

    // (?<name>...) — захват с именем, доступен через match.groups.name
    // (?:...)      — группа без захвата (только для группировки)
    
    // Парсинг даты ISO
    const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
    const match  = '2024-03-15'.match(dateRe)
    const { year, month, day } = match.groups
    // year='2024', month='03', day='15'
    
    // Деструктуризация прямо из groups

    Обратные ссылки (backreferences)

    // \1 — ссылка на первую группу захвата (в самом regex)
    // \k<name> — ссылка на именованную группу
    
    // Поиск удвоенных слов:
    const doubled = /\b(\w+) \1\b/gi
    'the the cat sat sat'.match(doubled)  // ['the the', 'sat sat']
    
    // Совпадающие кавычки:
    const quoted = /(['"]).*?\1/g

    Нежадные квантификаторы

    // Жадный   * : захватывает максимально
    // Нежадный *?: захватывает минимально
    
    const html = '<b>жирный</b> и <i>курсив</i>'
    
    html.match(/<.+>/g)    // ['<b>жирный</b> и <i>курсив</i>'] — ВСЁ!
    html.match(/<.+?>/g)   // ['<b>', '</b>', '<i>', '</i>']   — отдельно
    
    // Аналогично: +? ?? {n,m}?

    Флаги в regex

    // g — global: все совпадения (не только первое)
    // i — case-insensitive
    // m — multiline: ^ и $ работают с переносами строк
    // s — dotAll: . соответствует переносам строк
    // u — unicode: поддержка Unicode (нужен для эмодзи)
    // d — indices: добавляет .indices с позициями групп

    Важно: двойной слеш в шаблонных строках

    // В регулярном выражении /\d+/ — один слеш
    // В строке (шаблонной или обычной) для передачи в RegExp():
    const re = new RegExp('\\d+')  // \\d → \d в строке → \d в regex

    Типичные ошибки

    Ошибка 1: Флаг g без .exec() в цикле

    // НЕВЕРНО — с флагом g и .match() именованные группы теряются
    const re = /(?<year>\d{4})/g
    '2023 2024'.match(re)  // ['2023', '2024'] — groups нет!
    
    // ВЕРНО — .exec() в цикле сохраняет groups
    let m
    while ((m = re.exec('2023 2024')) !== null) {
      console.log(m.groups.year)  // '2023', '2024'
    }

    Ошибка 2: Забыть сбросить .lastIndex

    const re = /\d+/g
    re.exec('hello 42')  // { index: 6, ... }
    re.exec('1 2 3')     // null! lastIndex=8 не совпал в новой строке
    
    // ВЕРНО — создавай новый regex или сбрасывай:
    re.lastIndex = 0

    Ошибка 3: Жадный квантификатор для HTML

    // НЕВЕРНО — захватывает слишком много
    '<div>раз</div> <div>два</div>'.match(/<div>.*<\/div>/g)
    // ['<div>раз</div> <div>два</div>'] — ВСЁ как одно совпадение!
    
    // ВЕРНО — нежадный
    '<div>раз</div> <div>два</div>'.match(/<div>.*?<\/div>/g)
    // ['<div>раз</div>', '<div>два</div>']

    В реальных проектах

  • Парсинг логов: извлечение timestamp, уровня, сообщения через именованные группы
  • Валидация: пароли, email, телефоны, URL
  • Template engines: поиск {{переменных}} в шаблонах
  • Конфиг-файлы: парсинг .env, nginx.conf, package.json scripts
  • Code generation: поиск и замена паттернов при рефакторинге
  • Примеры

    Lookahead/lookbehind: извлечение цен и версий, именованные группы для дат и URL, парсинг конфига

    // Продвинутые регулярные выражения
    
    // ===== Lookahead =====
    console.log('=== Lookahead: числа перед символом =====')
    
    // Числа перед ₽ или $
    const priceRe = /\d+(\.\d+)?(?=[₽$])/g
    const priceStr = 'Яблоки 45₽, скидка 10%, USB-кабель $12, итого 57₽'
    console.log('Строка:', priceStr)
    console.log('Цены (₽/$):', priceStr.match(priceRe))  // ['45', '12', '57']
    
    // Негативный lookahead: числа НЕ перед %
    const noPercent = /\d+(?!%)/g
    const discountStr = '20% скидка на 500₽ товары, 3% кэшбек'
    const amounts = discountStr.match(/\d+(?=[₽$])/g)
    console.log('Суммы (не проценты):', amounts)  // ['500']
    
    // ===== Lookbehind =====
    console.log('\n=== Lookbehind: числа после символа ===')
    
    // Суммы после $
    const afterDollar = /(?<=\$)\d+(\.\d{2})?/g
    const invoice = 'Subtotal: $99.99, Tax: $8.50, Total: $108.49'
    console.log('Инвойс:', invoice)
    console.log('Суммы:', invoice.match(afterDollar))  // ['99.99', '8.50', '108.49']
    
    // Версии после "v"
    const versionRe = /(?<=v)\d+\.\d+(\.\d+)?/gi
    const releases = 'Node.js v20.5.0, Python v3.11, Chrome v118.0'
    console.log('\nВерсии:', releases.match(versionRe))  // ['20.5.0', '3.11', '118.0']
    
    // ===== Именованные группы =====
    console.log('\n=== Именованные группы ===')
    
    // ISO дата
    const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
    const dateStr = '2024-03-15'
    const dateMatch = dateStr.match(dateRe)
    if (dateMatch?.groups) {
      const { year, month, day } = dateMatch.groups
      console.log('Дата:', { year, month, day })
    }
    
    // URL разбор
    const urlRe = /(?<protocol>https?):\/\/(?<host>[^/?#]+)(?<path>\/[^?#]*)?(?:\?(?<query>[^#]*))?/
    const urlStr = 'https://api.example.com/users/profile?page=1&limit=20'
    const urlMatch = urlStr.match(urlRe)
    if (urlMatch?.groups) {
      const { protocol, host, path, query } = urlMatch.groups
      console.log('\nURL разбор:')
      console.log('  protocol:', protocol)  // https
      console.log('  host:    ', host)      // api.example.com
      console.log('  path:    ', path)      // /users/profile
      console.log('  query:   ', query)     // page=1&limit=20
    }
    
    // ===== Парсинг конфига ===
    console.log('\n=== Парсинг key=value конфига ===')
    
    function parseConfig(text) {
      const result = {}
      const lineRe = /^(?<key>[\w.]+)\s*=\s*(?<value>.+?)\s*$/gm
      let m
      while ((m = lineRe.exec(text)) !== null) {
        const { key, value } = m.groups
        result[key] = value
      }
      return result
    }
    
    const envText = `
    DB_HOST=localhost
    DB_PORT=5432
    DB_NAME=myapp_prod
    API_KEY=abc123xyz
    API_TIMEOUT=30
    DEBUG=false
    `
    
    const config = parseConfig(envText)
    console.log('DB_HOST:', config.DB_HOST)       // localhost
    console.log('DB_PORT:', config.DB_PORT)       // 5432
    console.log('API_KEY:', config.API_KEY)       // abc123xyz
    console.log('DEBUG:', config.DEBUG)           // false
    
    // ===== Нежадные квантификаторы ===
    console.log('\n=== Жадный vs нежадный ===')
    const html = '<b>жирный</b> и <i>курсив</i>'
    
    const greedy  = html.match(/<.+>/g)
    const lazy    = html.match(/<.+?>/g)
    console.log('Жадный   (<.+>):', greedy)   // 1 матч — всё
    console.log('Нежадный (<.+?>):', lazy)    // 4 тега отдельно
    
    // ===== Обратные ссылки ===
    console.log('\n=== Обратные ссылки: удвоенные слова ===')
    const doubleWord = /\b(\w+)\s+\1\b/gi
    const text = 'Это это тест. Ошибка ошибка часто бывает. All all good here.'
    
    const dups = []
    let m2
    while ((m2 = doubleWord.exec(text)) !== null) {
      dups.push(m2[0])
    }
    console.log('Удвоения:', dups)

    Задание

    Реализуй три функции с продвинутыми regex. - `extractPrices(str)` — извлекает все числа перед `$` или `₽` через lookahead. Возвращает массив чисел - `validatePassword(str)` — проверяет: минимум 8 символов, есть заглавная буква, есть цифра, есть спецсимвол. Возвращает `{ valid, checks }` - `parseConfig(str)` — парсит строки `"key=value"` через именованные группы (`(?<key>...)`, `(?<value>...)`). Возвращает объект

    Подсказка

    extractPrices: /\d+(\.\d+)?(?=[$₽])/g, match().map(Number). validatePassword: отдельный regex.test(str) для каждого условия. parseConfig: /^(?<key>[\w.]+)\s*=\s*(?<value>.+?)$/gm, re.exec(str) в цикле, result[m.groups.key] = m.groups.value

    Загружаем среду выполнения...
    Загружаем AI-помощника...