Конспект JS-course

Конспект. Регулярные выражения

Источник: http://forum.jscourse.com/t/20-konspekt-regulyarnye-vyrazheniya/796

Автор конспекта: @eimrine

Регулярные выражения

Регулярные выражения это такой паттерн, набор символов, который описывает множество строк. В общем виде запись регулярного выражения выглядит так:

/pattern/flags // option 1

new RegExp(pattern[, flags])  //option 2

Между двумя косыми чертами записывается сам паттерн - набор символов, который описывает множество возможных строк и набор известных флагов (option 1). Существует 3 флага:

/(fe)?male/g; // g -  все подстроки female и male
/(fe)?male/i; // i -подстрока в любом регистре fEmaLe или malE
/(fe)?male/ig; //  i,g - все подстроки female и male в любом регистре

Флаги модифицируют поведение регулярного выражения в разных ситуациях. С точки зрения JS, регулярно выражение это класс JavaScript Core, значит можно использовать как в браузере, так и в node.js или в других средах, где есть интерпретатор JS.

По большей мере регулярные выражения объявляются с помощью паттерна, но также есть возможность создать регулярное выражение из строки динамически с помощью конструктора (option 2).

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

Специальные символы можно поделить на несколько категорий. Категория, которая описывает количество появлений групп других символов, называется квантификатором. Квантификатор * описывает повторение 0 и больше раз:

/bo*/

подразумевает символ bo, а также в регулярных выражениях есть символы специальные, которые несут специальный смысл. В частности * предоставил такую логику, что предшествующий символ повторяется от 0 до бесконечности раз. Если хотим, чтобы строка повторилась 0 и больше раз, то мы берём её в запоминающие скобки и завершаем символом *:

/(bo)*/
/(bo)*/.test('') // === true

/foo(bar)*/.test('foo') // === true
/foo(bar)*/.test('fooba') // === true
/foo(bar)*/.test('foobar') // === true
/foo(bar)*/.test('foobarbar') // === true

Квантификатор + описывает повтор 1 и более раз:

/foo(bar)+/.test('foo') // === false
/foo(bar)+/.test('fooba') // === false
/foo(bar)+/.test('foobar') // === true
/foo(bar)+/.test('foobarbar') // === true

Квантификатор ? описывает наличие или отсутствие, эквивалентен {0,1},

/(fe)?male/ === /(fe){0,1}male/ // эквивалентны, но никак не true
/(fe)?male/.test('He is male') // === true
/(fe)?male/.test('She is femalemale') // === true
/(fe)?male/.test('She is fem') // === false

.test() проверяет, соответствует ли строка регулярному выражению.

Квантификатор {} определяет количество повторений:

{n} n раз
{n,m} от n до m раз

Есть описание групп символов, например, \d (числа, digit) или \D (нечисла, non-digit), \s (пробельные символы, space (есть много utf-ных символов, которые входят и не входят сюда))

Также есть такая интересная группа, как поиск границы слов \b (boundary) - полезно, если нужно вычленить слово.

Множество символов [] Множество символов по вхождение а это множество [a-zA-Z] /[a-z]/ and /[\w.]+/ - я хочу множество a-z и непробельный символ, который повторяется от 0 до бесконечности раз раз.

\w (word) - любой alphabetic character . множество всех символов кроме символа переноса строки ^ начало строки $ конец строки

/^.*$/.test('mama\n mila\n ramu') // false
/^.*$/.test('mama mila ramu') // true

Условные квантификаторы:

x(?=y) засчитывает соответствие x в том случае когда за ним следует y

/Jack(?=Sprat||Frost)/.test('JackSprat'); // true
/Jack(?=Sprat||Frost)/.test('JackFrost'); // true

x(?!y) засчитывает соответствие x в том случае когда за ним не следует y

Специальные символы экранируем с помощью бэк-слэша "\"

/mama\*/.test('mama*') // true
/mamaa\*/.test('mama*') // false

() - запоминающие скобочки, объединяют целую регулярку и с точки зрения последующего квантификатора всё что в скобках воспринимается как единый символ. Также () используются чтобы выделить отдельные подстроки в той строке, которая соответствуют регулярному выражению.

О флагах g (global) глобальный поиск, ищет все подстроки что соответствуют регулярному выражению, по умолчанию только первое совпадение.

i (ignore case) игнорирует нижний/верхний регистр символов

m (multiline) модифицирует поведение символов начала и конца строки: начало это значение предыдущего перевода строки или начала строки, а конец это следующий перевод строки в строке или конец строки; каждая логическая строка воспринимается как отдельная строка.

Роли регулярок часто сводятся к замене множества подстрок на какую-то строку. Например:

var str = regExp, regexp, RegExp;
console.log(str.replace(/regexp/ig, '$& =)')); // regExp =), regexp =), RegExp =)

var str = regExp, regexp, RegExp;
console.log(str.replace(/regexp/ig, 'RegExp')); // RegExp, RegExp, RegExp
console.log(str.replace(/regexp/i, 'RegExp')); // RegExp, regexp, RegExp
console.log(str.replace(/regexp/g, 'RegExp')); // regExp, RegExp, RegExp

$& значит "за-match-еная" строка которая заматчилась под регулярным выражением"

console.log(str.replace(/regexp/g, $& RegExp)); // regExp, regexp RegExp, RegExp

console.log(str.replace(/regexp/g, <strong>$&</strong>)); // regExp, regexp, RegExp
console.log(str.replace(/regexp/ig, <strong>$&</strong>)); // regExp, regexp, RegExp

Запоминающие скобки можно использовать, чтобы получить доступ к той подстроке, которая соответствует регулярному выражению внутри этих скобок. Мы можем запомнить вторую часть в запоминающих скобках и использовать её как $1, $2, $3, ..., $N.

console.log(str.replace(/reg(exp)/ig, '<strong>$1</strong>')); // <strong>Exp</strong>, <strong>exp</strong>, <strong>Exp</strong>

var sexReg = /^(fe)?male$/;
console.log(sexReg.test('male')); // true
console.log(sexReg.test('female')); // true
console.log(sexReg.test('He is male')); // false

"хитрая вещь, она нам не нужна" replace-string-occurances.js

function replaceStringOccurances(srcString, stringToReplace, replaceWith) {
        'use strict';
        var escapedString,
            reg;

        escapedString =        stringToReplace.replace(/[\(\)\[\]\\\.\^\$\|\?\+\*\{\}]/g, '\\$&');
        reg = new RegExp(escapedString, 'g');
        return srcString.replace(reg, replaceWith);
    }

"хорошая вещь" greedy.js

var str = '"petya","masha","elena","manya"';
    console.log(str.match(/".*"/g));    // жадное, заматчит всю строку
    console.log(str.match(/".*?"/g));   // недажное, выдаст массив имен
    console.log(str.match(/"[^"]*"/g)); // не может быть жадным или
                                        // нежадным по своей сути
    console.log(str.match(/".*"/g));

Метод exec возвращает массив заматченых подстрок.

var str = '"petya","masha","elena","manya"';
console.log(str); // '"petya","masha","elena","manya"'
console.log(str.match(/".*"/g)); // [""petya","masha","elena","manya""]
console.log(str.match(/".*?"/g)); //  [""petya"", ""masha"", ""elena"", ""manya""]
console.log(str.match(/"[^"]+"/g)); //  [""petya"", ""masha"", ""elena"", ""manya""]

var str = '"petya","masha","elena","manya"';
str.replace(/"([^"]+)"/g , function () {
     console.log(arguments);
});

Метод replace допускает использование регулярного выражения в качестве первого аргумента и функцию в качестве второго, в этом случае для каждой заматченой строки будет вызвана функция и в результирующую строку, которая возвращается из метода replace будет подставлена такая строка, в которую подстроки заматченые под регулярное выражение заменены тем значением, которое возвращается из функции. Но нам интересен тот момент, что в функцию в качестве аргумента будет передано на первое место заматченая строка, а на 2-3-4-5-ое соответствующее значение в запоминающих скобках, в нашем случае запоминающие скобки одни, поэтому 2м аргументом будет заматченое значение или что-то другое, так или иначе, заматченое значение из () у нас будет попадать 1м из аргументов String.replace()

var names = []
        string = '"petya","masha","elena","manya"';
    string.replace(/"([^"]+)"/g , function (matchedString) {
        console.log(arguments);
    });
// Output:
    [""petya"", "petya", 0, ""petya","masha","elena","manya""]
    [""masha"", "masha", 8, ""petya","masha","elena","manya""]
    [""elena"", "elena", 16, ""petya","masha","elena","manya""]
    [""manya"", "manya", 24, ""petya","masha","elena","manya""]
    "undefined,undefined,undefined,undefined"

Первое место в массиве это заматченая строка, дальше то, что в запоминающих скобках, индекс символа в строке - начала подстроки, последний аргумент - сама строка; в переменную userName будет записана строка, которая соответствует регулярному выражению, записанного в запоминающих скобках, соответственно мы можем извлечь все имена.

var names = [],
    string = '"petya","masha","elena","manya"';
string.replace(/"([^"]+)"/g , function (matchedString, userName) {
    names.push(userName);
});

console.log(names); ["petya", "masha", "elena", "manya"]

Из интересных моментов, случай когда мы используем регулярное выражение, которое матчит максимально возможно длинную строку:

var str = '"petya","masha","elena","manya"';

console.log(str.match(/".*"/g)); // [""petya","masha","elena","manya""]

Мы хотели строку покороче, а регулярка заматчила максимально длинную; такие регулярные выражения, которые матчат максимально длинные строки из возможных называются жадными (greedy). Иногда мы хотим, чтобы заматчилась максимально короткая строка, тогда следует написать нежадное выражение

/&quot;([^&quot;]+)&quot;/g - пример нежадного регулярного выражения.

Один из способов также описать это нежадное регулярное выражение - это использовать квантификатор повторения 0-1 раз ?, этот подход есть описание нежадного регулярного выражения в общем виде. Точно так же мы могли использовать вместо того, чтоб описывать множество символов за исключением тех, которые мы хотим чтоб оно попало в результирующую строку, мы б могли описывать просто с помощью квантификатора нежадности ?.

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

Если посмотреть на то, как выглядит templater, то сразу напрашивается в голове решение с помощью регулярных выражений, потому что по большому счёту нам надо искать строку, которая выглядит как ${ потом последовательность символов и }, нам надо найти значение ключа, которому соответствует строка между фигурных скобок.

Пара моментов на будущее:

Если можно написать код без регулярных выражений, то так и стоит сделать потому что регулярку сложно понять, чем больше становится регулярка, тем стремительней возрастает сложность её понимания.

Если нужно проверить строку на соответствие регулярному выражению, используйте test вместо exec потому что exec дороже.

Валидация емайлов это больная тема, рекомендую не валидировать регулярное выражение на абсолютную правильность. Пользователю нужно подсказать, ошибся ли он в написании емайла, а не сказать, валидный емайл или нет, поэтому валидация должна быть максимально простой, достаточно искать @ и ..

Помните про greedy/non-greedy, по умолчанию регулярные выражения жадные

Из интересных моментов, copmples-ds.js

function ds(serializedString) {
    var resultObject = {};
    serializedString.replace(/<(.+?)>/g, function (matchedString, bracketCotents) {
    var values = bracketCotents.split(",");
        if (!resultObject[values[0]]) {
            // Use first value as key for object
            resultObject[values[0]] = values;
        }
    });
    return resultObject;
}
console.log(ds("<anya,90,60,90><lena,92,65,87><nona,88,62,90>"));

Это код десериализации объектов. Был придуман формат хранения данных, идея в том, что данные сериализовались в строки для того, чтобы потом превращать их как-то в объекты (не спрашивайте почему не JSON). Функция десериализации выглядит следующим образом: нам нужно заматчить все подстроки, которые находятся между &lt;&gt; и запомнить то, что между треугольными скобками, используем квантификатор нежадности ? и во второй параметр String.replace будет попадать строка между &lt;&gt;. Дальше мы разбираем подстроку.

Про таймер

function StopWatch(node) {
        this.node = node;
        this.bindEvents();
    }
    StopWatch.prototype.start = function () {};
    StopWatch.prototype.stop = function () {};
    StopWatch.prototype.bindEvents = function () {
        var _this = this
        this.node.addEventListener('mouseenter', function (event) {
            StopWatch.lastActive = _this;
        }, false );
        document.addEventListener('keyup', function (event) {
            if (StopWatch.lastActive == _this) {
                if (event.keyCode === STOP) {
                    _this.stop();
                }
                if (event.keyCode === START) {
                    _this.start();
                }
            }
        }, false );
    };

Механизм такой: у нас на странице размещено несколько таймеров. Мы навели мышку и начинаем по нему клацать. В свойство StopWatch конструктора, которое доступно каждому инстансу записывается lastActive, ссылка на тот таймер, по которому мы клацаем. Если мышка наводится на другой таймер, то это свойство перезаписывается. Каждый таймер зарегистрировал обработчики на глобальном уровне и сравнивает, является ли последний активный таймер тем самым, который регистрировал этот обработчик, и если объявляется то мы как-то реагируем, если нет то не реагируем. Стараемся избегать хранения данных на глобальном уровне.