03 / РУКОВОДСТВА
Безопасный процесс тестирования регулярных выражений
Цикл «написал — проверил — отрефакторил» для регулярных выражений JavaScript: реалистичные примеры данных, гигиена катастрофического бэктрекинга, дисциплина якорей, семантика флагов и почему тестирование в браузере хранит паттерны в тайне.
Цикл: написал, проверил, отрефакторил
Надёжный regex никогда не пишется за один проход. Начните с наименьшего паттерна, который совпадает с одним реальным примером, прогоните его, затем расширяйте или сужайте по следующему примеру. Текст примера — это спецификация; паттерн — реализация.
Цикл быстр, когда обратная связь мгновенна: тестер, переоценивающий паттерн на каждое нажатие клавиши, превращает каждую правку в эксперимент с немедленным ответом — поэтому специальная рабочая область лучше ручного перезапуска скрипта.
- Начните с примераПишите паттерн по конкретному примеру, а не по памяти: вставьте настоящую строку лога или документа, который собираетесь разбирать.
- Тестируйте каждую правкуТестируйте после каждого изменения — у regex нет предупреждений компилятора, поэтому единственная обратная связь — что он совпал и что пропустил.
- Рефакторинг для читателейПосле успеха отрефакторьте для читателя: именованные и незахватывающие группы делают рабочий паттерн выживаемым для следующего читателя.
Уважайте катастрофический бэктрекинг
Движок regex в JavaScript бэктрекает: когда совпадение не удалось, он перепробует разные длины для каждого квантификатора. Вложенные квантификаторы вроде (a+)+ на почти совпадающей строке взрывают число повторов экспоненциально — паттерн, отвечающий мгновенно на хорошем вводе, может работать годами на плохом. Атакующие называют это ReDoS.
Тестер регулярных выражений JavaScript ограничивает радиус поражения: паттерны исполняются в выделенном Web Worker с бюджетом 650 миллисекунд, а вышедшее из-под контроля выражение сообщается как явный таймаут, а не как зависшая вкладка. Если ваш паттерн упирается в этот потолок на реалистичном вводе, считайте это находкой — тот же ввод в продакшене остановил бы цикл событий.
- Вложенные квантификаторыКлассическая форма — квантифицированная группа с квантифицированным токеном внутри: (a+)+, (\w+)* — где множество разбиений одной строки удовлетворяют внутреннему паттерну.
- Неоднозначная альтернацияНеоднозначная альтернация делает то же: (a|aa)+ даёт движку два способа совпасть с каждым лишним символом, удваивая дерево поиска на символ.
- Структурные исправленияИсправления структурные: сделайте внутреннее совпадение однозначным, ограничьте его явными лимитами длины и всегда тестируйте почти совпадающий ввод, а не только валидный.
Ставьте якоря до того, как доверять совпадению
Паттерн, находящий подстроку, — не паттерн, валидирующий значение. /\d+/ совпадёт внутри abc123xyz; /^\d+$/ примет только цифры. Большинство ошибок валидации — потерянные якоря, а большинство ошибок извлечения — якоря, которых быть не должно.
- Начало и конец^ и $ прибивают совпадение к началу и концу строки — или к каждой строке при включённом флаге multiline, что незаметно меняет смысл «всего ввода».
- Границы словГраницы слов \b не дают коротким паттернам совпадать внутри длинных слов: \bcat\b находит животное, а не cat внутри concatenate.
- Поиск или валидацияРешите «поиск или валидация» заранее: паттерны извлечения обычно не хотят якорей, валидаторы почти всегда хотят оба.
Тестируйте на реалистичных данных
Паттерн, доказанный на трёх аккуратных примерах, падает в продакшене на четвёртом реальном. Соберите набор примеров, отражающий распределение ввода: самое длинное допустимое значение, пустая строка, варианты пробелов, имена с Unicode и почти-совпадение, которое должно почти совпасть, но не должно.
- Почти-совпаденияВключите почти-совпадение: почтовый паттерн стоит тестировать на a@b и a b@c.d, а не только на корректных адресах.
- UnicodeВключайте Unicode там, где он есть в данных: \w и . меняют смысл под флагом u, а имена с акцентами ломают предположения эпохи ASCII.
- Перезапускайте наборДержите набор примеров рядом с паттерном; при изменении паттерна перезапускайте весь набор, а не только случай, вызвавший правку.
Флаги меняют язык, а не только поиск
Флаги — часть смысла паттерна. g решает, получите ли вы первое совпадение или все, i сворачивает регистр, m перепривязывает ^ и $ к строкам, s позволяет точке пересекать переводы строк, а u переключает движок в Unicode-режим с более строгим синтаксисом.
Рабочая область вычисляет паттерн с выбранными флагами и перечисляет каждое совпадение со смещениями начала и конца, нумерованные и именованные группы с их позициями и живой предпросмотр замены — так что смена флага видна как конкретная разница в результате, а не как теория.
- g и lastIndexБез g test() и match() видят только первое попадание; с g объект regex несёт состояние lastIndex, и повторные вызовы test() чередуют результаты — печально известная продакшен-ошибка.
- s расширяет точкуs (dotAll) заставляет . совпадать с переводами строк, что может тихо расширить аппетит старого паттерна при глобальном включении.
- u и компанияu делает классы и экранирования корректными для Unicode и отвергает неоднозначный унаследованный синтаксис; тестер также поддерживает d (индексы совпадений), y (sticky) и v (наборы Unicode).
Держите паттерн и примеры локально
Тестовые данные часто — настоящие данные: адреса клиентов, строки внутренних логов, идентификаторы из отчёта об инциденте. Вставка их в hosted-тестер отправляет их в чужую инфраструктуру. Браузерный тестер держит сессию во вкладке — паттерн и текст примера вычисляются движком вашего собственного устройства и никогда не передаются.
Локальный рантайм честен о своих потолках: паттерны до 4 096 символов, текст примера до 100 000 символов, замены до 20 000 и не более 1 000 сообщаемых совпадений за прогон. Эти лимиты рассчитаны на разработку и отладку; массовый разбор логов — задача локального скрипта, а не вкладки браузера.
Копируйте наружу только нужное — готовый паттерн и строку замены — и оставьте данные примеров там, где они начались: на вашей машине.
Протестируйте паттерн локальноИзвлечённые данные обычно оказываются в JSON.
Когда паттерн вытащил значения из сырого текста, инспектируйте и сравнивайте полученную нагрузку детерминированной локальной процедурой.