Марейн Хавербеке - Выразительный JavaScript
Но вывод предыдущего примера неправильный. Почему?
Часть сначала попытается захватить столько символов, сколько может. Если из-за этого следующая часть регулярки не найдёт себе совпадения, произойдёт откат на один символ и попробует снова. В примере, алгоритм пытается захватить всю строку, и затем откатывается. Откатившись на четыре символа назад, он найдёт в строчке / — а это не то, чего мы добивались. Мы-то хотели захватить только один комментарий, а не пройти до конца строки и найти последний комментарий.
Из-за этого мы говорим, что операторы повторения (+, *, ?, and {}) жадные, то есть они сначала захватывают, сколько могут, а потом идут назад. Если вы поместите вопрос после такого оператора (+?, *?, ??, {}?), они превратятся в нежадных, и начнут находить самые маленькие из возможных вхождений.
И это то, что нам нужно. Заставив звёздочку находить совпадения в минимально возможном количестве символов строчки, мы поглощаем только один блок комментариев, и не более того.
function stripComments(code) {
return code.replace(///.*|/*[^]*?*//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1
Множество ошибок возникает при использовании жадных операторов вместо нежадных. При использовании оператора повтора сначала всегда рассматривайте вариант нежадного оператора.
Динамическое создание объектов RegExp
В некоторых случаях точный шаблон неизвестен во время написания кода. Например, вам надо будет искать имя пользователя в тексте, и заключать его в подчёркивания. Так как вы узнаете имя только после запуска программы, вы не можете использовать запись со слэшами.
Но вы можете построить строку и использовать конструктор RegExp. Вот пример:
var name = "гарри";
var text = "А у Гарри на лбу шрам.";
var regexp = new RegExp("\b(" + name + ")\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → А у _Гарри_ на лбу шрам.
При создании границ слова приходится использовать двойные слэши, потому что мы пишем их в нормальной строке, а не в регулярке с прямыми слэшами. Второй аргумент для RegExp содержит опции для регулярок – в нашем случае “gi”, т. е. глобальный и регистронезависимый.
Но что, если имя будет "dea+hl[]rd" (если наш пользователь – кульхацкер)? В результате мы получим бессмысленную регулярку, которая не найдёт в строке совпадений.
Мы можем добавить обратных слэшей перед любым символом, который нам не нравится. Мы не можем добавлять обратные слэши перед буквами, потому что b или n – это спецсимволы. Но добавлять слэши перед любыми не алфавитно-цифровыми символами можно без проблем.
var name = "dea+hl[]rd";
var text = "Этот dea+hl[]rd всех достал.";
var escaped = name.replace(/[^ws]/g, "\$&");
var regexp = new RegExp("\b(" + escaped + ")\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → Этот _dea+hl[]rd_ всех достал.
Метод search
Метод indexOf нельзя использовать с регулярками. Зато есть метод search, который как раз ожидает регулярку. Как и indexOf, он возвращает индекс первого вхождения, или -1, если его не случилось.
console.log(" word".search(/S/));
// → 2
console.log(" ".search(/S/));
// → -1
К сожалению, никак нельзя задать, чтобы метод искал совпадение, начиная с конкретного смещения (как это можно сделать с indexOf). Это было бы полезно.
Свойство lastIndex
Метод exec тоже не даёт удобного способа начать поиск с заданной позиции в строке. Но неудобный способ даёт.
У объекта регулярок есть свойства. Одно из них – source, содержащее строку. Ещё одно – lastIndex, контролирующее, в некоторых условиях, где начнётся следующий поиск вхождений.
Эти условия включают необходимость присутствия глобальной опции g, и то, что поиск должен идти с применением метода exec. Более разумным решением было бы просто допустить дополнительный аргумент для передачи в exec, но разумность – не основополагающая черта в интерфейсе регулярок JavaScript.
var pattern = /y/g;
pattern.lastIndex = 3;
var match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5
Если поиск был успешным, вызов exec обновляет свойство lastIndex, чтобы оно указывало на позицию после найденного вхождения. Если успеха не было, lastIndex устанавливается в ноль – как и lastIndex у только что созданного объекта.
При использовании глобальной переменной-регулярки и нескольких вызовов exec эти автоматические обновления lastIndex могут привести к проблемам. Ваша регулярка может начать поиск с позиции, оставшейся с предыдущего вызова.
var digit = /d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null
Ещё один интересный эффект опции g в том, что она меняет работу метода match. Когда он вызывается с этой опцией, вместо возврата массива, похожего на результат работы exec, он находит все вхождения шаблона в строке и возвращает массив из найденных подстрок.
console.log("Банан".match(/ан/g));
// → ["ан", "ан"]
Так что поосторожнее с глобальными переменными-регулярками. В случаях, когда они необходимы – вызовы replace или места, где вы специально используете lastIndex – пожалуй и все случаи, в которых их следует применять.
Циклы по вхождениям
Типичная задача – пройти по всем вхождениям шаблона в строку так, чтобы иметь доступ к объекту match в теле цикла, используя lastIndex и exec.
var input = "Строчка с 3 числами в ней... 42 и 88.";
var number = /b(d+)b/g;
var match;
while (match = number.exec(input))
console.log("Нашёл ", match[1], " на ", match.index);
// → Нашёл 3 на 10
// Нашёл 42 на 29
// Нашёл 88 на 34
Используется тот факт, что значением присвоения является присваиваемое значение. Используя конструкцию match = re.exec(input) в качестве условия в цикле while, мы производим поиск в начале каждой итерации, сохраняем результат в переменной, и заканчиваем цикл, когда все совпадения найдены.
Разбор INI файлы
В заключение главы рассмотрим задачу с использованием регулярок. Представьте, что мы пишем программу, собирающую сведения о наших врагах через интернет в автоматическом режиме. (Всю программу писать не будем, только ту часть, которая читает файл с настройками. Извините.) Файл выглядит так:
searchengine=http://www.google.com/search?q=$1
spitefulness=9.7
; перед комментариями ставится точка с запятой
; каждая секция относится к отдельному врагу
[larry]
fullname=Larry Doe
type=бычара из детсада
website=http://www.geocities.com/CapeCanaveral/11451
[gargamel]
fullname=Gargamel
type=злой волшебник
outputdir=/home/marijn/enemies/gargamel
Точный формат файла (который довольно широко используется, и обычно называется INI), следующий:
• Пустые строки и строки, начинающиеся с точки с запятой, игнорируются.
• Строки, заключённые в квадратные скобки, начинают новую секцию.
• Строки, содержащие алфавитно-цифровой идентификатор, за которым следует =, добавляют настройку в данной секции.
• Всё остальное – неверные данные.
Наша задача – преобразовать такую строку в массив объектов, каждый со свойством name и массивом настроек. Для каждой секции нужен один объект, и ещё один – для глобальных настроек сверху файла.
Так как файл надо разбирать построчно, неплохо начать с разбиения файла на строки. Для этого в главе 6 мы использовали string.split("n"). Некоторые операционки используют для перевода строки не один символ n, а два — rn. Так как метод split принимает регулярки в качестве аргумента, мы можем делить линии при помощи выражения /r?n/, разрешающего и одиночные n и rn между строками.
function parseINI(string) {
// Начнём с объекта, содержащего настройки верхнего уровня
var currentSection = {name: null, fields: []};
var categories = [currentSection];
string.split(/r?n/).forEach(function(line) {
var match;
if (/^s*(;.*)?$/.test(line)) {
return;
} else if (match = line.match(/^[(.*)]$/)) {
currentSection = {name: match[1], fields: []};
categories.push(currentSection);
} else if (match = line.match(/^(w+)=(.*)$/)) {
currentSection.fields.push({name: match[1],
value: match[2]});
} else {
throw new Error("Строчка '" + line + "' содержит неверные данные.");