Статьи

3 изящных трюка с регулярными выражениями

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

  1. Удаление комментариев
  2. Использование замещающих обратных вызовов
  3. Работа с невидимыми разделителями

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" в качестве входных данных, мы получим три аргумента:

  1. "en-us"
  2. "en"
  3. "-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(); }); 

Как это легко!