Тестер регулярных выражений
Проверьте шаблон на реальном тексте с подсветкой совпадений и жёсткой остановкой при катастрофическом бэктрекинге.
Зачем тестеру регулярных выражений нужен воркер
Почти вся механика этой страницы существует ради одного сценария отказа. Возьмите /(a+)+b/ и тридцать букв a без b. Движку надо решить, как поделить эти тридцать символов между внутренним и внешним квантификатором, а способов сделать это 2³⁰. Каждый будет опробован, прежде чем совпадение объявят невозможным.
Важно вот что: выполняющееся регулярное выражение нельзя прервать. JavaScript однопоточен, поэтому сторож на setTimeout не сработает, пока выражение не вернётся, — а для такого ввода это где-то за тепловой смертью Вселенной. Вкладки больше нет.
Поэтому шаблон исполняется в Web Worker, в своём потоке, а главная страница держит таймер. Когда таймер побеждает, worker.terminate() попросту убивает поток. Это единственный работающий механизм, и именно поэтому тестер сообщает вам, что шаблон опасен, вместо того чтобы превратиться в зависшую вкладку.
Это стоит усвоить, потому что тот же шаблон в боевом коде — это отказ в обслуживании: пользовательский ввод сопоставляется с уязвимым выражением, и один запрос занимает ядро процессора на неопределённый срок. Этот класс ошибок называют ReDoS.
Каких конструкций избегать
| Опасно | Почему | Безопаснее |
|---|---|---|
(a+)+ | Вложенные квантификаторы — экспоненциальное число разбиений | a+ |
(\w|\s)* | Альтернатива внутри квантификатора, ветви перекрываются | [\w\s]* |
(\d+)*$ | Группа с квантификатором и якорем, вынуждающим полный перебор | \d*$ |
.*.*= | Две неограниченные маски борются за один и тот же текст | [^=]*= |
Общий знаменатель — неоднозначность: у шаблона больше одного способа поглотить одни и те же символы. Класс символов делает работу альтернативы, не создавая неоднозначности, — поэтому в колонке «безопаснее» в основном просто | заменена на [].
Жадность, лень и ошибка, которую пишет однажды каждый
Квантификаторы по умолчанию жадны: .+ забирает всё, что может, и отдаёт символы обратно только по принуждению. На строке <b>bold</b>:
| Шаблон | Совпадает с |
|---|---|
<.+> | <b>bold</b> — целиком |
<.+?> | <b> — лениво, останавливается на первом > |
<[^>]+> | <b> — и быстрее, потому что откатывать нечего |
Тянуться следует именно к третьей форме. Отрицаемый класс символов не может проскочить лишнего, поэтому и откатывать нечего: он и яснее выражает намерение, и дешевле в исполнении, чем ленивый вариант.
lastIndex, и почему исчезает каждое второе совпадение
Объект RegExp с флагом g хранит состояние. Он держит lastIndex, который сдвигается после каждого exec или test, поэтому повторное использование одного объекта в разных вызовах продолжает поиск оттуда, где он остановился:
const re = /\d+/g;
re.test('123'); // true, lastIndex is now 3
re.test('123'); // false! it resumed from index 3Создавайте выражение внутри цикла, сбрасывайте lastIndex = 0 либо используйте matchAll, который сам этим управляет. Тестер выше создаёт новый объект на каждый запуск, так что здесь вы видите поведение первого вызова.
Чем регулярные выражения JavaScript отличаются
Результаты здесь в точности совпадают с Node и кодом в браузере, потому что это тот же движок. По сравнению с другими языками спотыкаются обычно на этом:
- Нет атомарных групп и захватывающих квантификаторов. В PCRE есть
(?>...)иa++, полностью запрещающие откат; в JavaScript нет ни того, ни другого, отчего ReDoS здесь и пишется легче. - Нет рекурсии. Сбалансированные скобки регулярным выражением JavaScript не разобрать. Если задача в этом, вам нужен парсер.
- Ретроспективная проверка появилась поздно.
(?<=...)поддерживается в современных браузерах, но не в старом Safari, так что сверьтесь со своими целевыми платформами. - RE2 в Go — другая машина. Гарантированное линейное время, ни обратных ссылок, ни просмотров. Если вы когда-нибудь недоумевали, зачем Go отказался от возможностей, — вот вам причина.
Смежное
Если текст, по которому вы ищете, — это JSON, то сначала отформатировать его обычно делает шаблон ненужным: на структурированных данных парсер выигрывает у регулярного выражения всегда. Если вам нужно расписание, а не поиск, соседний инструмент — разборщик cron.
Вопросы о регулярных выражениях
Почему мой шаблон остановили через две секунды?
Потому что он катастрофически откатывался. Некоторые конструкции — вложенные квантификаторы вроде (a+)+ или (\w|\s)* — заставляют движок перебирать экспоненциальное число вариантов совпадения, так что строка из сорока символов может считаться дольше, чем существует Вселенная. Прервать уже выполняющееся регулярное выражение из JavaScript невозможно, поэтому здесь шаблон работает в Web Worker: воркер можно завершить, и страница остаётся отзывчивой. Тайм-аут в основном потоке сработал бы только после завершения регулярного выражения, а для таких шаблонов это «никогда».
Здесь тот же движок регулярных выражений, что и в моём коде?
Используется движок вашего браузера, то есть движок JavaScript, — поэтому результаты в точности совпадают с Node и кодом в браузере. С PCRE, Python, Go или Java они совпадут не всегда. В JavaScript нет атомарных групп, нет захватывающих квантификаторов и нет рекурсии; ретроспективная проверка в современных браузерах есть, но появилась поздно. RE2 в Go устроен принципиально иначе и намеренно не поддерживает обратные ссылки — именно так он и обходит описанную выше проблему отката.
Почему моё глобальное регулярное выражение находит только каждое второе совпадение?
Объект RegExp с флагом g хранит lastIndex, который сдвигается с каждым вызовом, поэтому повторное использование одного и того же объекта в разных вызовах exec или test продолжает поиск с того места, где он остановился. Это одна из самых надёжно сбивающих с толку частей этого API. Либо создавайте регулярное выражение заново каждый раз, либо сбрасывайте lastIndex в 0 перед использованием, либо берите matchAll, который сделает это за вас.
Чем жадный квантификатор отличается от ленивого?
Жадный забирает столько, сколько может, и отдаёт символы обратно только под принуждением; ленивый (пишется с ? на конце) берёт как можно меньше и расширяется, лишь когда иначе нельзя. На строке <b>bold</b> шаблон <.+> совпадёт со всей строкой, потому что .+ проглатывает всё и откатывается ровно настолько, чтобы найти финальный >. Запись <.+?> совпадёт только с <b>. Это самая частая причина, по которой шаблон захватывает куда больше задуманного.
Можно ли вставлять сюда настоящие данные?
Да. Шаблон и текст передаются воркеру внутри этой страницы и вычисляются там. Ничего не отправляется на сервер — именно поэтому и можно отменить сорвавшийся шаблон: ждать нечего, никакого запроса нет.
Последняя проверка . Заметили, что что-то устарело? Напишите нам.
