Skip to content

Commit 9d28ab2

Browse files
committed
fixes
1 parent 1218af6 commit 9d28ab2

File tree

1 file changed

+26
-26
lines changed
  • 9-regular-expressions/15-regexp-infinite-backtracking-problem

1 file changed

+26
-26
lines changed

9-regular-expressions/15-regexp-infinite-backtracking-problem/article.md

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
1-
# Проблема поиска с вечным бэктрекингом
1+
# Проблема поиска с бесконечным бэктрекингом
22

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

5-
Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение –- проще простого.
5+
Рано или поздно с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение –- проще простого.
66

77
Типичная ситуация: регулярное выражение работает нормально, но иногда, с некоторыми строками, "подвешивает" интерпретатор и потребляет 100% процессора.
88

99
В веб-браузере такой случай убивает страницу. Явно плохая ситуация.
1010

11-
Такой код, выполняемый на стороне сервера, может стать уязвимостью, так как он использует регулярные выражения для обработки пользовательских данных. Некорректный ввод данных приведет к зависанию процесса и, как следствие, отказу сервиса. Автор(?) лично видел и сообщал о таких уязвимостях даже для очень известных и широко используемых программ.
11+
Такой код, выполняемый на стороне сервера, может стать уязвимостью, так как он использует регулярные выражения для обработки пользовательских данных. Некорректный ввод данных приведет к зависанию процесса и, как следствие, отказу сервиса. Автор лично видел и сообщал о таких уязвимостях даже для очень известных и широко используемых программ.
1212

1313
Так что проблема, несомненно, достойна рассмотрения.
1414

1515
## Вступление
1616

1717
План изложения у нас будет таким:
1818

19-
1. Сначала посмотрим на проблему в реальной ситуации.
20-
2. Потом упростим реальную ситуацию до "корней" и увидим, откуда она берётся.
21-
3. Ну и, на конец, исправим её.
19+
1. Сначала взглянем на проблему, на то, как это могло произойти.
20+
2. Потом упростим ситуацию и увидим, почему проблема возникает.
21+
3. Ну и, наконец, исправим её.
2222

2323
Например, давайте рассмотрим поиск тегов в HTML.
2424

25-
Мы хотим найти все теги с атрибутами (или без них), типа: `subject:<a href="..." class="doc" ...>`. Нужно чтобы регулярное выражение работало надёжно, так как HTML приходит из Интернета и может быть запутанным.
25+
Мы хотим найти все теги с атрибутами (или без них) типа: `subject:<a href="..." class="doc" ...>`. Нужно, чтобы регулярное выражение работало надёжно, так как HTML приходит из Интернета и может быть "грязным".
2626

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).
2828

2929
Как видим, простое регулярное выражение `pattern:<[^>]+>` не работает, потому что оно останавливает поиск на первом `>`, а нам нужно игнорировать `<>`, если они являются частью атрибута.
3030

@@ -35,11 +35,11 @@ alert( '<a test="<>" href="#">'.match(/<[^>]+>/) ); // <a test="<>
3535

3636
Для того, чтобы правильно обрабатывать подобные ситуации, нужно более сложное регулярное выражение. Оно будет иметь вид: `pattern:<tag (key=value)*>`.
3737

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:"[^"]*"`.
4141

42-
Если мы подставим это в паттерн описанный выше и добавим дополнительные пробелы `pattern:\s`, то получим следующее: `pattern:<\w+(\s*\w+="[^"]*"\s*)*>`.
42+
Если мы подставим это в паттерн, описанный выше, и добавим дополнительные пробелы `pattern:\s`, то получим следующее: `pattern:<\w+(\s*\w+="[^"]*"\s*)*>`.
4343

4444
Это регулярное выражение неидеально! Оно всё ещё не поддерживает все детали HTML, например, значения в кавычках, и, хотя и есть способы улучшить его, давайте не будем его усложнять. Оно продемонстрирует нам проблему.
4545

@@ -55,11 +55,11 @@ alert( str.match(reg) ); // <a test="<>" href="#">, <b>
5555

5656
Отлично! Нашло длинный `match:<a test="<>" href="#">` и короткий `match:<b>` теги.
5757

58-
Теперь у нас есть на вид рабочее решение. А теперь – демонстрация проблемы.
58+
Теперь у нас есть рабочее на вид решение. А теперь – демонстрация проблемы.
5959

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

62-
Если запустить пример ниже, то он может подвесить браузер (или другой JavaScript хост):
62+
Если запустить пример ниже, то он может подвесить браузер (или другую среду, где выполняется JavaScript):
6363

6464
```js run
6565
let reg = /<\w+(\s*\w+="[^"]*"\s*)*>/g;
@@ -79,10 +79,10 @@ alert( str.match(reg) );
7979

8080
Давайте упростим регулярное выражение, удалив имя тега и кавычки. Теперь мы ищем только атрибуты -- пары `key=value`: `pattern:<(\s*\w+=\w+\s*)*>`.
8181

82-
К сожалению, регулярное выражение все еще "зависает":
82+
К сожалению, регулярное выражение всё ещё "зависает":
8383

8484
```js run
85-
// поиск только по атрибутам, разделенных пробелом
85+
// поиск только по атрибутам, разделённым пробелом
8686
let reg = /<(\s*\w+=\w+\s*)*>/g;
8787

8888
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) );
9494
*/!*
9595
```
9696

97-
На этом мы закончим с демонстрацией "практического примера" и перейдём к разбору происходящего и способам устранения проблемы.
97+
На этом мы закончим с демонстрацией практического примера и перейдём к разбору происходящего и способам устранения проблемы.
9898

9999
## Подробный пример
100100

101-
Чтобы сделать пример еще проще, давайте рассмотрим `pattern:(\d+)*$`.
101+
Чтобы сделать пример ещё проще, давайте рассмотрим `pattern:(\d+)*$`.
102102

103103
Это регулярное выражение имеет ту же проблему. В большинстве движков регулярных выражений этот поиск занимает очень много времени (осторожно - может "зависнуть"):
104104

@@ -165,7 +165,7 @@ alert( '12345678901234567890123456789123456789z'.match(/(\d+)*$/) );
165165
166166
...увы, всё ещё нет соответствия для `pattern:$`.
167167
168-
Поисковой движок снова должен отступить назад. В общем бэктрекинг работает так: последний жадный квантификатор понижает количество повторений до тех пор, пока это возможно. Затем понижает предыдущий "жадный" квантификатор и т.д. В нашем случае последний "жадный" квантификатор -- это второй `pattern:\d+`, сокращающий `subject:89` до `subject:8`, а звёздочка берёт `subject:9`:
168+
Поисковый движок снова должен отступить назад. В общем, бэктрекинг работает так: последний жадный квантификатор понижает количество повторений до тех пор, пока это возможно. Затем понижает предыдущий "жадный" квантификатор и т.д. В нашем случае последний "жадный" квантификатор -- это второй `pattern:\d+`, сокращающий `subject:89` до `subject:8`, а звёздочка берёт `subject:9`:
169169
170170
```
171171
X
@@ -204,7 +204,7 @@ alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
204204

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

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

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

@@ -246,15 +246,15 @@ alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
246246
(name=value) name=value
247247
```
248248

249-
В современных регулярных выражениях для решения этой проблемы придумали "possessive" (сверхжадные? неоткатные? точный перевод пока не устоялся) квантификаторы, которые вообще не используют бэктрегинг. То есть, они даже проще, чем "жадные" – берут максимальное количество символов и всё. Поиск продолжается дальше. Также есть "атомарные скобочные группы" -- средство, запрещающее перебор внутри скобок.
249+
В современных регулярных выражениях для решения этой проблемы придумали сверхжадные ("possessive") квантификаторы, которые вообще не используют бэктрегинг. То есть, они даже проще, чем "жадные" – берут максимальное количество символов и всё. Поиск продолжается дальше. Также есть "атомарные скобочные группы" -- средство, запрещающее перебор внутри скобок.
250250

251251
К сожалению, в JavaScript они все не поддерживаются.
252252

253253
### Предпросмотр в помощь!
254254

255255
Но мы можем исключить бэктрекинг с помощью предпросмотра.
256256

257-
Паттерн, совершающий максимальное количество повторений без "отката" выглядит так: `pattern:(?=(a+))\1`.
257+
Паттерн, совершающий максимальное количество повторений без "отката", выглядит так: `pattern:(?=(a+))\1`.
258258

259259
Другими словами:
260260
- Предпросмотр `pattern:?=` ищет максимальное количество `pattern:a+`, доступных с текущей позиции.
@@ -263,7 +263,7 @@ alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
263263
Откат в этой логике в принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 `pattern:a+`, и в результате поиск не удался, то он не будет откатываться на 4 повторения.
264264

265265
```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).
267267
```
268268

269269
Такой метод нивелирует проблему.
@@ -274,7 +274,7 @@ alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
274274
// регулярное выражение для поиска 'name=value'
275275
let attrReg = /(\s*\w+=(\w+|"[^"]*")\s*)/
276276

277-
// используем new RegExp() чтобы красиво вставить его source в (?=(a+))\1
277+
// используем new RegExp() чтобы красиво вставить его исходную строку (source) в (?=(a+))\1
278278
let fixedReg = new RegExp(`<\\w+(?=(${attrReg.source}*))\\1>`, 'g');
279279

280280
let goodInput = '...<a test="<>" href="#">... <b>...';
@@ -283,9 +283,9 @@ let badInput = `<tag a=b a=b a=b a=b a=b a=b a=b a=b
283283
a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b`;
284284

285285
alert( goodInput.match(fixedReg) ); // <a test="<>" href="#">, <b>
286-
alert( badInput.match(fixedReg) ); // null (нет резульатов, быстро!)
286+
alert( badInput.match(fixedReg) ); // null (нет резульатов, отработало быстро!)
287287
```
288288

289289
Отлично, всё работает! Нашло как длинный тег `match:<a test="<>" href="#">`, так и одинокий `match:<b>`, и (!) не "вешает" интерпретатор при некорректных данных.
290290

291-
Обратите внимание на свойство `attrReg.source`. Объект `RegExp` предоставляет доступ к своей (?)исходной(?) строке. Это удобно, когда мы хотим вставить одно регулярное выражение в другое.
291+
Обратите внимание на свойство `attrReg.source`. Объект `RegExp` предоставляет доступ к своей исходной (`source`) строке. Это удобно, когда мы хотим вставить одно регулярное выражение в другое.

0 commit comments

Comments
 (0)