Some regular expressions are looking simple, but can execute a veeeeeery long time, and even âhangâ the JavaScript engine.
Sooner or later most developers occasionally face such behavior. The typical symptom â a regular expression works fine sometimes, but for certain strings it âhangsâ, consuming 100% of CPU.
ÙÙ Ù Ø«Ù ÙØ°Ù Ø§ÙØØ§ÙØ© Ø ÙÙØªØ±Ø Ù ØªØµÙØ اÙÙÙØ¨ Ø¥ÙÙØ§Ø¡ Ø§ÙØ¨Ø±Ùا٠ج اÙÙØµÙ ÙØ¥Ø¹Ø§Ø¯Ø© تØÙ ÙÙ Ø§ÙØµÙØØ©. ÙÙØ³ Ø¨Ø§ÙØ´ÙØ¡ Ø§ÙØ¬Ùد Ø¨Ø§ÙØªØ£ÙÙØ¯
For server-side JavaScript such a regexp may hang the server process, thatâs even worse. So we definitely should take a look at it.
٠ثاÙ
Letâs say we have a string, and weâd like to check if it consists of words \w+ with an optional space \s? after each.
An obvious way to construct a regexp would be to take a word followed by an optional space \w+\s? and then repeat it with *.
That leads us to the regexp ^(\w+\s?)*$, it specifies zero or more such words, that start at the beginning ^ and finish at the end $ of the line.
ÙÙ Ø§ÙØ¹Ù Ù:
let regexp = /^(\w+\s?)*$/;
alert( regexp.test("A good string") ); // true
alert( regexp.test("Bad characters: $@#") ); // false
The regexp seems to work. The result is correct. Although, on certain strings it takes a lot of time. So long that JavaScript engine âhangsâ with 100% CPU consumption.
If you run the example below, you probably wonât see anything, as JavaScript will just âhangâ. A web-browser will stop reacting on events, the UI will stop working (most browsers allow only scrolling). After some time it will suggest to reload the page. So be careful with this:
let regexp = /^(\w+\s?)*$/;
let str = "An input string that takes a long time or even makes this regexp hang!";
// will take a very long time
alert( regexp.test(str) );
To be fair, letâs note that some regular expression engines can handle such a search effectively, for example V8 engine version starting from 8.8 can do that (so Google Chrome 88 doesnât hang here), while Firefox browser does hang.
٠ثا٠٠بسط
Whatâs the matter? Why does the regular expression hang?
ÙÙÙÙ
ذÙÙ Ø Ø¯Ø¹ÙØ§ ÙØ¨Ø³Ø· اÙÙ
ثاÙ: Ø¥Ø²Ø§ÙØ© اÙÙ
Ø³Ø§ÙØ§Øª \ sØ. Ø«Ù
ÙØµØ¨Ø اÙÙÙ
Ø·: ^ (\ w +) * $.
ÙÙØ¬Ø¹Ù Ø§ÙØ£Ù
ÙØ± Ø£ÙØ«Ø± ÙØ¶ÙØÙا Ø Ø¯Ø¹ÙØ§ ÙØ³ØªØ¨Ø¯Ù \ w ب٠\ d. Ù
Ø§Ø²Ø§Ù Ø§ÙØªØ¹Ø¨Ùر Ø§ÙØ¹Ø§Ø¯Ù اÙÙØ§ØªØ¬ Ù
عÙÙÙØ§ Ø Ø¹Ù٠سبÙ٠اÙÙ
ثاÙ:
let regexp = /^(\d+)*$/;
let str = "012345678901234567890123456789z";
// will take a very long time (careful!)
alert( regexp.test(str) );
إذ٠٠ا ÙÙ Ø§ÙØ®Ø·Ø£ ÙÙ regexpØ
Ø£ÙÙØ§Ù Ø ÙØ¯ ÙÙØ§ØØ¸ اÙÙ
رء Ø£Ù ÙÙ
Ø· regexp : (\ d +) * ØºØ±ÙØ¨ بعض Ø§ÙØ´ÙØ¡. ÙØ¨Ø¯Ù "ÙÙ
Ø· Ù
ØØ¯Ø¯ اÙÙÙ
ÙØ©: *ØºØ±ÙØ¨Ùا. إذا Ø£Ø±Ø¯ÙØ§ رÙÙ
ÙØ§ Ø ÙÙÙ
ÙÙÙØ§ استخداÙ
pattern: \ d +`.
Indeed, the regexp is artificial; we got it by simplifying the previous example. But the reason why it is slow is the same. So letâs understand it, and then the previous example will become obvious.
What happens during the search of ^(\d+)*$ in the line 123456789z (shortened a bit for clarity, please note a non-digit character z at the end, itâs important), why does it take so long?
Hereâs what the regexp engine does:
- First, the regexp engine tries to find the content of the parentheses: the number
\d+. The plus+is greedy by default, so it consumes all digits:
\ d + ....... Â Â Â Â (123456789) z
After all digits are consumed, `pattern:\d+` is considered found (as `match:123456789`).
Then the star quantifier `pattern:(\d+)*` applies. But there are no more digits in the text, so the star doesn't give anything.
The next character in the pattern is the string end `pattern:$`. But in the text we have `subject:z` instead, so there's no match:
```
X
\d+........$
(123456789)z
```
-
ÙØ¸Ø±Ùا ÙØ¹Ø¯Ù ÙØ¬Ùد ØªØ·Ø§Ø¨Ù Ø ÙØ¥Ù ÙÙ Ø· اÙÙ ØØ¯Ø¯ اÙÙÙ Ù Ø§ÙØ¬Ø´Ø¹
: +ÙÙÙ٠٠٠عدد Ø§ÙØªÙرار Ø ÙÙØ¹Ùد ØØ±ÙÙØ§ ÙØ§ØØ¯Ùا Ø¥Ù٠اÙÙØ±Ø§Ø¡.Now
\d+takes all digits except the last one (12345678):\d+....... (12345678)9z -
Then the engine tries to continue the search from the next position (right after
12345678).The star
(\d+)*can be applied â it gives one more match of\d+, the number9:
``
\d+.......\d+
(12345678)(9)z
```
The engine tries to match `pattern:$` again, but fails, because it meets `subject:z` instead:
X Â Â Â Â \ d + ....... \ d + Â Â Â Â (12345678) (9) z
- Thereâs no match, so the engine will continue backtracking, decreasing the number of repetitions. Backtracking generally works like this: the last greedy quantifier decreases the number of repetitions until it reaches the minimum. Then the previous greedy quantifier decreases, and so on.
ØªØªÙ Ù ØØ§ÙÙØ© Ø¬Ù ÙØ¹ Ø§ÙØªØ±ÙÙØ¨Ø§Øª اÙÙ Ù ÙÙØ©. ÙÙØ§ Ø£Ù Ø«ÙØ©ÙÙ .
ÙØªÙÙÙ Ø§ÙØ±ÙÙ
Ø§ÙØ£ÙÙ Ù
٠اÙÙÙ
Ø·: \ d + Ù
Ù 7 Ø£Ø±ÙØ§Ù
Ø Ø«Ù
عدد Ù
٠رÙÙ
ÙÙ:
```
X
\d+......\d+
(1234567)(89)z
```
ÙØªÙÙÙ Ø§ÙØ±ÙÙ Ø§ÙØ£ÙÙ Ù Ù 7 Ø£Ø±ÙØ§Ù Ø Ø«Ù Ø±Ù٠ا٠٠٠رÙÙ ÙØ§ØØ¯ ÙÙÙ Ù ÙÙ٠ا:
```
X
\d+......\d+\d+
(1234567)(8)(9)z
```
ÙØªÙÙÙ Ø§ÙØ±ÙÙ Ø§ÙØ£ÙÙ Ù Ù 6 Ø£Ø±ÙØ§Ù Ø Ø«Ù Ø¹Ø¯Ø¯ 3 Ø£Ø±ÙØ§Ù :
```
X
\d+.......\d+
(123456)(789)z
```
ÙØªÙÙÙ Ø§ÙØ±ÙÙ Ø§ÙØ£ÙÙ Ù Ù 6 Ø£Ø±ÙØ§Ù Ø Ø«Ù Ø±Ù٠اÙ:
```
X
\d+.....\d+ \d+
(123456)(78)(9)z
```
â¦Ù٠ا Ø¥Ù٠ذÙÙ ÙÙÙ٠جرا.
ÙÙØ§Ù عدة Ø·Ø±Ù ÙØªÙسÙÙ
Ù
جÙ
ÙØ¹Ø© Ù
Ù Ø§ÙØ£Ø±ÙاÙ
123456789 Ø¥ÙÙ Ø£Ø±ÙØ§Ù
. عÙÙ ÙØ¬Ù Ø§ÙØ¯ÙØ© Ø ÙÙØ§Ù 2 n ‑1 Ø ØÙØ« n ÙÙ Ø·Ù٠اÙÙ
جÙ
ÙØ¹Ø©.
There are many ways to split a sequence of digits 123456789 into numbers. To be precise, there are 2n-1, where n is the length of the sequence.
- For
123456789we haven=9, that gives 511 combinations. - For a longer sequence with
n=20there are about one million (1048575) combinations. - For
n=30â a thousand times more (1073741823 combinations).
Trying each of them is exactly the reason why the search takes so long.
ÙØØ¯Ø« Ø§ÙØ´ÙØ¡ ÙÙØ³Ù ÙÙ Ù
ثاÙÙØ§ Ø§ÙØ£ÙÙ Ø Ø¹ÙØ¯Ù
ا ÙÙØ¸Ø± Ø¥Ù٠اÙÙÙÙ
ات ØØ³Ø¨ اÙÙÙ
Ø· ^ (\ w + \ sØ) * $ ÙÙ Ø§ÙØ³ÙØ³ÙØ© Ù
دخ٠Ù
عÙÙ!.
The similar thing happens in our first example, when we look for words by pattern ^(\w+\s?)*$ in the string An input that hangs!.
The reason is that a word can be represented as one \w+ or many:
(input)
(inpu)(t)
(inp)(u)(t)
(in)(p)(ut)
...
باÙÙØ³Ø¨Ø© ÙÙØ¥ÙØ³Ø§Ù Ø Ù
٠اÙÙØ§Ø¶Ø Ø£ÙÙ ÙØ¯ ÙØ§ ÙÙÙÙ ÙÙØ§Ù ØªØ·Ø§Ø¨Ù Ø ÙØ£Ù Ø§ÙØ³ÙØ³ÙØ© ØªÙØªÙÙ Ø¨Ø¹ÙØ§Ù
Ø© تعجب ! Ø ÙÙÙ Ø§ÙØªØ¹Ø¨Ùر Ø§ÙØ¹Ø§Ø¯Ù ÙØªÙÙØ¹ ØØ±ÙÙØ§ ÙÙÙ
Ø© ÙÙ
Ø·: \ w Ø£Ù ÙÙ
Ø·Ù
Ø³Ø§ÙØ©: \ s Ù٠اÙÙÙØ§ÙØ©. ÙÙ٠اÙÙ
ØØ±Ù ÙØ§ ÙØ¹Ø±Ù ذÙÙ.
It tries all combinations of how the regexp (\w+\s?)* can âconsumeâ the string, including variants with spaces (\w+\s)* and without them (\w+)* (because spaces \s? are optional). As there are many such combinations (weâve seen it with digits), the search takes a lot of time.
What to do?
Should we turn on the lazy mode?
Unfortunately, that wonât help: if we replace \w+ with \w+?, the regexp will still hang. The order of combinations will change, but not their total count.
Some regular expression engines have tricky tests and finite automations that allow to avoid going through all combinations or make it much faster, but most engines donât, and it doesnât always help.
ÙÙÙÙØ© Ø§ÙØ¥ØµÙØ§ØØ
ÙÙØ§Ù طرÙÙØªØ§Ù Ø±Ø¦ÙØ³ÙØªØ§Ù ÙØ¥ØµÙØ§Ø Ø§ÙÙ Ø´ÙÙØ©.
Ø§ÙØ£ÙÙ Ù٠تÙÙÙ٠عدد Ø§ÙØªØ±ÙÙØ¨Ø§Øª اÙÙ Ù ÙÙØ©.
Letâs make the space non-optional by rewriting the regular expression as ^(\w+\s)*\w*$ â weâll look for any number of words followed by a space (\w+\s)*, and then (optionally) a final word \w*.
تعاد٠regexp ÙØ°Ù Ø§ÙØ³Ø§Ø¨ÙØ© (تتطاب٠٠ع ÙÙØ³Ùا) ÙØªØ¹Ù ٠بشÙÙ Ø¬ÙØ¯:
let regexp = /^(\w+\s)*\w*$/;
let str = "An input string that takes a long time or even makes this regex hang!";
alert( regexp.test(str) ); // false
Ù٠اذا Ø§Ø®ØªÙØª اÙÙ Ø´ÙÙØ©Ø
Thatâs because now the space is mandatory.
The previous regexp, if we omit the space, becomes (\w+)*, leading to many combinations of \w+ within a single word
So input could be matched as two repetitions of \w+, like this:
\w+ \w+
(inp)(ut)
The new pattern is different: (\w+\s)* specifies repetitions of words followed by a space! The input string canât be matched as two repetitions of \w+\s, because the space is mandatory.
The time needed to try a lot of (actually most of) combinations is now saved.
Ù ÙØ¹ Ø§ÙØªØ±Ø§Ø¬Ø¹
Itâs not always convenient to rewrite a regexp though. In the example above it was easy, but itâs not always obvious how to do it.
Besides, a rewritten regexp is usually more complex, and thatâs not good. Regexps are complex enough without extra efforts.
Luckily, thereâs an alternative approach. We can forbid backtracking for the quantifier.
The root of the problem is that the regexp engine tries many combinations that are obviously wrong for a human.
عÙ٠سبÙ٠اÙÙ
ثا٠ÙÙ ÙÙ
Ø· regexp : (\ d +) * $ Ù
٠اÙÙØ§Ø¶Ø ÙÙØ¥ÙØ³Ø§Ù Ø Ø£Ù Ø§ÙÙÙ
Ø·: + ÙØ§ ÙØ¬Ø¨ Ø§ÙØªØ±Ø§Ø¬Ø¹ عÙÙ. إذا استبدÙÙØ§ ÙÙ
Ø·ÙØ§ `` ÙØ§ØØ¯Ùا: \ d + بÙÙ
Ø·ÙÙ Ù
ÙÙØµÙÙÙ: \ d + \ d + `Ø ÙÙÙ ÙØªØºÙر Ø´ÙØ¡:
`` \d+â¦â¦â¦ (123456789)!
\d+â¦\d+â¦. (1234)(56789)!
And in the original example `pattern:^(\w+\s?)*$` we may want to forbid backtracking in `pattern:\w+`. That is: `pattern:\w+` should match a whole word, with the maximal possible length. There's no need to lower the repetitions count in `pattern:\w+` or to split it into two words `pattern:\w+\w+` and so on.
Modern regular expression engines support possessive quantifiers for that. Regular quantifiers become possessive if we add `pattern:+` after them. That is, we use `pattern:\d++` instead of `pattern:\d+` to stop `pattern:+` from backtracking.
Possessive quantifiers are in fact simpler than "regular" ones. They just match as many as they can, without any backtracking. The search process without bracktracking is simpler.
ÙÙØ§Ù Ø£ÙØ¶Ùا Ù
ا ÙØ³Ù
Ù "Ù
جÙ
ÙØ¹Ø§Øª Ø§ÙØ§ÙØªÙØ§Ø· Ø§ÙØ°Ø±Ù" - ÙÙ٠طرÙÙØ© ÙØªØ¹Ø·ÙÙ Ø§ÙØªØ±Ø§Ø¬Ø¹ Ø¯Ø§Ø®Ù Ø§ÙØ£ÙÙØ§Ø³.
...But the bad news is that, unfortunately, in JavaScript they are not supported.
We can emulate them though using a "lookahead transform".
### Ø§ÙØ¸Ø±Ùا Ø¥ÙÙ Ø§ÙØ¥ÙÙØ§Ø°!
So we've come to real advanced topics. We'd like a quantifier, such as `pattern:+` not to backtrack, because sometimes backtracking makes no sense.
The pattern to take as many repetitions of `pattern:\w` as possible without backtracking is: `pattern:(?=(\w+))\1`. Of course, we could take another pattern instead of `pattern:\w`.
That may seem odd, but it's actually a very simple transform.
Let's decipher it:
- Lookahead `pattern:?=` looks forward for the longest word `pattern:\w+` starting at the current position.
- The contents of parentheses with `pattern:?=...` isn't memorized by the engine, so wrap `pattern:\w+` into parentheses. Then the engine will memorize their contents
- ...And allow us to reference it in the pattern as `pattern:\1`.
ÙØ°Ø§ ÙÙ: ÙØÙ ÙØªØ·Ùع Ø¥Ù٠اÙÙ
Ø³ØªÙØ¨Ù - ÙØ¥Ø°Ø§ ÙØ§Ùت ÙÙØ§Ù ÙÙÙ
Ø© `pattern: \ w +` Ø ÙÙÙ
بÙ
Ø·Ø§Ø¨ÙØªÙا ÙÙ `pattern: \ 1`.
ÙÙ
اذا Ø§Ø Ø°ÙÙ ÙØ£Ù lookahead ÙØ¹Ø«Ø± عÙÙ ÙÙÙ
Ø© `pattern: \ w +` ÙÙÙ ÙÙÙØªÙØ·ÙØ§ Ù٠اÙÙÙ
Ø· ب٠`pattern: \ 1`. ÙØ°Ø§ ÙÙ
ÙØ§ بتطبÙÙ ÙÙ
Ø· اÙ
ØªÙØ§Ù زائد `ÙÙ
Ø·: +` ÙÙ
ÙÙ. ÙÙØªÙØ· ÙÙØ· اÙÙÙÙ
Ø© `ÙÙ
Ø·: \ w +` Ø ÙÙÙØ³ Ø¬Ø²Ø¡ÙØ§ Ù
ÙÙØ§.
عÙ٠سبÙ٠اÙÙ
Ø«Ø§Ù Ø ÙÙ ÙÙÙ
Ø© `subject: JavaScript` Ø ÙØ¯ ÙØ§ تتطاب٠ÙÙØ· Ù
ع` match: Java` Ø ÙÙÙ٠تتر٠`match: Script` ÙØªØªØ·Ø§Ø¨Ù Ù
ع باÙ٠اÙÙÙ
Ø·.
Ø¥ÙÙÙ Ù
ÙØ§Ø±ÙØ© بÙÙ ÙÙ
Ø·ÙÙ:
```js run
alert( "JavaScript".match(/\w+Script/)); // JavaScript
alert( "JavaScript".match(/(?=(\w+))\1Script/)); // null
- Ù٠اÙÙ
ØªØºÙØ± Ø§ÙØ£ÙÙ
اÙÙÙ Ø·: \ w +ÙÙØªÙØ· Ø£ÙÙØ§Ù اÙÙÙ٠ةاÙÙ ÙØ¶Ùع: Ø¬Ø§ÙØ§ Ø³ÙØ±ÙØ¨ØªØ«ÙØ§ÙÙÙ Ø·: +ÙØªØ±Ø§Ø¬Ø¹ ØØ±ÙÙØ§ Ø¨ØØ±Ù Ø ÙÙ Ù ØØ§ÙÙØ© ÙÙ Ø·Ø§Ø¨ÙØ© بÙÙØ© اÙÙÙ Ø· Ø ØØªÙ ÙÙØ¬Ø Ù٠اÙÙÙØ§ÙØ© (Ø¹ÙØ¯Ù ا\ w +ÙØªØ·Ø§Ø¨Ù ٠عJava). - Ù٠اÙÙ
ØªØºÙØ± Ø§ÙØ«Ø§ÙÙ
اÙÙÙ Ø·: (Ø = (\ w +))ÙØªØ·Ùع Ø¥ÙÙ Ø§ÙØ£Ù ا٠ÙÙØ¬Ø¯ ÙÙ٠ةاÙÙ ÙØ¶Ùع: JavaScriptØ Ø§ÙÙ Ø¶Ù ÙØ© Ù٠اÙÙÙ Ø· ÙÙÙ Ø¨ÙØ§Ø³Ø·Ø©Ø§ÙÙÙ Ø·: \ 1Ø ÙØ°ÙÙ ÙØ§ ÙØ²Ø§Ù ÙÙØ§Ù ÙØ§ ØªÙØ¬Ø¯ طرÙÙØ© ÙÙØ¹Ø«Ùر عÙÙ âØ§ÙÙ ÙØ¶Ùع: Ø§ÙØ¨Ø±Ùا٠ج اÙÙØµÙâ Ø¨Ø¹Ø¯Ù.
ÙÙ
ÙÙÙØ§ ÙØ¶Ø¹ ØªØ¹Ø¨ÙØ± Ø¹Ø§Ø¯Ù Ø£ÙØ«Ø± تعÙÙØ¯Ùا Ù٠اÙÙÙ
Ø·: (Ø = (\ w +)) \ 1 Ø¨Ø¯ÙØ§Ù Ù
ÙØ§ÙÙÙ
Ø·: \ w Ø Ø¹ÙØ¯Ù
ا ÙØØªØ§Ø¬ Ø¥ÙÙ Ù
ÙØ¹ Ø§ÙØªØ±Ø§Ø¬Ø¹ ع٠اÙÙÙ
Ø·: + بعدÙ.
ÙÙØ§Ù اÙÙ Ø²ÙØ¯ ØÙÙ Ø§ÙØ¹ÙØ§ÙØ© بÙÙ Ù ØØ¯Ø¯Ø§Øª اÙÙÙ ÙØ© Ø§ÙØªÙ ÙÙÙØ© Ù lookahead Ù٠اÙÙ ÙØ§Ùات [Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead] (http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead ) Ù [Ù ØØ§Ùاة اÙÙ Ø¬Ù ÙØ¹Ø§Øª Ø§ÙØ°Ø±ÙØ©] (http://blog.stevenlevithan.com/archives/mimic-atomic-groups). ``
دعÙÙØ§ ÙØ¹Ùد ÙØªØ§Ø¨Ø© اÙÙ Ø«Ø§Ù Ø§ÙØ£Ù٠باستخدا٠lookahead ÙÙ ÙØ¹ Ø§ÙØªØ±Ø§Ø¬Ø¹:
let regexp = /^((?=(\w+))\2\s?)*$/;
alert( regexp.test("A good string") ); // true
let str = "An input string that takes a long time or even makes this regex hang!";
alert( regexp.test(str) ); // false, works and fast!
ÙÙØ§ ÙÙØ³ØªØ®Ø¯Ù
âØ§ÙÙÙ
Ø·: \ 2â Ø¨Ø¯Ùا٠Ù
Ù âØ§ÙÙÙ
Ø·: \ 1â Ø ÙØ¸Ø±Ùا ÙÙØ¬Ùد Ø£ÙÙØ§Ø³ Ø®Ø§Ø±Ø¬ÙØ© إضاÙÙØ©. ÙØªØ¬Ùب Ø§ÙØ¹Ø¨Ø« Ø¨Ø§ÙØ£Ø±ÙاÙ
Ø ÙÙ
ÙÙÙØ§ تسÙ
ÙØ© Ø§ÙØ£ÙÙØ§Ø³ Ø Ø¹Ù٠سبÙ٠اÙÙ
ثا٠ÙÙ
Ø· :(Ø <word> \ w +).
// parentheses are named ?<word>, referenced as \k<word>
let regexp = /^((?=(?<word>\w+))\k<word>\s?)*$/;
let str = "An input string that takes a long time or even makes this regex hang!";
alert( regexp.test(str) ); // false
alert( regexp.test("A correct string") ); // true
تس٠٠اÙÙ Ø´ÙÙØ© اÙÙ ÙØ¶ØØ© ÙÙ ÙØ°Ù اÙÙ ÙØ§ÙØ© âØ§ÙØªØ±Ø§Ø¬Ø¹ اÙÙØ§Ø±Ø«Ùâ.
ØªÙØ§ÙÙÙØ§ طرÙÙØªÙÙ ÙÙÙÙÙØ© ØÙÙØ§:
- أعد ÙØªØ§Ø¨Ø© regexp ÙØ®Ùض عدد اÙÙ Ø¬Ù ÙØ¹Ø§Øª اÙÙ Ù ÙÙØ©.
- Ù ÙØ¹ Ø§ÙØªØ±Ø§Ø¬Ø¹.
Ø§ÙØªØ¹ÙÙÙØ§Øª
<code>Ø ÙÙÙÙØ«Ùر Ù Ù Ø§ÙØ³Ø·Ùر استخدÙ<pre>Ø ÙÙØ£Ùثر Ù Ù 10 Ø³Ø·ÙØ± استخد٠(plnkr, JSBin, codepenâ¦)