Я хотел бы показать вам три хитрые вещи, которые вы можете сделать с помощью регулярных выражений, которые обеспечивают аккуратное решение некоторых очень сложных проблем:
1. Удаление комментариев
Регулярные выражения облегчают работу с односимвольными разделителями , поэтому разметку из строки так легко удалить:
str = str.replace(/(<[\/]?[^>]+>)/g, '');
Именно отрицание в классе персонажей делает настоящую работу:
[^>]
Что означает «все, кроме <
» . Таким образом, выражение ищет начальный тег-разделитель и возможный слеш, затем все, кроме закрывающего тега-разделителя, а затем сам разделитель. Легко.
Однако комментарии не так просты, потому что разделители комментариев состоят из более чем одного символа . Например, многострочные комментарии в CSS и JavaScript начинаются с /*
и заканчиваются */
, но между этими двумя разделителями может быть любое количество не связанных звездочек .
Я часто использую несколько звездочек в комментариях, чтобы указать серьезность ошибки, которую я только что заметил, например:
/*** this is a bug with 3-star severity ***/
Но если бы мы попытались разобрать это с помощью одного символа отрицания, это бы не сработало:
str = str.replace(/(\/\*[^\*]+\*\/)/g, '');
Однако с помощью регулярных выражений невозможно сказать: «все, кроме [этой последовательности символов]» , мы можем только сказать: «все, кроме [одного из этих отдельных символов]» .
Итак, вот регулярное выражение, которое нам нужно:
str = str.replace(/(\/\*([^*]|(\*+[^*\/]))*\*+\/)/gm, '');
Выражение обрабатывает несвязанные символы, глядя на то, что следует за ними — звездочки разрешены, если за ними не следует косая черта, пока мы не найдем тот, который есть, и это конец комментария.
Поэтому он говорит: « /
затем *
(затем все, кроме *
ИЛИ любое число *
за которым следует что угодно, кроме /
) (и любое количество экземпляров этого), затем любое число *
то /
»
(Синтаксис выглядит особенно запутанным, поскольку *
и /
являются специальными символами в регулярных выражениях, поэтому необходимо избегать неоднозначных литеральных символов. Также обратите внимание на флаг m
в конце выражения, что означает многострочный , и указывает, что регулярное выражение должно искать более одной строки текста.)
Используя тот же принцип, мы можем адаптировать выражение для поиска любого вида сложных разделителей. Вот еще один, который соответствует HTML- комментариям:
str = str.replace(/(<!\-\-([^\-]|(\-+[^>]))*\-+>)/gm, '');
А вот для разделов CDATA
:
str = str.replace(/(<\!\[CDATA\[([^\]]|(\]+[^>]))*\]+>)/gm, '');
2. Использование замещающих обратных вызовов
Функция replace
также может передавать обратный вызов в качестве второго параметра, и это неоценимо в тех случаях, когда желаемая замена не может быть описана простым выражением. Например:
isocode = isocode.replace(/^([az]+)(\-[az]+)?$/i, function(match, lang, country) { return lang.toLowerCase() + (country ? country.toUpperCase() : ''); });
Этот пример нормализует использование заглавных букв в кодах языков — поэтому "EN"
станет "en"
, а "en-us"
станет "en-US"
.
Первым аргументом, который передается в обратный вызов, всегда является полное совпадение, затем каждый последующий аргумент соответствует обратным ссылкам (т. Е. Arguments arguments[1]
— это то, что замена строки будет называть $1
и т. Д.).
Поэтому, взяв "en-us"
в качестве входных данных, мы получим три аргумента:
-
"en-us"
-
"en"
-
"-us"
Тогда все, что нужно сделать функции — это обеспечить выполнение соответствующих случаев, повторно объединить части и вернуть их. Все, что возвращает обратный вызов, — это то, что возвращает сама замена.
Но на самом деле нам не нужно присваивать возвращаемое значение (или возвращать вообще), и если мы этого не сделаем, то исходная строка не изменится. Это означает, что мы можем использовать replace
в качестве строкового процессора общего назначения — для извлечения данных из строки без ее изменения.
Вот еще один пример, который объединяет многострочное выражение комментария из предыдущего раздела с обратным вызовом, который извлекает и сохраняет текст каждого комментария:
var comments = []; str.replace(/(\/\*([^*]|(\*+[^*\/]))*\*+\/)/gm, function(match) { comments.push(match); });
Поскольку ничего не возвращается, исходная строка остается неизменной. Хотя, если бы мы хотели извлечь и удалить комментарии, мы могли бы просто вернуть и назначить пустую строку:
var comments = []; str = str.replace(/(\/\*([^*]|(\*+[^*\/]))*\*+\/)/gm, function(match) { comments.push(match); return ''; });
3. Работа с невидимыми разделителями
Извлечение содержимого очень хорошо, когда оно использует стандартные разделители, но что если вы используете пользовательские разделители , о которых знает только ваша программа? Проблема в том, что строка может уже содержать ваш разделитель , буквально символ для символа, и что вы тогда делаете?
Ну, недавно я придумал очень милый трюк, который не только избегает этой проблемы, но также прост в использовании, как односимвольный класс, который мы видели в начале! Хитрость заключается в использовании символов Юникода, которые документ не может содержать .
Первоначально я пробовал это с неопределенными символами, и это, безусловно, сработало, но небезопасно предполагать, что любой такой символ всегда будет неопределенным (или что документ все равно не будет содержать его). Затем я обнаружил, что Unicode на самом деле резервирует набор кодов специально для такого рода вещей — так называемые нехарактеры , которые никогда не будут использоваться для определения реальных символов. Допустимый документ Unicode не может содержать нехарактеры, но программа может использовать их внутри себя для своих собственных целей.
Я работал над процессором CSS , и мне нужно было удалить все комментарии перед синтаксическим анализом селекторов, чтобы они не перепутали выражения, соответствующие селектору. Но их нужно было заменить в источнике чем-то, что занимало такое же количество строк, чтобы номера строк оставались точными. Затем позже они должны быть добавлены обратно к источнику для окончательного вывода.
Итак, сначала мы используем обратный вызов регулярного выражения для извлечения и сохранения комментариев. Обратный вызов возвращает копию совпадения, в котором все непробельные символы преобразованы в пробелы и которые разделены не символами с обеих сторон:
var comments = []; csstext = csstext.replace(/(\/\*([^*]|(\*+([^*\/])))*\*+\/)/gm, function(match) { comments.push(match); return '\ufddf' + match.replace(/[\S]/gim, ' ') + '\ufddf'; });
Это создает массив комментариев в том же порядке исходного кода, что и пробелы, которые они оставляют позади, а сами пробелы занимают столько же строк, сколько и исходный комментарий.
Затем оригиналы можно восстановить, просто заменив каждый разделенный пробел соответствующим сохраненным комментарием — и поскольку разделители представляют собой отдельные символы, нам нужен только простой класс символов для соответствия каждой паре:
csstext = csstext.replace(/(\ufddf[^\ufddf]+\ufddf)/gim, function() { return comments.shift(); });
Как это легко!