Эй, сценарист!Поводя бровями на регулярные выражения

Программисты корпорации Майкрософт

Загрузить исходный код для этой статьи: HeyScriptingGuy2008_05.exe (150KB)

В ноябре прошлого года сценаристы провели день в Париже по дороге на конференцию по информационным технологиям Tech•Ed IT Forum, проходившую в Барселоне. В ходе этой однодневной остановки мы воспользовались возможностью посетить Лувр, самый знаменитый музей искусств в городе.

Спрашиваете, как мы нашли Лувр? Все просто: вышли из собора Нотр-Дам и повернули налево.

Ой, вы имели в виду, понравился ли нам Лувр? В основном да. Единственная проблема была в том, что Лувр, подобно многим музеям, работает по принципу «смотри, но не трогай». Всем ясно, что Мона Лиза смотрелась бы лучше, будь у нее брови, но по какой-то причине сотрудникам музея очень не нравятся попытки исправить какую-нибудь из картин.

Примечание: на самом деле обоим сценаристам понравилась Мона Лиза. Это, кстати, было приятным сюрпризом – после всей шумихи и накрутки мы боялись, что это окажется просто еще одной картиной. Но нет, все оказалось очень клёво. (Хотя ей могло бы пригодиться немного бровей.) Что интересно, столь же разрекламированная Венера Милосская нас разочаровала. Нам обоим она показалась не столь уж искусно сделанной, и сценарист, пишущий эту статью, был озадачен самой идеей Венеры Милосской. Статуя безрукой женщины? Чем же она должна мыть полы или тарелки!?!

Примечание для наших читательниц (если кто-то из них еще читает): это была явная опечатка. Мы хотели сказать следующее: статуя безрукой женщины? И она, тем не менее, может выполнить вдвое больше работы, чем любой мужчина, да еще и сделать ее правильно.

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

Так или иначе, в тот момент, когда нашему взгляду предстали величайшие сокровища Лувра, обеим сценаристам пришла в голову одна и та же мысль: где здесь туалет? В ходе поиска автору этой статьи пришла в голову еще одна мысль: наша группа лицемерна. В конце концов, нас обидело, что Лувр не позволил нам исправить Мону Лизу. А в то же время мы и сами не без подобного греха. В выпуске журнала TechNet Magazine за январь 2008 года мы написали статью о использовании регулярных выражений в сценариях. Эта статья – превосходный пример «смотри, но не трогай»: мы показали, как использовать регулярные выражения для определения проблем в текстовом файле, но не показали, как решать эти проблемы. Шьорт побьери!

Примечание: так если сценаристы из группы Scripting Guys посетили Лувр в ноябре 2007 года, как могла у того из них, что пишет эту статью, возникнуть неожиданная мысль в статье, которая появилась в TechNet Magazine лишь в январе 2008 года? Ничего себе загадка, да? Должно быть, дело в разнице часовых поясов между Редмондом и Парижем.

К счастью (и в отличие от сотрудников Лувра), мы умеем признавать свои ошибки. Мы были неправы, показав только как искать что-либо с помощью регулярных выражений, нам следовало показать и как это что-либо заменять с помощью регулярных выражений,. По сути, нам следовало показать читателю сценарий, подобный приведенному на рис. 1..

Figure 1 Найти и заменить

      Set objRegEx = _
    CreateObject("VBScript.RegExp")

objRegEx.Global = True   
objRegEx.IgnoreCase = True
objRegEx.Pattern = "Mona Lisa"

strSearchString = _
    "The Mona Lisa is in the Louvre."
strNewString = _
    objRegEx.Replace(strSearchString, _
                     "La Gioconda")

Wscript.Echo strNewString 

Впрочем, по правде говоря, это довольно-таки заурядное использование регулярных выражений: все, что мы здесь делаем, это заменяем все экземпляры строкового значения Mona Lisa на La Gioconda («Так, куда я дел эти брови?» по итальянски). Надо заметить, эту замену было бы куда легче произвести, используя функцию Replace («Заменить») VBScript. Но не бойтесь: мы используем этот нехитрый маленький сценарий, чтобы объяснить читателям, как выполнять операции поиска и замены с помощью регулярных выражений, а когда это будет сделано, мы продемонстрируем некоторые из более хитрых вещей, которые можно проделать с помощью этих выражений.

Как можно заметить, этот сценарий весьма прост. Мы начинаем с создания объекта VBScript.RegExp, нечего и говорить, что именно этот объект позволяет нам использовать регулярные выражения внутри сценария VBScript. После создания объекта мы присваиваем значения трем из свойств объекта:

Global («Глобальный») Установив это свойство на True, мы указываем сценарию найти (и заменить) каждый экземпляр Mona Lisa в целевом тексте. Если свойство Global установлено на False (значение по умолчанию), сценарий будет искать и заменять только первый экземпляр Mona Lisa.

IgnoreCase («Игнорировать регистр») Устанавливая IgnoreCase на True, мы указываем сценарию, что хотим выполнить не зависящий от регистра поиск, другими словами, мы хотим, чтобы mona lisa и Mona Lisa воспринимались одинаково. По умолчанию, VBScript выполняет зависящий от регистра поиск, а это значит, что благодаря строчным и прописным буквам mona lisa и Mona Lisa рассматриваются как совершенно различные значения.

Pattern («Шаблон») Свойство Pattern содержит искомое значение. В данном случае мы без затей ищем простое строковое значение: Mona Lisa.

Далее мы присваиваем текст, который следует искать, переменной, именуемой strSearchString:

strSearchString = "The Mona Lisa is in the Louvre."

Затем мы вызываем метод регулярного выражения Replace («Заменить») и передаем этому методу два параметра: целевой текст, который следует искать (переменная strSearchString), и текст-замену (La Gioconda). Что мы здесь и делаем:

strNewString = objRegEx.Replace(strSearchString, "La Gioconda")

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

The La Gioconda is in the Louvre.

Грамматика может быть несколько сомнительной, но основная идея понятна.

Как мы уже отмечали раньше, все это замечательно, но избыточно; точно того же можно достигнуть, используя следующие строки кода (на самом деле, при желании можно было бы уместить все в одну строку):

strSearchString = "The Mona Lisa is in the Louvre."
strNewString = Replace(strSearchString, "Mona Lisa", "La Gioconda")
Wscript.Echo strNewString

Другими словами, давайте посмотрим, можно ли сделать с регулярными выражениями что-либо интересное, что нельзя сделать с помощью функции Replace в VBScript.

Хм, ни у кого никаких идей? Ну, вот одна, для примера. Часто нам, сценаристам, приходится копировать текст из одного типа документа в другой. Порой это работает неплохо, порой нет. Когда это не работает, часто возникают проблемы с интервалами между словами, приводящие к тексту, который выглядит примерно следующим образом:

Myer Ken, Vice President, Sales and Services

Кошмар, только взгляните на все эти лишние пробелы! И в этом случае полезность функции Replace ограничена. Почему, спросите вы? Ну, у нас имеется кажущееся случайным число лишних пробелов: между словами их может быть 7 или 2, или 6. Это делает сложным исправление проблемы с помощью Replace. Например, при попытке искать два последовательных пробела (заменяя их одним пробелом между словами), мы получим следующее:

Myer Ken, Vice President, Sales and  Services

Это лучше, но не намного. Есть способ, которым можно было бы сделать это, но он требует поиска случайного числа пробелов (скажем, 39), выполнения замены; вычитания единицы из начального числа, поиска 38 пробелов, выполнения замены, и т.д., и т.п. В качестве альтернативы можно использовать этот гораздо более простой (и гораздо более надежный) сценарий с регулярными выражениями:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = " {2,}"

strSearchString = _
"Myer Ken, Vice President, Sales and Services"
strNewString = objRegEx.Replace(strSearchString," ")

Wscript.Echo strNewString

Ключом к этому сценарию (как и к большинству сценариев с регулярными выражениями) является Pattern:

objRegEx.Pattern = " {2,}"

Здесь производится поиск 2 (или более) последовательных пробелов. Из чего можно понять, что этот Pattern ищет 2 (или более) последовательных пробела? Ну, внутри наших двойных кавычек имеется единственный пробел, за которым следует такая конструкция: {2,}. В синтаксисе регулярных выражений это означает искать как минимум два последовательных экземпляра предыдущего символа (в данном случае, пробела). А что если последовательных пробелов 3, или 4, или 937? Ничего страшного, все они тоже будут уловлены. (Если, по какой либо причине, нам нужно было бы уловить минимум 2 пробела, но не более чем 8, мы бы использовали синтаксис {2,8}, где 8 указывает максимальное число совпадений.)

Другими словами, при каждом обнаружении 2 или более пробелов одного за другим все эти последовательные пробелы будут заменены на единственный пробел. Что получится из нашего начального значения строки, полного лишних пробелов? Вот что:

Myer Ken, Vice President, Sales and Services

Видите? Сценаристы действительно могут вносить улучшения. Жаль, что парни в Лувре не дали нам добраться до Моны Лизы.

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

555-123-4567

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

(555) 123-4567

Как предполагается переформатировать все эти телефонные номера? Ну, если нам будет позволено, мы можем предложить сценарий, подобный следующему:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString, "($1) $2-$3")

Wscript.Echo strNewString

Здесь мы ищем последовательность из 3 цифр (\d{3}), за которой следует тире, за которым следуют еще 3 цифры, тире и 4 цифры. Другими словами, мы ищем следующее (где каждый знак Х представляет число от 0 до 9):

XXX-XXX-XXXX

Примечание: так откуда мы знаем, что \d{3} укажет сценарию искать три числа, стоящие рядом друг с другом? Ну, насколько можно вспомнить, мы прочитали об этом где-то. То ли в шокирующей финальной главе «Кода Да Винчи», то ли в справке по языку VBScript из интерактивной версии MSDN® (см. go.microsoft.com/fwlink/?LinkID=111387).

Возможность искать случайные телефонные номера с помощью регулярных выражений – это замечательно. Но у нас все еще остается крупная проблема. В конце концов, мы не можем этот заменить этот случайный телефонный номер на другой случайный телефонный номер, нам нужно использовать тот же самый номер, лишь немного иначе отформатированный. Как же мы можем эту сделать?

Само собой, используя следующий заменяющий текст:

"($1) $2-$3"

$1, $2 являются $3 примерами «обратной ссылки» регулярного выражения. Обратная ссылка – это просто часть найденного текста, которую можно сохранить и использовать повторно. В данном конкретном сценарии мы ищем три «под-совпадения»:

  • Набор из 3 цифр
  • Еще один набор из 3 цифр
  • Набор из 4 цифр

Каждому из этих под-совпадений автоматически назначается обратная ссылка: первому один доллар; второму два и так далее, вплоть до девяти. Другими словами, в этом сценарии трем частям нашего телефонного номера автоматически назначаются обратные ссылки, показанные на рис. 2..

Figure 2 Обратные ссылки телефонного номера

Часть телефонного номера Обратная ссылка
555 $1
123 $2
4567 $3

В строке замены мы используем эти обратные ссылки, чтобы гарантировать повторное использование верного телефонного номера. Наш заменяющий текст просто говорит следующее: взять первую обратную ссылку ($1) и заключить ее в кавычки. Оставить пробел, затем вставить вторую обратную ссылку ($2), за которой следует тире. Наконец, добавить третью обратную ссылку ($3).

Что это все даст нам? Это даст нам телефонный номер, выглядящий следующим образом:

(555) 123-4567

Неплохо, совсем неплохо.

Вот вариант сценария для телефонных номеров. Предположим, что организация установила совершенно новую телефонную систему, и, как часть этого изменения, все телефонные номера теперь имеют один префикс; тогда как ранее номера могли начинаться с 666, 777 или 888, все они теперь начинаются с 333. Можем ли мы переформатировать телефонные номера и изменить их префиксы одновременно? Конечно, можем:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString,"($1) 333-$3")

Wscript.Echo strNewString

Видите, что мы здесь сделали? Мы просто удалили старый префикс (обратная ссылка $2) в нашем замещающем тексте; на ее место мы поместили жестко закодированное стандартное значение префикса 333. Как будет выглядеть телефонный номер 555-123-4567 после выполнения этого измененного сценария? Он должен выглядеть сильно похожим на следующее:

(555) 333-4567

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

Myer, Ken

Есть ли у нас способ перевернуть это значение и отобразить имя вот так:

Ken Myer

Ну, мы бы выглядели довольно глупо, если б у нас его не было, верно? Вот сценарий, который делает именно это:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\S+), (\S+)"

strSearchString = "Myer, Ken"
strNewString = objRegEx.Replace _

strSearchString,"$2 $1")

Wscript.Echo strNewString

В данном конкретном сценарии мы ищем слово, (\S+), за которым следует запятая, за которой следуют пробел и еще одно слово. (В данном случае мы используем \S+ чтобы представить «слово»). Конструкция \S+ значит любой последовательный набор любых символов, отличных от разделителя. Другими, словами, это может быть буква, число, символ; на деле, практически что угодно, кроме пробела, знака табуляции или символа возврата каретки. Как можно заметить, здесь мы ожидаем найти два подсовпадения: представляющее фамилию ($1) и представляющее имя ($2). В силу этого мы можем отобразить имя пользователя как FirstName LastName, используя следующий синтаксис:

"$2 $1"

Где запятая? Ну, она здесь явно не нужна, так что мы ее выкинули.

Примечание: что забавно, отчего-то мы задумались и о редакторе сценариев тоже. Хм...

Теперь покажем вам еще одну вещь, и рабочий день окончен. (Ну ладно, рабочий месяц.) Она не на 100% надежна; в конце концов, мы не хотим слишком усложнять вводную статью вроде этой. (А регулярные выражения потенциально могут стать очень сложными.) Так или иначе, вот сценарий, который в большинстве случаев удаляет ведущие нули из значений вроде 0000.34500044:

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\b0{1,}\."

strSearchString = _
"The final value was 0000.34500044."
strNewString = objRegEx.Replace _
strSearchString,".")

script.Echo strNewString

Как обычно, это работает только благодаря шаблону: "\b0{1,}\." Мы начинаем с поиска границы слова (\b); это гарантирует, что мы не удалим нули в значении вроде 100.546. Мы затем ищем один или несколько нулей – 0{1,} – за которыми следует знак десятичной дроби (\.). Если этот шаблон найден, мы заменяем эти нули (и знак десятичной дроби), на единственный знак десятичной дроби ".". Если все пойдет по плану, это преобразует нашу строку в следующее:

The final value was ..34500044.

Все время, отведенное нам на этот месяц, истекло. Прежде чем расстаться, мы можем заметить, что не успела краска на картине высохнуть, как Мона Лиза стала предметом существенных споров. Кто эта таинственная женщина? Почему она так улыбается? Почему у нее нет бровей? Некоторые из историков искусства даже предположили, что Мона Лиза вообще не женщина, а картина на деле является автопортретом Леонардо да Винчи. (Если это верно, ему бы стоило найти нового портного.) Между тем, образовательный фонд Unarius Educational Foundation пошел еще дальше, утверждая, что картина на деле является изображением «родственной души» Леонардо в «высшем мире» и что эта родственная душа направляла руку Леонардо. По примечательному совпадению, выпуск «Эй, сценарист!» за этот месяц был написан точно так же.

Это значит, что все претензии следует отправлять родственной душе сценариста, написавшего эту статью, пребывающей в отделении Майкрософт в высшем мире. Спасибо.

Головоломка-сценарий доктора Скрипто.

Задача на этот месяц потребует не только умения решать головоломки, но и навыков создания сценариев.

Май 2008 года: Сценарий-доку

В этом месяце мы играем в судоку, но с неожиданным поворотом. Вместо числе от 1 до 9 в сетке расположены буквы и символы, составляющие командлет Windows PowerShell™. В итоговом решении одна из строк, читаемых поперек, составит имя командлета.

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

ANSWER:

Головоломка-сценарий доктора Скрипто.

Ответ: сценарий-доку, май 2008 года

Программисты корпорации Майкрософт работают... ну хорошо, получают зарплату в корпорации Майкрософт. Когда они не играют в бейсбол, не тренируют бейсбольную команду, не смотрят бейсбол и не занимаются другими делами, они заведуют центром сценариев TechNet. Веб-узел проекта находится по адресу www.scriptingguys.com..

© 2008 Корпорация Майкрософт и компания CMP Media, LLC. Все права защищены; полное или частичное воспроизведение без разрешения запрещено.