en.javascript.info/10-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/article.md
2015-02-27 13:21:58 +03:00

5.8 KiB
Raw Blame History

Чёрная дыра бэктрекинга

Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже подвешивать браузер.

[cut] Например, попробуйте пример ниже в Chrome или IE (осторожно, подвесит браузер!):

//+ run
alert( '123456789012345678901234567890z'.match(/(\d+)*$/) );

Некоторые движки регулярных выражений (Firefox) справляются с таким регэкспом, а некоторые (IE, Chrome) -- нет.

В чём же дело, что не так с регэкспом?

Да с регэкспом-то всё так, синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему.

Для краткости рассмотрим более короткую строку: 1234567890z:

  1. Первым делом, движок регэкспов пытается найти \d+. Плюс + является жадным по умолчанию, так что он хватает все цифры, какие может.

    Затем движок пытается применить звёздочку вокруг скобок (\d+)*, но больше цифр нет, так что звёздочка не даёт повторений.

    После этого в паттерне остаётся $, а в тексте -- символ z.

    Так как соответствия нет, то жадный плюс + отступает на один символ (бэктрекинг, зелёная стрелка на рисунке выше).

  2. После бэктрекинга, \d+ содержит всё число, кроме последней цифры. Затем движок снова пытается найти совпадение, уже с новой позиции (`9`).

    Звёздочка (\d+)* теперь может быть применена -- она даёт ещё одно число 9:

    Движок пытается найти $, но это ему не удаётся -- на его пути опять z:

    Так как совпадения нет, то поисковой движок отступает назад ещё раз.

  3. Теперь первое число \d+ будет содержать 8 цифр, а остаток строки 90 становится вторым \d+:

    Увы, всё ещё нет соответствия для $.

    Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй \d+, до одного символа 9.

  4. Теперь движок регулярных выражений снова может применить звёздочку и находит третье число \d+:

    ...И снова неудача. Второе и третье \d+ отступили по-максимуму, так что сокращается снова первое число.

  5. Теперь есть 7 цифр в первом \d+. Поисковой движок видит место для второго \d+, теперь уже с позиции 8:

    Так как совпадения нет, второй \d+ отступает назад....

  6. ...И так далее, легко видеть, что поисковой движок будет перебирать *все возможные комбинации* \d+ в числе. А их много.

На этом месте умный читатель может воскликнуть: "Бэктрекинг? Давайте включим ленивый режим -- и не будет никакого бэктрекинга!"

Что ж, заменим \d+ на \d+? и посмотрим (аккуратно, может подвесить браузер):

//+ run
alert( '123456789012345678901234567890z'.match(/(\d+?)*$/) );

Не помогло!

Ленивые регулярные выражения делают то же самое, но в обратном порядке.

Просто подумайте о том, как будет в этом случае работать поисковой движок.

Некоторые движки регулярных выражений, например Firefox, содержат хитрые проверки, в дополнение к алгоритму выше, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда.