# Чёрная дыра бэктрекинга Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже "подвешивать" интерпретатор JavaScript. Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение -- легче лёгкого. Типична ситуация, когда регулярное выражение до поры до времени работает нормально, и вдруг на каком-то тексте как начнёт "подвешивать" интерпретатор и есть 100% процессора. Это может стать уязвимостью. Например, если JavaScript выполняется на сервере, то при разборе данных, присланных посетителем, он может зависнуть, если использует подобный регэксп. На клиенте тоже возможно подобное, при использовании регэкспа для подсветки синтаксиса. Такие уязвимости "убивали" почтовые сервера и системы обмена сообщениями и до появления JavaScript, и наверно будут "убивать" и после его исчезновения. Так что мы просто обязаны с ними разобраться. [cut] ## Пример План изложения у нас будет таким:
<a href="..." class=doc ...>
.
Самый простой способ это сделать -- <[^>]*>
. Но он же и не совсем корректный, так как тег может выглядеть так: <a test="<>" href="#">
. То есть, внутри "закавыченного" атрибута может быть символ `>`. Простейший регэксп на нём остановится и найдёт <a test="<>
.
Соответствие:
```
<[^>]*....>
```
А нам нужен весь тег.
Для того, чтобы правильно обрабатывать такие ситуации, нужно учесть их в регулярном выражении. Оно будет иметь вид <тег (ключ=значение)*>
.
Если перевести на язык регэкспов, то: <\w+(\s*\w+=(\w+|"[^"]*")\s*)*>
:
<\w+
-- начало тега(\s*\w+=(\w+|"[^"]*")\s*)*
-- произвольное количество пар вида `слово=значение`, где "значение" может быть также словом \w+
, либо строкой в кавычках "[^"]*"
.<a test="<>" href="#">
, так и одинокий <b>
.
А теперь -- демонстрация проблемы.
Если запустить пример ниже, то он может подвесить браузер:
```js
//+ run
var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g;
var str = "*
здесь выглядит лишним.
Если хочется найти число, то с тем же успехом можно искать \d+$
.
Да, этот регэксп носит искусственный характер, но, разобравшись с ним, мы поймём и практический пример, данный выше. Причина их медленной работы одинакова.
В целом, с регэкспом "всё так", синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему.
Посмотрим, что происходит при поиске в строке 123456789z
:
\d+
. Плюс +
является жадным по умолчанию, так что он хватает все цифры, какие может:
```
\d+.......
(123456789)z
```
(\d+)*
, но больше цифр нет, так что звёздочка не даёт повторений.
Затем в шаблоне идёт символ конца строки $
, а в тексте -- символ z
.
```
X
\d+........$
(123456789)z
```
Соответствия нет.
+
отступает на один символ (бэктрекинг).
Теперь `\d+` -- это все цифры, за исключением последней:
```
\d+.......
(12345678)9z
```
\d+
содержит всё число, кроме последней цифры. Движок снова пытается найти совпадение, уже с новой позиции (`9`).
Звёздочка (\d+)*
теперь может быть применена -- она даёт число 9
:
```
\d+.......\d+
(12345678)(9)z
```
Движок пытается найти `$`, но это ему не удаётся -- на его пути опять `z`:
```
X
\d+.......\d+
(12345678)(9)z
```
Так как совпадения нет, то поисковой движок отступает назад ещё раз.
\d+
будет содержать 7 цифр, а остаток строки 89
становится вторым \d+
:
```
X
\d+......\d+
(1234567)(89)z
```
Увы, всё ещё нет соответствия для $
.
Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй \d+
, до одного символа 8
, и звёздочка забирает следующий 9
.
```
X
\d+......\d+\d+
(1234567)(8)(9)z
```
\d+
отступили по-максимуму, так что сокращается снова первое число, до 123456
, а звёздочка берёт оставшееся:
```
X
\d+.......\d+
(123456)(789)z
```
Снова нет совпадения. Процесс повторяется, последний жадный квантификатор +
отпускает один символ (`9`):
```
X
\d+.....\d+ \d+
(123456)(78)(9)z
```
\d+
на \d+?
и посмотрим (аккуратно, может подвесить браузер):
```js
//+ run
alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
```
Не помогло!
**Ленивые регулярные выражения делают то же самое, но в обратном порядке.**
Просто подумайте о том, как будет в этом случае работать поисковой движок.
Некоторые движки регулярных выражений содержат хитрые проверки и конечные автоматы, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда.
Возвращаясь к примеру выше -- при поиске <(\s*\w+=\w+\s*)*>
в строке <a=b a=b a=b a=b
происходит то же самое.
Поиск успешно начинается, выбирается некая комбинация из \s*\w+=\w+\s*
, которая, так как в конце нет `>`, оказывается не подходящей. Движок честно отступает, пробует другую комбинацию -- и так далее.
## Что делать?
Проблема -- в сверхмноговариантном переборе.
Движок регулярных выражений перебирает кучу возможных вариантов скобок там, где это не нужно.
Например, в регэкспе (\d+)*$
нам (людям) очевидно, что в (\d+)
откатываться не нужно. От того, что вместо одного \d+
у нас два независимых \d+\d+
, ничего не изменится.
Без разницы:
```
\d+........
(123456789)z
\d+...\d+....
(1234)(56789)z
```
Если вернуться к более реальному примеру <(\s*\w+=\w+\s*)*>
то
cам алгоритм поиска, который у нас в голове, предусматривает, что мы "просто" ищем тег, а потом пары `атрибут=значение` (сколько получится).
Никакого "отката" здесь не нужно.
В современных регулярных выражениях для решения этой проблемы придумали "possessive" (сверхжадные? неоткатные? точный перевод пока не устоялся) квантификаторы, которые вообще не используют бэктрегинг.
То есть, они даже проще, чем "жадные" -- берут максимальное количество символов и всё. Поиск продолжается дальше. При несовпадении никакого возврата не происходит.
Это, c стороны уменьшает количество возможных результатов, но с другой стороны -- в ряде случаев очевидно, что возврат (уменьшение количество повторений квантификатора) результата не даст. А только потратит время, что как раз и доставляет проблемы. Как раз такие ситуации и описаны выше.
Есть и другое средство -- "атомарные скобочные группы", которые запрещают перебор внутри скобок, по сути позволяя добиваться того же, что и сверхжадные квантификаторы,
К сожалению, в JavaScript они не поддерживаются.
Однако, можно получить подобный эффект при помощи предпросмотра. Подробное описание соответствия с учётом синтаксиса сверхжадных квантификаторов и атомарных групп есть в статьях [Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead](http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead) и [Mimicking Atomic Groups](http://blog.stevenlevithan.com/archives/mimic-atomic-groups), здесь же мы останемся в рамках синтаксиса JavaScript.
Взятие максимального количества повторений `a+` без отката выглядит так: (?=(a+))\1
.
То есть, иными словами, предпросмотр ?=
ищет максимальное количество повторений a+
, доступных с текущей позиции. А затем они "берутся в результат" обратной ссылкой \1
. Дальнейший поиск -- после найденных повторений.
Откат в этой логике принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 штук a+
, и в результате поиск не удался, то он не будет откатываться на 4 повторения. Эта возможность в предпросмотре отсутствует, а в данном случае она как раз и не нужна.
Исправим регэксп для поиска тега с атрибутами <\w+(\s*\w+=(\w+|"[^"]*")\s*)*>
, описанный в начале главы. Используем предпросмотр, чтобы запретить откат на меньшее количество пар `атрибут=значение`:
```js
//+ run
// регэксп для пары атрибут=значение
var attr = /(\s*\w+=(\w+|"[^"]*")\s*)/
// используем его внутри регэкспа для тега
var reg = new RegExp('<\\w+(?=(' + attr.source + '*))\\1>', 'g');
var good = '...... ...';
var bad = "<a test="<>" href="#">
, так и одинокий <b>
.