You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 9-regular-expressions/15-regexp-infinite-backtracking-problem/article.md
+26-26Lines changed: 26 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,30 +1,30 @@
1
-
# Проблема поиска с вечным бэктрекингом
1
+
# Проблема поиска с бесконечным бэктрекингом
2
2
3
3
Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже "подвешивать" интерпретатор JavaScript.
4
4
5
-
Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение –- проще простого.
5
+
Рано или поздно с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение –- проще простого.
6
6
7
7
Типичная ситуация: регулярное выражение работает нормально, но иногда, с некоторыми строками, "подвешивает" интерпретатор и потребляет 100% процессора.
8
8
9
9
В веб-браузере такой случай убивает страницу. Явно плохая ситуация.
10
10
11
-
Такой код, выполняемый на стороне сервера, может стать уязвимостью, так как он использует регулярные выражения для обработки пользовательских данных. Некорректный ввод данных приведет к зависанию процесса и, как следствие, отказу сервиса. Автор(?) лично видел и сообщал о таких уязвимостях даже для очень известных и широко используемых программ.
11
+
Такой код, выполняемый на стороне сервера, может стать уязвимостью, так как он использует регулярные выражения для обработки пользовательских данных. Некорректный ввод данных приведет к зависанию процесса и, как следствие, отказу сервиса. Автор лично видел и сообщал о таких уязвимостях даже для очень известных и широко используемых программ.
12
12
13
13
Так что проблема, несомненно, достойна рассмотрения.
14
14
15
15
## Вступление
16
16
17
17
План изложения у нас будет таким:
18
18
19
-
1. Сначала посмотрим на проблему в реальной ситуации.
20
-
2. Потом упростим реальную ситуацию до "корней" и увидим, откуда она берётся.
21
-
3. Ну и, на конец, исправим её.
19
+
1. Сначала взглянем на проблему, на то, как это могло произойти.
20
+
2. Потом упростим ситуацию и увидим, почему проблема возникает.
21
+
3. Ну и, наконец, исправим её.
22
22
23
23
Например, давайте рассмотрим поиск тегов в HTML.
24
24
25
-
Мы хотим найти все теги с атрибутами (или без них), типа: `subject:<a href="..." class="doc" ...>`. Нужно чтобы регулярное выражение работало надёжно, так как HTML приходит из Интернета и может быть запутанным.
25
+
Мы хотим найти все теги с атрибутами (или без них) типа: `subject:<a href="..." class="doc" ...>`. Нужно, чтобы регулярное выражение работало надёжно, так как HTML приходит из Интернета и может быть "грязным".
26
26
27
-
В частности, нам нужно, чтобы оно соответствовало тегам типа: `<a test="<>" href="#">` -- т.е. с символами `<` и `>` внутри атрибутов, так как это поддерживается [стандартом HTML](https://html.spec.whatwg.org/multipage/syntax.html#syntax-attributes).
27
+
В частности, чтобы регулярное выражение находило теги типа: `<a test="<>" href="#">` -- т.е. с символами `<` и `>` внутри атрибутов, так как это поддерживается [стандартом HTML](https://html.spec.whatwg.org/multipage/syntax.html#syntax-attributes).
28
28
29
29
Как видим, простое регулярное выражение `pattern:<[^>]+>` не работает, потому что оно останавливает поиск на первом `>`, а нам нужно игнорировать `<>`, если они являются частью атрибута.
Для того, чтобы правильно обрабатывать подобные ситуации, нужно более сложное регулярное выражение. Оно будет иметь вид: `pattern:<tag (key=value)*>`.
37
37
38
-
1. Для `tag`: `pattern:\w+`,
39
-
2. Для `key`: `pattern:\w+`,
40
-
3. И для`value`: строка в кавычках `pattern:"[^"]*"`.
38
+
1. Для имени тега `tag`: `pattern:\w+`,
39
+
2. Для имени атрибута `key`: `pattern:\w+`,
40
+
3. И значения атрибута`value`: строка в кавычках `pattern:"[^"]*"`.
41
41
42
-
Если мы подставим это в паттерн описанный выше и добавим дополнительные пробелы `pattern:\s`, то получим следующее: `pattern:<\w+(\s*\w+="[^"]*"\s*)*>`.
42
+
Если мы подставим это в паттерн, описанный выше, и добавим дополнительные пробелы `pattern:\s`, то получим следующее: `pattern:<\w+(\s*\w+="[^"]*"\s*)*>`.
43
43
44
44
Это регулярное выражение неидеально! Оно всё ещё не поддерживает все детали HTML, например, значения в кавычках, и, хотя и есть способы улучшить его, давайте не будем его усложнять. Оно продемонстрирует нам проблему.
Отлично! Нашло длинный `match:<a test="<>" href="#">` и короткий `match:<b>` теги.
57
57
58
-
Теперь у нас есть на вид рабочее решение. А теперь – демонстрация проблемы.
58
+
Теперь у нас есть рабочее на вид решение. А теперь – демонстрация проблемы.
59
59
60
60
## Чёрная дыра бэктрекинга
61
61
62
-
Если запустить пример ниже, то он может подвесить браузер (или другой JavaScript хост):
62
+
Если запустить пример ниже, то он может подвесить браузер (или другую среду, где выполняется JavaScript):
63
63
64
64
```js run
65
65
let reg =/<\w+(\s*\w+="[^"]*"\s*)*>/g;
@@ -79,10 +79,10 @@ alert( str.match(reg) );
79
79
80
80
Давайте упростим регулярное выражение, удалив имя тега и кавычки. Теперь мы ищем только атрибуты -- пары `key=value`: `pattern:<(\s*\w+=\w+\s*)*>`.
81
81
82
-
К сожалению, регулярное выражение все еще "зависает":
82
+
К сожалению, регулярное выражение всё ещё "зависает":
83
83
84
84
```js run
85
-
// поиск только по атрибутам, разделенных пробелом
85
+
// поиск только по атрибутам, разделённым пробелом
86
86
let reg =/<(\s*\w+=\w+\s*)*>/g;
87
87
88
88
let str =`<a=b a=b a=b a=b a=b a=b a=b a=b
@@ -94,11 +94,11 @@ alert( str.match(reg) );
94
94
*/!*
95
95
```
96
96
97
-
На этом мы закончим с демонстрацией "практического примера" и перейдём к разбору происходящего и способам устранения проблемы.
97
+
На этом мы закончим с демонстрацией практического примера и перейдём к разбору происходящего и способам устранения проблемы.
98
98
99
99
## Подробный пример
100
100
101
-
Чтобы сделать пример еще проще, давайте рассмотрим `pattern:(\d+)*$`.
101
+
Чтобы сделать пример ещё проще, давайте рассмотрим `pattern:(\d+)*$`.
102
102
103
103
Это регулярное выражение имеет ту же проблему. В большинстве движков регулярных выражений этот поиск занимает очень много времени (осторожно - может "зависнуть"):
Поисковой движок снова должен отступить назад. В общем бэктрекинг работает так: последний жадный квантификатор понижает количество повторений до тех пор, пока это возможно. Затем понижает предыдущий "жадный" квантификатор и т.д. В нашем случае последний "жадный" квантификатор -- это второй `pattern:\d+`, сокращающий `subject:89` до `subject:8`, а звёздочка берёт `subject:9`:
168
+
Поисковый движок снова должен отступить назад. В общем, бэктрекинг работает так: последний жадный квантификатор понижает количество повторений до тех пор, пока это возможно. Затем понижает предыдущий "жадный" квантификатор и т.д. В нашем случае последний "жадный" квантификатор -- это второй `pattern:\d+`, сокращающий `subject:89` до `subject:8`, а звёздочка берёт `subject:9`:
**"Ленивые" регулярные выражения делают то же самое, но в обратном порядке.**
206
206
207
-
Просто подумайте о том, как будет в этом случае работать поисковой движок.
207
+
Просто подумайте о том, как будет в этом случае работать поисковый движок.
208
208
209
209
Некоторые движки регулярных выражений содержат хитрые проверки и конечные автоматы, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но не все движки и не всегда.
В современных регулярных выражениях для решения этой проблемы придумали "possessive" (сверхжадные? неоткатные? точный перевод пока не устоялся) квантификаторы, которые вообще не используют бэктрегинг. То есть, они даже проще, чем "жадные" – берут максимальное количество символов и всё. Поиск продолжается дальше. Также есть "атомарные скобочные группы" -- средство, запрещающее перебор внутри скобок.
249
+
В современных регулярных выражениях для решения этой проблемы придумали сверхжадные ("possessive") квантификаторы, которые вообще не используют бэктрегинг. То есть, они даже проще, чем "жадные" – берут максимальное количество символов и всё. Поиск продолжается дальше. Также есть "атомарные скобочные группы" -- средство, запрещающее перебор внутри скобок.
250
250
251
251
К сожалению, в JavaScript они все не поддерживаются.
252
252
253
253
### Предпросмотр в помощь!
254
254
255
255
Но мы можем исключить бэктрекинг с помощью предпросмотра.
256
256
257
-
Паттерн, совершающий максимальное количество повторений без "отката" выглядит так: `pattern:(?=(a+))\1`.
257
+
Паттерн, совершающий максимальное количество повторений без "отката", выглядит так: `pattern:(?=(a+))\1`.
258
258
259
259
Другими словами:
260
260
- Предпросмотр `pattern:?=` ищет максимальное количество `pattern:a+`, доступных с текущей позиции.
Откат в этой логике в принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 `pattern:a+`, и в результате поиск не удался, то он не будет откатываться на 4 повторения.
264
264
265
265
```smart
266
-
Больше о взаимодействиях кватификаторов "possessive" и предпросмотра вы можете найти в статьях [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).
266
+
Больше о взаимодействиях сверхжадных кватификаторов и предпросмотра вы можете найти в статьях [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).
Отлично, всё работает! Нашло как длинный тег `match:<a test="<>" href="#">`, так и одинокий `match:<b>`, и (!) не "вешает" интерпретатор при некорректных данных.
290
290
291
-
Обратите внимание на свойство `attrReg.source`. Объект `RegExp` предоставляет доступ к своей (?)исходной(?) строке. Это удобно, когда мы хотим вставить одно регулярное выражение в другое.
291
+
Обратите внимание на свойство `attrReg.source`. Объект `RegExp` предоставляет доступ к своей исходной (`source`) строке. Это удобно, когда мы хотим вставить одно регулярное выражение в другое.
0 commit comments