renovate regexps
|
@ -5,18 +5,22 @@
|
|||
На скобочные группы можно ссылаться как в самом паттерне, так и в строке замены.
|
||||
[cut]
|
||||
|
||||
## Группа в замене
|
||||
## Группа в строке замены
|
||||
|
||||
Ссылки в строке замены имеют вид `$n`, где `n` -- это номер скобочной группы. Вместо `$n` подставляется содержимое соответствующей скобки:
|
||||
Ссылки в строке замены имеют вид `$n`, где `n` -- это номер скобочной группы.
|
||||
|
||||
Вместо `$n` подставляется содержимое соответствующей скобки:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var name = "Александр Пушкин";
|
||||
|
||||
name = name.replace(/([а-яё]+) ([а-яё]+)/i, "$2, $1");
|
||||
name = name.replace(/([а-яё]+) ([а-яё]+)/i, *!*"$2, $1"*/!*);
|
||||
alert( name ); // Пушкин, Александр
|
||||
```
|
||||
|
||||
В примере выше вместо <code class="pattern">$2</code> подставляется второе найденное слово, а вместо <code class="pattern">$1</code> -- первое.
|
||||
|
||||
## Группа в шаблоне
|
||||
|
||||
Выше был пример использования содержимого групп в строке замены. Это удобно, когда нужно реорганизовать содержимое или создать новое с использованием старого.
|
||||
|
@ -27,11 +31,11 @@ alert( name ); // Пушкин, Александр
|
|||
|
||||
Как такие строки искать?
|
||||
|
||||
Можно в регэкспе предусмотреть произвольные кавычки: <code class="pattern">`['"](.*?)['"]`</code>. Такой регэксп найдёт строки вида <code class="match">"..."</code>, <code class="match">'...'</code>, но он даст неверный ответ в случае, если одна кавычка ненароком оказалась внутри другой, как например в строке <code class="subject">"She's the one"</code>:
|
||||
Можно в регэкспе предусмотреть произвольные кавычки: <code class="pattern">`['"](.*?)['"]`</code>. Такой регэксп найдёт строки вида <code class="match">"..."</code>, <code class="match">'...'</code>, но он даст неверный ответ в случае, если одна кавычка ненароком оказалась внутри другой, как например в строке <code class="subject">"She's the one!"</code>:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
str = "He said:\"She's the one\".";
|
||||
str = "He said: \"She's the one!\".";
|
||||
|
||||
reg = /['"](.*?)['"]/g;
|
||||
|
||||
|
@ -45,14 +49,14 @@ alert( str.match(reg) ); // "She'
|
|||
|
||||
```js
|
||||
//+ run
|
||||
str = "He said:\"She's the one\".";
|
||||
str = "He said: \"She's the one!\".";
|
||||
|
||||
reg = /(['"])(.*?)\1/g;
|
||||
|
||||
alert( str.match(reg) ); // "She's the one"
|
||||
alert( str.match(reg) ); // "She's the one!"
|
||||
```
|
||||
|
||||
Теперь работает верно!
|
||||
Теперь работает верно! Движок регулярных выражений, найдя первое скобочное выражение -- кавычку <code class="pattern">(['"])</code>, запоминает его и далее <code class="pattern">\1</code> означает "найти то же самое, что в первой скобочной группе".
|
||||
|
||||
Обратим внимание на два нюанса:
|
||||
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
# Чёрная дыра бэктрекинга [todo]
|
||||
|
||||
Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже подвешивать браузер.
|
||||
|
||||
[cut]
|
||||
Например, попробуйте пример ниже в Chrome или IE (осторожно, подвесит браузер!):
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( '123456789012345678901234567890z'.match(/(\d+)*$/) );
|
||||
```
|
||||
|
||||
Некоторые движки регулярных выражений (Firefox) справляются с таким регэкспом, а некоторые (IE, Chrome) -- нет.
|
||||
|
||||
В чём же дело, что не так с регэкспом?
|
||||
|
||||
Да с регэкспом-то всё так, синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему.
|
||||
|
||||
Для краткости рассмотрим более короткую строку: <code class="subject">1234567890z</code>:
|
||||
|
||||
<ol>
|
||||
<li>Первым делом, движок регэкспов пытается найти <code class="pattern">\d+</code>. Плюс <code class="pattern">+</code> является жадным по умолчанию, так что он хватает все цифры, какие может.
|
||||
|
||||
Затем движок пытается применить звёздочку вокруг скобок <code class="pattern">(\d+)*</code>, но больше цифр нет, так что звёздочка не даёт повторений.
|
||||
|
||||
После этого в паттерне остаётся <code class="pattern">$</code>, а в тексте -- символ <code class="subject">z</code>.
|
||||
|
||||
<img src="bad_backtrack_greedy1.png">
|
||||
|
||||
Так как соответствия нет, то жадный плюс <code class="pattern">+</code> отступает на один символ (бэктрекинг, зелёная стрелка на рисунке выше).
|
||||
</li>
|
||||
<li>После бэктрекинга, <code class="pattern">\d+</code> содержит всё число, кроме последней цифры. Затем движок снова пытается найти совпадение, уже с новой позиции (`9`).
|
||||
|
||||
Звёздочка <code class="pattern">(\d+)*</code> теперь может быть применена -- она даёт ещё одно число <code class="match">9</code>:
|
||||
|
||||
<img src="bad_backtrack_greedy11.png">
|
||||
|
||||
Движок пытается найти `$`, но это ему не удаётся -- на его пути опять `z`:
|
||||
|
||||
<img src="bad_backtrack_greedy2.png">
|
||||
|
||||
Так как совпадения нет, то поисковой движок отступает назад ещё раз.
|
||||
</li>
|
||||
<li>Теперь первое число <code class="pattern">\d+</code> будет содержать 8 цифр, а остаток строки <code class="subject">90</code> становится вторым <code class="pattern">\d+</code>:
|
||||
|
||||
<img src="bad_backtrack_greedy3.png">
|
||||
|
||||
Увы, всё ещё нет соответствия для <code class="pattern">$</code>.
|
||||
|
||||
Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй <code class="pattern">\d+</code>, до одного символа <code class="subject">9</code>.
|
||||
</li>
|
||||
<li>Теперь движок регулярных выражений снова может применить звёздочку и находит третье число <code class="pattern">\d+</code>:
|
||||
|
||||
<img src="bad_backtrack_greedy4.png">
|
||||
|
||||
...И снова неудача. Второе и третье <code class="pattern">\d+</code> отступили по-максимуму, так что сокращается снова первое число.
|
||||
</li>
|
||||
<li>Теперь есть 7 цифр в первом <code class="pattern">\d+</code>. Поисковой движок видит место для второго <code class="pattern">\d+</code>, теперь уже с позиции 8:
|
||||
|
||||
<img src="bad_backtrack_greedy5.png">
|
||||
|
||||
Так как совпадения нет, второй <code class="pattern">\d+</code> отступает назад....
|
||||
</li>
|
||||
<li>...И так далее, легко видеть, что поисковой движок будет перебирать *все возможные комбинации* <code class="pattern">\d+</code> в числе. А их много.</li>
|
||||
</ol>
|
||||
|
||||
На этом месте умный читатель может воскликнуть: "Бэктрекинг? Давайте включим ленивый режим -- и не будет никакого бэктрекинга!"
|
||||
|
||||
Что ж, заменим <code class="pattern">\d+</code> на <code class="pattern">\d+?</code> и посмотрим (аккуратно, может подвесить браузер):
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( '123456789012345678901234567890z'.match(/(\d+?)*$/) );
|
||||
```
|
||||
|
||||
Не помогло!
|
||||
|
||||
**Ленивые регулярные выражения делают то же самое, но в обратном порядке.**
|
||||
|
||||
Просто подумайте о том, как будет в этом случае работать поисковой движок.
|
||||
|
||||
Некоторые движки регулярных выражений, например Firefox, содержат хитрые проверки, в дополнение к алгоритму выше, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда.
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
Например, нам нужно найти языки программирования: HTML, PHP, Java и JavaScript.
|
||||
|
||||
Соответствующее регулярное выражение: <code class="pattern">/html|php/java(script)?/</code>:
|
||||
Соответствующее регулярное выражение: <code class="pattern">html|php|java(script)?</code>.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -14,13 +16,11 @@ var reg = /html|php|css|java(script)?/gi
|
|||
|
||||
var str = "Сначала появился HTML, затем CSS, потом JavaScript"
|
||||
|
||||
alert(str.match(reg)) // 'HTML', 'CSS', 'JavaScript'
|
||||
alert( str.match(reg) ) // 'HTML', 'CSS', 'JavaScript'
|
||||
```
|
||||
|
||||
**Альтернация имеет очень низкий приоритет.**
|
||||
|
||||
Чтобы регэксп находил одновременно <code class="match">gr<b>a</b>y</code> и <code class="match">gr<b>e</b>y</code>, можно использовать <code class="pattern">gr(a|e)y</code> или <code class="pattern">gr[ae]y</code>, но не <code class="pattern">gra|ey</code>. Последний регэксп применит альтернацию к подвыражениям: <code class="pattern">gra</code> (ИЛИ) <code class="pattern">ey</code>.
|
||||
|
||||
|
||||
Мы уже знаем похожую вещь -- квадратные скобки. Они позволяют выбирать между символами, например <code class="pattern">gr[ae]y</code> найдёт <code class="match">gray</code>, либо <code class="match">grey</code>.
|
||||
|
||||
Альтернация работает уже не посимвольно, а на уровне фраз и подвыражений. Регэксп <code class="pattern">A|B|C</code> обозначает поиск одного из выражений: `A`, `B` или `C`, причём в качестве выражений могут быть другие, сколь угодно сложные регэкспы.
|
||||
|
||||
Для указания границ альтернации используют скобки `(...)`, например: <code class="pattern">before(XXX|YYY)after</code> будет искать <code class="match">beforeXXXafter</code> или <code class="match">beforeYYYafter</code>.
|
|
@ -5,43 +5,45 @@
|
|||
|
||||
Каретка <code class="pattern">^</code> совпадает в начале текста, а доллар <code class="pattern">$</code> -- в конце.
|
||||
|
||||
**Якоря являются не символами, а проверками. Они совпадают на "позиции".**
|
||||
**Якоря являются не символами, а проверками.**
|
||||
|
||||
Это очень важное отличие по сравнению с символьными классами. Если движок регулярных выражений видит <code class="pattern">^</code> -- он *проверяет* начало текста, <code class="pattern">$</code> -- проверяет конец текста, при этом *никаких символов к совпадению не добавляется*.
|
||||
До этого мы говорили о регулярных выражениях, которые ищут один или несколько символов. Если совпадение есть -- эти символы включаются в результат.
|
||||
|
||||
А якоря -- не такие. Когда поиск ходит до якоря -- он проверяет, есть ли соответствие, если есть -- продолжает идти по шаблону, не прибавляя ничего к результату.
|
||||
|
||||
Каретку <code class="pattern">^</code> обычно используют, чтобы указать, что регулярное выражение необходимо проверить именно с начала текста.
|
||||
|
||||
Например, без каретки:
|
||||
Например, без каретки найдёт все числа:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '100500 попугаев съели 500100 бананов!';
|
||||
alert(str.match(/\d+/ig) // 100500, 500100 (все числа)
|
||||
alert( str.match(/\d+/ig) ); // 100500, 500100 (нашло все числа)
|
||||
```
|
||||
|
||||
А с кареткой:
|
||||
А с кареткой -- только первое:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '100500 попугаев съели 500100 бананов!';
|
||||
alert(str.match(/^\d+/ig) // 100500 (только в начале строки)*!*
|
||||
alert( str.match(/^\d+/ig) ); // 100500 (только в начале строки)*!*
|
||||
```
|
||||
|
||||
Знак доллара <code class="pattern">$</code> используют, чтобы указать, что паттерн должен заканчиваться в конце текста.
|
||||
|
||||
Тот же пример с долларом:
|
||||
Аналогичный пример с долларом для поиска числа в конце:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '100500 попугаев съели 500100 бананов!';
|
||||
alert(str.match(/\d+$/ig) // null (в начале строки чисел нет)*!*
|
||||
var str = '100500 попугаев съели 500100';
|
||||
alert( str.match(/\d+$/ig) ); // 500100
|
||||
```
|
||||
|
||||
Якоря используют одновременно, чтобы указать, что паттерн должен охватывать текст с начала и до конца. Обычно это требуется при валидации.
|
||||
Оба якоря используют одновременно, если требуется, чтобы шаблон охватывал текст с начала и до конца. Обычно это требуется при валидации.
|
||||
|
||||
Например, мы хотим проверить, что в переменной `num` хранится именно десятичная дробь.
|
||||
Например, мы хотим проверить, что в переменной `num` хранится именно десятичная дробь.
|
||||
|
||||
Ей соответствует регэксп <code class="pattern">\d+\.\d+</code>. Но простая проверка найдёт дробь в любом тексте:
|
||||
Ей соответствует регэксп <code class="pattern">\d+\.\d+</code>. Но простой поиск найдёт дробь в любом тексте:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -49,7 +51,9 @@ var num = "ля-ля 12.34";
|
|||
alert( num.match(/\d+\.\d+/ig) ); // 12.34
|
||||
```
|
||||
|
||||
Если мы хотим проверить, что `num` *целиком* соответствует паттерну <code class="pattern">\d+\.\d+</code>, то укажем якоря по обе стороны от него:
|
||||
Наша же задача -- проверить, что `num` *целиком* соответствует паттерну <code class="pattern">\d+\.\d+</code>.
|
||||
|
||||
Для этого обернём шаблон в якоря <code class="pattern">^...$</code>:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
|
@ -60,3 +64,5 @@ var num = "12.34";
|
|||
alert( num.match(/^\d+\.\d+$/ig) ); // 12.34, дробь!
|
||||
```
|
||||
|
||||
Теперь поиск ищет начало текста, за которым идёт число, затем точка, ещё число и конец текста. Это как раз то, что нужно.
|
||||
|
||||
|
|
|
@ -5,55 +5,85 @@
|
|||
|
||||
В этом случае изменяется поведение <code class="pattern">^</code> и <code class="pattern">$</code>.
|
||||
|
||||
**В многострочном режиме якоря означают не только начало/конец текста, но и начало/конец строки.**
|
||||
В многострочном режиме якоря означают не только начало/конец текста, но и начало/конец строки.
|
||||
|
||||
## Начало строки ^
|
||||
|
||||
В примере ниже текст состоит из нескольких строк. Паттерн <code class="pattern">/^\d+/gm</code> берёт число с начала каждой строки:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '1е место: Винни-пух\n' +
|
||||
var str = '1е место: Винни\n' +
|
||||
'2е место: Пятачок\n' +
|
||||
'33е место: Слонопотам';
|
||||
|
||||
alert( str.match(/^\d+/gm) ); // 1, 2, 33*!*
|
||||
*!*
|
||||
alert( str.match(/^\d+/gm) ); // 1, 2, 33
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание -- без флага <code class="pattern">/m</code> было бы только первое число:
|
||||
Обратим внимание -- без флага <code class="pattern">/m</code> было бы найдено только первое число:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '1е место: Винни-пух\n' +
|
||||
var str = '1е место: Винни\n' +
|
||||
'2е место: Пятачок\n' +
|
||||
'33е место: Слонопотам';
|
||||
|
||||
alert( str.match(/^\d+/g) ); // 1
|
||||
```
|
||||
|
||||
Это потому что в обычном режиме каретка <code class="pattern">^</code> -- это только начало текста.
|
||||
Это потому что в обычном режиме каретка <code class="pattern">^</code> -- это только начало текста, а в многострочном -- начало любой строки.
|
||||
|
||||
Символ доллара <code class="pattern">$</code> ведёт себя точно так же.
|
||||
Движок регулярных выражений двигается по тексту, и как только видит начало строки, начинает искать там <code class="pattern">\d+</code>.
|
||||
|
||||
Следующий пример находит последнее слово в строке:
|
||||
## Конец строки $
|
||||
|
||||
TODO: указать на коренное отличие $ от \n: доллар не матчит символ, а \n матчит!!!!
|
||||
Символ доллара <code class="pattern">$</code> ведёт себя аналогично.
|
||||
|
||||
Регулярное выражение <code class="pattern">[а-я]+$</code> в следующем примере находит последнее слово в каждой строке:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
showMatch(
|
||||
'1st: *!*John*!*\n' +
|
||||
'2nd: *!*Mary*/!*\n' +
|
||||
'33rd: *!*Peter*/!*', /\w+$/gm ) // John, Mary, Peter
|
||||
var str = '1е место: Винни\n' +
|
||||
'2е место: Пятачок\n' +
|
||||
'33е место: Слонопотам';
|
||||
|
||||
alert( str.match(/[а-я]+$/gim) ); // Винни,Пятачок,Слонопотам
|
||||
```
|
||||
|
||||
Please note that <code class="pattern">$</code> as well as <code class="pattern">^</code> doesn't add <code class="match">\n</code> to the match. They only check that the position is right.
|
||||
Без флага <code class="pattern">m</code> якорь <code class="pattern">$</code> обозначал бы конец всего текста, и было бы найдено только последнее слово.
|
||||
|
||||
[smart header="Якорь `$` против `\n`"]
|
||||
Для того, чтобы найти конец строки, можно использовать не только `$`, но и символ `\n`.
|
||||
|
||||
[summary]
|
||||
Но, в отличие от `$`, символ `\n` во-первых берёт символ в результат, а во-вторых -- не совпадает в конце текста (если, конечно, последний символ -- не конец строки).
|
||||
|
||||
Посмотрим, что будет с примером выше, если вместо <code class="pattern">[а-я]+$</code> использовать <code class="pattern">[а-я]+\n</code>:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var str = '1е место: Винни\n' +
|
||||
'2е место: Пятачок\n' +
|
||||
'33е место: Слонопотам';
|
||||
|
||||
alert( str.match(/[а-я]+\n/gim) );
|
||||
/*
|
||||
Винни
|
||||
,Пятачок
|
||||
*/
|
||||
```
|
||||
|
||||
Всего два результата: <code class="match">Винни\n</code> (с символом перевода строки) и <code class="match">Пятачок\n</code>. Последнее слово "Слонопотам" здесь не даёт совпадения, так как после него нет перевода строки.
|
||||
[/smart]
|
||||
|
||||
## Итого
|
||||
|
||||
В мультистрочном режиме:
|
||||
<ul>
|
||||
<li>The caret <code class="pattern">'^'</code> matches the position at the the text start. *In multiline mode* it also matches after the newline symbol.</li>
|
||||
<li>The dollar <code class="pattern">'$'</code> matches the position at the text end. *In multiline mode* it also matches before the newline symbol.</li>
|
||||
<li>Символ `^` означает начало строки.</li>
|
||||
<li>Символ `$` означает конец строки.</li>
|
||||
</ul>
|
||||
|
||||
**For both anchors, the regexp engine only checks the position, and doesn't match a character.**
|
||||
[/summary]
|
||||
Оба символа являются проверками, они не добавляют ничего к результату. Про них также говорят, что "они имеют нулевую длину".
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Word boundary
|
||||
# Word boundary [todo]
|
||||
|
||||
Another position check is a *word boundary* <code class="pattern">\b</code>. It doesn't match a character, but matches in situations when a wordly character follows a non-wordly or vice versa. A "non-wordly" may also be text start or end.
|
||||
[cut]
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# Practice
|
||||
|
||||
Here you found tasks which help in understanding regexp construction principles.
|
||||
[cut]
|
||||
|
||||
|
||||
The tutorial is not finished. Till now, we have it up to this part...
|
|
@ -0,0 +1,5 @@
|
|||
# ToDo
|
||||
|
||||
Требуется добавить главы про предпросмотр lookahead.
|
||||
|
||||
Какие-то ещё?
|
|
@ -0,0 +1,303 @@
|
|||
# Чёрная дыра бэктрекинга
|
||||
|
||||
Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже "подвешивать" интерпретатор JavaScript.
|
||||
|
||||
Рано или поздно, с этим сталкивается любой разработчик, потому что нечаянно создать такое регулярное выражение -- легче лёгкого.
|
||||
|
||||
Типична ситуация, когда регулярное выражение до поры до времени работает нормально, и вдруг на каком-то тексте как начнёт "подвешивать" интерпретатор и есть 100% процессора.
|
||||
|
||||
Это может стать уязвимостью. Например, если JavaScript выполняется на сервере, то при разборе данных, присланных посетителем, он может зависнуть, если использует подобный регэксп. На клиенте тоже возможно подобное, при использовании регэкспа для подсветки синтаксиса.
|
||||
|
||||
Такие уязвимости "убивали" почтовые сервера и системы обмена сообщениями и до появления JavaScript, и наверно будут "убивать" и после его исчезновения. Так что мы просто обязаны с ними разобраться.
|
||||
|
||||
[cut]
|
||||
|
||||
## Пример
|
||||
|
||||
План изложения у нас будет таким:
|
||||
|
||||
<ol>
|
||||
<li>Сначала посмотрим на проблему в реальной ситуации.</li>
|
||||
<li>Потом упростим реальную ситуацию до "корней" и увидим, откуда она берётся.</li>
|
||||
</ol>
|
||||
|
||||
Рассмотрим, например, поиск по HTML.
|
||||
|
||||
Мы хотим найти теги с атрибутами, то есть совпадения вида <code class="subject"><a href="..." class=doc ...></code>.
|
||||
|
||||
Самый простой способ это сделать -- <code class="pattern"><[^>]*></code>. Но он же и не совсем корректный, так как тег может выглядеть так: <code class="subject"><a test="<>" href="#"></code>. То есть, внутри "закавыченного" атрибута может быть символ `>`. Простейший регэксп на нём остановится и найдёт <code class="match"><a test="<></code>.
|
||||
|
||||
Соответствие:
|
||||
```
|
||||
<[^>]*....>
|
||||
<a test="<>" href="#">
|
||||
```
|
||||
|
||||
А нам нужен весь тег.
|
||||
|
||||
Для того, чтобы правильно обрабатывать такие ситуации, нужно учесть их в регулярном выражении. Оно будет иметь вид <code class="pattern"><тег (ключ=значение)*></code>.
|
||||
|
||||
Если перевести на язык регэкспов, то: <code class="pattern"><\w+(\s*\w+=(\w+|"[^"]*")\s*)*></code>:
|
||||
<ol>
|
||||
<li><code class="pattern"><\w+</code> -- начало тега</li>
|
||||
<li><code class="pattern">(\s*\w+=(\w+|"[^"]*")\s*)*</code> -- произвольное количество пар вида `слово=значение`, где "значение" может быть также словом <code class="pattern">\w+</code>, либо строкой в кавычках <code class="pattern">"[^"]*"</code>.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
Мы пока не учитываем все детали грамматики HTML, ведь строки возможны и в 'одинарных' кавычках, но на данный момент этого достаточно. Главное, что регулярное выражение получилось в меру простым и понятным.
|
||||
|
||||
|
||||
Испытаем полученный регэксп в действии:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g;
|
||||
|
||||
var str='...<a test="<>" href="#">... <b>...';
|
||||
|
||||
alert( str.match(reg) ); // <a test="<>" href="#">, <b>
|
||||
```
|
||||
|
||||
Отлично, всё работает! Нашло как длинный тег <code class="match"><a test="<>" href="#"></code>, так и одинокий <code class="match"><b></code>.
|
||||
|
||||
А теперь -- демонстрация проблемы.
|
||||
|
||||
Если запустить пример ниже, то он может подвесить браузер:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
var reg = /<\w+(\s*\w+=(\w+|"[^"]*")\s*)*>/g;
|
||||
|
||||
var str = "<tag 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 a=b a=b a=b a=b a=b a=b a=b a=b";
|
||||
|
||||
*!*
|
||||
// Этот поиск будет выполняться очень, очень долго
|
||||
alert( str.match(reg) );
|
||||
*/!*
|
||||
```
|
||||
|
||||
Некоторые движки регулярных выражений могут в разумное время разобраться с таким поиском, но большинство -- нет.
|
||||
|
||||
В чём дело? Почему несложное регулярное выражение на такой небольшой строке "виснет" наглухо?
|
||||
|
||||
Упростим ситуацию, удалив тег и возможность указывать строки в кавычках:
|
||||
|
||||
|
||||
```js
|
||||
//+ run
|
||||
// только атрибуты, разделённые пробелами
|
||||
var reg = /<(\s*\w+=\w+\s*)*>/g;
|
||||
|
||||
var str = "<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 a=b a=b a=b a=b a=b a=b a=b a=b";
|
||||
|
||||
*!*
|
||||
// Этот поиск будет выполняться очень, очень долго
|
||||
alert( str.match(reg) );
|
||||
*/!*
|
||||
```
|
||||
|
||||
То же самое.
|
||||
|
||||
На этом мы закончим с демонстрацией "практического примера" и перейдём к разбору происходящего.
|
||||
|
||||
## Бектрекинг
|
||||
|
||||
В качестве ещё более простого регулярного выражения, рассмотрим <code class="pattern">(\d+)*$</code>.
|
||||
|
||||
В большинстве движков регэкспов, например в Chrome или IE, этот поиск выполняется очень долго (осторожно, может "подвесить" браузер):
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( '12345678901234567890123456789123456789z'.match(/(\d+)*$/) );
|
||||
```
|
||||
|
||||
В чём же дело, что не так с регэкспом?
|
||||
|
||||
Внимательный читатель, посмотрев на него, наверняка удивится, ведь он "какой-то странный". Квантификатор <code class="pattern">*</code> здесь выглядит лишним.
|
||||
|
||||
Если хочется найти число, то с тем же успехом можно искать <code class="pattern">\d+$</code>.
|
||||
|
||||
Да, этот регэксп носит искусственный характер, но, разобравшись с ним, мы поймём и практический пример, данный выше. Причина их медленной работы одинакова.
|
||||
|
||||
В целом, с регэкспом "всё так", синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему.
|
||||
|
||||
Посмотрим, что происходит при поиске в строке <code class="subject">123456789z</code>:
|
||||
|
||||
<ol>
|
||||
<li>Первым делом, движок регэкспов пытается найти <code class="pattern">\d+</code>. Плюс <code class="pattern">+</code> является жадным по умолчанию, так что он хватает все цифры, какие может:
|
||||
|
||||
```
|
||||
\d+.......
|
||||
(123456789)z
|
||||
```
|
||||
</li>
|
||||
<li>Затем движок пытается применить звёздочку вокруг скобок <code class="pattern">(\d+)*</code>, но больше цифр нет, так что звёздочка не даёт повторений.
|
||||
|
||||
Затем в шаблоне идёт символ конца строки <code class="pattern">$</code>, а в тексте -- символ <code class="subject">z</code>.
|
||||
|
||||
```
|
||||
X
|
||||
\d+........$
|
||||
(123456789)z
|
||||
```
|
||||
Соответствия нет.
|
||||
</li>
|
||||
<li>Так как соответствие не найдено, то "жадный" плюс <code class="pattern">+</code> отступает на один символ (бэктрекинг).
|
||||
|
||||
Теперь `\d+` -- это все цифры, за исключением последней:
|
||||
```
|
||||
\d+.......
|
||||
(12345678)9z
|
||||
```
|
||||
</li>
|
||||
<li>После бэктрекинга, <code class="pattern">\d+</code> содержит всё число, кроме последней цифры. Движок снова пытается найти совпадение, уже с новой позиции (`9`).
|
||||
|
||||
Звёздочка <code class="pattern">(\d+)*</code> теперь может быть применена -- она даёт число <code class="match">9</code>:
|
||||
|
||||
```
|
||||
|
||||
\d+.......\d+
|
||||
(12345678)(9)z
|
||||
```
|
||||
Движок пытается найти `$`, но это ему не удаётся -- на его пути опять `z`:
|
||||
|
||||
```
|
||||
X
|
||||
\d+.......\d+
|
||||
(12345678)(9)z
|
||||
```
|
||||
|
||||
Так как совпадения нет, то поисковой движок отступает назад ещё раз.
|
||||
</li>
|
||||
<li>Теперь первое число <code class="pattern">\d+</code> будет содержать 7 цифр, а остаток строки <code class="subject">89</code> становится вторым <code class="pattern">\d+</code>:
|
||||
|
||||
|
||||
```
|
||||
X
|
||||
\d+......\d+
|
||||
(1234567)(89)z
|
||||
```
|
||||
|
||||
Увы, всё ещё нет соответствия для <code class="pattern">$</code>.
|
||||
|
||||
Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй <code class="pattern">\d+</code>, до одного символа <code class="subject">8</code>, и звёздочка забирает следующий <code class="subject">9</code>.
|
||||
|
||||
|
||||
```
|
||||
X
|
||||
\d+......\d+\d+
|
||||
(1234567)(8)(9)z
|
||||
```
|
||||
</li>
|
||||
<li>...И снова неудача. Второе и третье <code class="pattern">\d+</code> отступили по-максимуму, так что сокращается снова первое число, до <code class="subject">123456</code>, а звёздочка берёт оставшееся:
|
||||
|
||||
```
|
||||
X
|
||||
\d+.......\d+
|
||||
(123456)(789)z
|
||||
```
|
||||
|
||||
Снова нет совпадения. Процесс повторяется, последний жадный квантификатор <code class="pattern">+</code> отпускает один символ (`9`):
|
||||
|
||||
```
|
||||
X
|
||||
\d+.....\d+ \d+
|
||||
(123456)(78)(9)z
|
||||
```
|
||||
</li>
|
||||
|
||||
...И так далее.
|
||||
|
||||
Получается, что движок регулярных выражений перебирает все комбинации из `123456789` и их подпоследовательности. А таких комбинаций очень много.
|
||||
|
||||
На этом месте умный читатель может воскликнуть: "Во всём виноват бэктрекинг? Давайте включим ленивый режим -- и не будет никакого бэктрекинга!"
|
||||
|
||||
Что ж, заменим <code class="pattern">\d+</code> на <code class="pattern">\d+?</code> и посмотрим (аккуратно, может подвесить браузер):
|
||||
|
||||
```js
|
||||
//+ run
|
||||
alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
|
||||
```
|
||||
|
||||
Не помогло!
|
||||
|
||||
**Ленивые регулярные выражения делают то же самое, но в обратном порядке.**
|
||||
|
||||
Просто подумайте о том, как будет в этом случае работать поисковой движок.
|
||||
|
||||
Некоторые движки регулярных выражений содержат хитрые проверки и конечные автоматы, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда.
|
||||
|
||||
Возвращаясь к примеру выше -- при поиске <code class="pattern"><(\s*\w+=\w+\s*)*></code> в строке <code class="subject"><a=b a=b a=b a=b</code> происходит то же самое.
|
||||
|
||||
Поиск успешно начинается, выбирается некая комбинация из <code class="pattern">\s*\w+=\w+\s*</code>, которая, так как в конце нет `>`, оказывается не подходящей. Движок честно отступает, пробует другую комбинацию -- и так далее.
|
||||
|
||||
## Что делать?
|
||||
|
||||
Проблема -- в сверхмноговариантном переборе.
|
||||
|
||||
Движок регулярных выражений перебирает кучу возможных вариантов скобок там, где это не нужно.
|
||||
|
||||
Например, в регэкспе <code class="pattern">(\d+)*$</code> нам (людям) очевидно, что в <code class="pattern">(\d+)</code> откатываться не нужно. От того, что вместо одного <code class="pattern">\d+</code> у нас два независимых <code class="pattern">\d+\d+</code>, ничего не изменится.
|
||||
|
||||
Без разницы:
|
||||
|
||||
```
|
||||
\d+........
|
||||
(123456789)z
|
||||
|
||||
\d+...\d+....
|
||||
(1234)(56789)z
|
||||
```
|
||||
|
||||
Если вернуться к более реальному примеру <code class="pattern"><(\s*\w+=\w+\s*)*></code> то
|
||||
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+` без отката выглядит так: <code class="pattern">(?=(a+))\1</code>.
|
||||
|
||||
То есть, иными словами, предпросмотр <code class="pattern">?=</code> ищет максимальное количество повторений <code class="pattern">a+</code>, доступных с текущей позиции. А затем они "берутся в результат" обратной ссылкой <code class="pattern">\1</code>. Дальнейший поиск -- после найденных повторений.
|
||||
|
||||
Откат в этой логике принципе не предусмотрен, поскольку предпросмотр "откатываться" не умеет. То есть, если предпросмотр нашёл 5 штук <code class="pattern">a+</code>, и в результате поиск не удался, то он не будет откатываться на 4 повторения. Эта возможность в предпросмотре отсутствует, а в данном случае она как раз и не нужна.
|
||||
|
||||
Исправим регэксп для поиска тега с атрибутами <code class="pattern"><\w+(\s*\w+=(\w+|"[^"]*")\s*)*></code>, описанный в начале главы. Используем предпросмотр, чтобы запретить откат на меньшее количество пар `атрибут=значение`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
// регэксп для пары атрибут=значение
|
||||
var attr = /(\s*\w+=(\w+|"[^"]*")\s*)/
|
||||
|
||||
// используем его внутри регэкспа для тега
|
||||
var reg = new RegExp('<\\w+(?=(' + attr.source + '*))\\1>', 'g');
|
||||
|
||||
var good = '...<a test="<>" href="#">... <b>...';
|
||||
|
||||
var bad = "<tag 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 a=b a=b a=b a=b a=b a=b a=b a=b";
|
||||
|
||||
alert( good.match(reg) ); // <a test="<>" href="#">, <b>
|
||||
alert( bad.match(reg) ); // null (нет результатов, быстро)
|
||||
```
|
||||
|
||||
Отлично, всё работает! Нашло как длинный тег <code class="match"><a test="<>" href="#"></code>, так и одинокий <code class="match"><b></code>.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -1,3 +1,3 @@
|
|||
# Регулярные выражения [в работе]
|
||||
# Регулярные выражения [незавершён]
|
||||
|
||||
Регулярные выражения -- мощный способ поиска и замены для строк.
|