Практическое применение регулярных выражений
Помните задачу с повторяющимися словами из первой главы? Я уже говорил, что ее полное решение в языке типа Perl состоит из нескольких строк. Например, оно может выглядеть так:
$/ = ".\n";
while (<>) {
next if !s/\b([a-z]+)((\s|<[^>]+>)+)(\1\b)/\e[7m$1\e[m$2\e[7m$4\e[m/ig;
s/^([^\e]*\n)+//mg; # Удалить непомеченные строки
s/^/$ARGV: /mg; # Начинать строку с имени файла
print;
}
Да, перед вами вся программа.
Я не рассчитываю на то, что вы разберетесь в ней (пока!) Скорее, это пример, выходящий за рамки возможностей egrep и призванный разжечь ваш интерес к регулярным выражениям. Вся основная работа этой программы связана с тремя регулярными выражениями:
\b([a-z]+)((\s|<[^>]+>)+)(\1\b)
^([^\e]*\n)+
^
Конечно, [^] узнать нетрудно, но в остальных выражениях присутствуют элементы, не встречавшиеся в egrep (хотя метасимвол [\b] кратко упоминался на странице <$R[P#,R1-3]> как обозначение границы слова — именно эту функцию он и выполняет здесь). Дело в том, что в Perl и egrep используются разные диалекты регулярных выражений. Отличаются некоторые обозначения, и Perl обладает гораздо более богатым набором метасимволов. Примеры будут приведены в этой главе.
По возможностям работы с регулярными выражениями Perl значительно превосходит egrep. Фрагменты, написанные на Perl, продемонстрируют новые примеры использования регулярных выражений, но что еще важнее, представят их в несколько ином контексте, нежели в egrep. А пока мы познакомимся с похожим (хотя и несколько отличающимся) диалектом регулярных выражений.
В этой главе мы рассмотрим несколько задач-примеров (проверка пользовательского ввода; работа с заголовками электронной почты) и воспользуемся ими для общего обзора регулярных выражений. Вы познакомитесь с языком Perl и некоторыми приемами, используемыми при создании регулярных выражений. Во время путешествия мы будем часто отвлекаться от основной темы и исследовать различные важные концепции.
Впрочем, выбор Perl в данном случае не играет особой роли. С таким же успехом можно было воспользоваться и другими языками (Tcl, Python и даже elisp от GNU Emacs). Я выбрал Perl в основном из-за того, что среди всех распространенных языков он обладает наиболее интегрированной поддержкой регулярных выражений и, возможно, является самым доступным. Кроме того, в Perl существует много вспомогательных конструкций, которые выполняют всю «черную работу» и позволяют нам сконцентрироваться на регулярных выражениях. Вспомните пример с проверкой файлов, описанный на с. <$R[P#,R1-4]>. Для решения этой задачи я воспользовался Perl, а команда выглядела так<$M[R2-7]>:
% perl -0ne 'print "$ARGV\n" if s/ResetSize//ig != s/SetSize//ig' *
(я снова не рассчитываю, что вы поймете эту команду, а лишь надеюсь, что на вас произведет впечатление лаконичность решения).
Я люблю Perl, но не собираюсь уделять ему слишком много внимания. Не забывайте: эта глава посвящена регулярным выражениям. Приведу слова одного профессора, обращенные к студентам-первокурсникам: «Мы изучаем общие концепции компьютерных технологий, но рассматриваем их на примере Pascal» (Pascal — традиционный язык программирования, первоначально разработанный в учебных целях)[1].
Поскольку эта глава не предполагает знания Perl, я в общих чертах представлю этот язык, чтобы вы могли разобраться в приведенных примерах (глава 7, в которой рассматриваются всевозможные нюансы работы Perl, требует некоторых познаний в языке). Даже если вы вам приходилось программировать на разных языках, Perl на первый взгляд выглядит довольно странно; это объясняется компактностью его синтаксиса и семантической многозначностью. Хотя приведенные примеры нельзя назвать «плохими», это далеко не лучшие образцы «Пути Программирования Perl». Ради наглядности я отказался от использования многих возможностей Perl; я постарался представить программы в более общем, приближенном к псевдокоду стиле. Но зато вы увидите некоторые замечательные применения регулярных выражений.
Мощный сценарный язык Perl был создан Ларри Уоллом в конце 1980-х годов на основе идей, почерпнутых из ряда других языков программирования. Многие его концепции обработки текста и регулярных выражений позаимствованы из двух языков, awk и sed, заметно отличающихся от «традиционных» языков типа C или Pascal. Perl существует на многих платформах, включая DOS/Windows, MacOS, OS/2, VMS и Unix. Он в значительной степени ориентирован на обработку текстов и является самым распространенным инструментом для написания сценариев CGI в World Wide Web (программ, которые конструируют и отправляют динамические Web-страницы). Информация о том, как достать копию Perl для вашей системы, приведена в приложении А. На момент написания книги существовал Perl версии 5.003, но примеры этой главы написаны для версии 4.036 и выше[2].
Рассмотрим простой пример:
$celsius = 30;
$fahrenheit = ($celsius * 9 / 5) + 32; # Вычислить температуру
# по шкале Фаренгейта
print "$celsius C is $fahrenheit F.\n"; # Вывести обе температуры
Программа выводит следующий результат:
30 C is 86 F.
Имена простых переменных (таких, как $fahrenheit и $celsius) всегда начинаются со знака доллара и могут содержать число или строку любой длины (в данном примере используются только числа). Комментарии начинаются со знака # и продолжаются до конца строки. Если вы программировали на традиционных языках типа C или Pascal, вероятно, вас больше всего удивит присутствие переменных в строках Perl, заключенных в кавычки. В строке "$celsius C is $fahrenheit F.\n" каждая переменная заменяется своим значением. В данном случае полученная строка затем выводится (символ \n начинает новую строку на экране).
По своим управляющим структурам Perl напоминает другие распространенные языки:
$celsius = 20;
while ($celsius <= 45)
{
$fahrenheit = ($celsius * 9 / 5) + 32; # Вычислить температуру
# по шкале Фаренгейта
print "$celsius C is $fahrenheit F.\n";
$celsius = $celsius + 5;
}
Блок кода, находящийся в теле цикла while, выполняется многократно, пока условие (в данном случае — $celsius <= 45) остается истинным. Если сохранить этот фрагмент в файле (например, temps), его можно запустить из командной строки.
Вот как это выглядит:
% perl -w temps
20C is 68 F.
25 C is 77 F.
30 C is 86 F.
35 C is 95 F.
40 C is 104 F.
45 C is 113 F.
Ключ -w<$M[R2-4]> не является обязательным и вообще не относится к регулярным выражениям. Он приказывает Perl проверить вашу программу и выдать предупреждения по тем аспектам, которые выглядят подозрительно (например, использование неинициализированных переменных и т. д. — заранее объявлять переменные в Perl не требуется). Я использую его лишь потому, что это рекомендуется делать всегда.
В Perl существует несколько способов применения регулярных выражений. Самый простой вариант — проверка совпадения регулярного выражения в тексте, хранящемся в переменной. Следующий фрагмент проверяет содержимое переменной $reply и сообщает, состоит ли она из одних цифр:
if ($reply =~ m/^[0-9]+$/) {
print "only digits\n";
} else {
print "not only digits\n";
}
Первая строка выглядит немного странно. Регулярное выражение состоит из символов [^[0-9]+$], а окружающая конструкция m/…/ сообщает Perl, что с этим выражением нужно сделать. m означает поиск совпадения регулярного выражения в тексте, а символы / определяют границы регулярного выражения. m~ связывает m/…/ со строкой, в которой происходит поиск — в данном случае с содержимым переменной $reply.
Не путайте =~ с = или ==. Оператор == проверяет, равны ли два числа (как вы вскоре увидите, при проверке равенства двух строк применяется оператор eq). Оператор = присваивает значение переменной — например, $celsius = 20. Наконец, оператор =~ связывает команду поиска со строкой, в которой будет происходить поиск (в данном примере использована команда поиска m/^[0-9]+$/, а целевая строка хранится в переменной $reply). Оператор =~ иногда читается как «соответствует», поэтому строку
if ($reply =~ m/^[0-9]+$/){
можно прочитать так:
«Если текст, хранящийся в переменной $reply, соответствует регулярному выражению [^[0-9]+$], то…
Результат выполнения команды $reply =~ m/^[0-9]+$/ равен true, если [^[0-9]+$] совпадает в строке $reply, или false в противном случае. На основании этого значения команда if выбирает выводимое сообщение.
Обратите внимание: результат команды $reply =~ m/[0-9]+/ (той же, что и раньше, но без ограничивающих символов ^ и $) равен true, если $reply содержит хотя бы одну цифру. [^…$] гарантирует, что $reply содержит только цифры.
Давайте объединим два предыдущих примера. Мы предложим пользователю ввести значение и затем проверим его при помощи регулярного выражения, чтобы убедиться в том, что было введено число. Если проверка даст положительный результат, мы вычисляем и выводим эквивалентную температуру по шкале Фаренгейта. В противном случае выводится предупреждающее сообщение:
print "Enter a temperature in Celsius:\n";
$celsius = <STDIN>; # Получить одну строку от пользователя
chop($celsius); # Удалить из $celsius завершающий символ новой строки
if ($celsius =~ m/^[0-9]+$/) {
$fahrenheit = ($celsius * 9 / 5) + 32; # Вычислить температуру
# по шкале Фаренгейта
print "$celsius C = $fahrenheit F\n";
} else {
print "Expecting a number, so don't understand \"$celsius\".\n";
}
Обратите внимание на префикс \ перед кавычками в завершающей команде print. Здесь он выполняет ту же функцию, как и перед метасимволами в регулярных выражениях. В разделе «Многоликие метасимволы» (см. с. <$R[P#,R2-8]>) эта тема рассматривается чуть подробнее.
Предположим, программа хранится в файле c2f. При запуске она выводит следующий результат:
% perl -w c2f
Enter a temperature in Celsius:
22
22 С = 71.599999999999994316 F
Что-то неладно. Оказывается, простая команда print плохо подходит для вывода вещественных чисел. Чтобы не увязнуть в технических деталях Perl, я не стану вдаваться в подробные объяснения и просто скажу, что для улучшения внешнего вида результата можно воспользоваться командой printf (аналогом команды printf языка C или команд format языков Pascal, Tcl, elisp и Python):
printf "%.2f C = %.2f F\n", $celsius, $fahrenheit;
Команда изменяет не значения переменных, а лишь их внешний вид при выводе. Теперь результат выглядит так:
Enter a temperature in Celsius:
22
22.00 С = 71.60 F
Смотрится гораздо лучше.
Пожалуй, наш пример было бы неплохо усовершенствовать, чтобы он поддерживал отрицательные и дробные значения температуры. С точки зрения математики ничего не изменится — обычно Perl не различает целые и вещественные числа. Тем не менее, мы должны модифицировать регулярное выражение, чтобы оно разрешало ввод отрицательных и вещественных величин. Вставка [-?] разрешит наличие минуса в начале отрицательного числа. А если сделать еще один шаг и вставить [[-+]?], вы разрешите и начальный плюс для положительных чисел.
Чтобы регулярное выражение поддерживало наличие необязательной дробной части, в него добавляется [(\.[0-9]*)?]. Комбинация [\.] совпадает с обычным символом «точка», поэтому выражение [\.[0-9]*] совпадает с точкой, за которой следует любое количество необязательных цифр. Поскольку подвыражение [\.[0-9]*] заключается в конструкцию [(…)?], оно как единое целое становится необязательным (и этим принципиально отличается от выражения [\.?[0-9]*], допускающего совпадение дополнительных цифр при отсутствии совпадения [\.]).
Объединяя все сказанное, мы получаем:
if ($celsius =~ m/^[-+]?[0-9]+(\.[0-9]*)?$/) {
Команда пропускает такие числа, как 32, -3.723 и +98.6. В действительности она не идеальна, поскольку не поддерживает чисел, начинающихся с десятичной точки (например, .357). Разумеется, пользователь может добавить начальный ноль (то есть 0.357), поэтому вряд ли это можно считать крупным недостатком. Проблема вещественных чисел ставит ряд интересных задач, подробно рассматриваемых в главе 4 (см. с. <$R[P#,R4-32]>).
Давайте расширим наш пример, чтобы пользователь мог вводить температуру как по Цельсию, так и по Фаренгейту. В зависимости от выбранной шкалы к введенному значению присоединяется суффикс C или F. Чтобы суффикс прошел через регулярное выражение, после выражения для числа просто добавляется [[CF]]. Однако мы должны изменить саму программу так, чтобы она определяла шкалу введенной температуры и вычисляла значение по другой шкале.
В Perl, в отличие от многих языков с поддержкой регулярных выражений, существует набор полезных специальных переменных для ссылок на текст, совпавший с подвыражениями в круглых скобках. В первой главе говорилось о том, что некоторые версии egrep поддерживают метасимволы [\1], [\2], [\3] на уровне самих регулярных выражений. Perl также поддерживает эти метасимволы и добавляет к ним переменные, которые могут использоваться за пределами регулярного выражения после выполнения поиска. Этим переменным Perl присваиваются имена $1, $2, $3 и т. д. Хотя на первый взгляд они выглядят немного странно, это действительно переменные. Просто имена переменных представляют собой числа. Perl присваивает им значения при каждом успешном совпадении регулярного выражения. На всякий случай повторю: метасимвол [\1] используется в регулярном выражении для ссылки на текст, совпавший ранее в ходе текущей попытки найти совпадение, а переменная $1 может использоваться в программе для ссылки на этот текст после успешного совпадения.
Чтобы не загромождать пример и сосредоточиться на новых аспектах, я временно удалю подвыражение для идентификации дробной части, но мы к нему еще вернемся. Итак, чтобы увидеть переменную $1 в действии, сравните:
$celsius =~ m/^[-+]?[0-9]+[CF]$/
$celsius =~ m/^([-+]?[0-9]+)([CF])$/
Изменяют ли новые круглые скобки общий смысл выражения? Чтобы ответить на этот вопрос, мы должны знать:
l Обеспечивают ли они группировку символов для * или других квантификаторов?
l Ограничивают ли они конструкцию [|]?
Рис. 2.1. Сохранение текста в круглых скобках
На оба вопроса ответ отрицательный, поэтому совпадение всего выражения остается неизменным. Однако скобки заключают два подвыражения, совпадающие с «интересными» частями проверяемой строки. Как видно из рис. 2.1, в $1 сохраняется введенное число, а в $2 — суффикс (C или F). Из блок-схемы на рис. 2.2 видно, что это позволяет нам легко выбрать дальнейшие действия после применения регулярного выражения.
Если программа, приведенная ниже, находится в файле convert, то пример ее использования может выглядеть так:
% perl -w convert
Enter a temperature (i.e. 32F, 100C):
39F
3.89 C = 39.00 F
% perl -w convert
Enter a temperature (i.e. 32F, 100C):
39C
39.00 C = 102.20 F
% perl -w convert
Enter a temperature (i.e. 32F, 100C):
oops
3.89 C = 39.00 F
Expecting a number, so don't understand "oops".
Рис. 2.2. Блок-схема программы преобразования температур
print "Enter a temperature (i.e. 32F, 100C):\n";
$input = <STDIN>; # Получить одну строку от пользователя
chop($input); # Удалить из $input завершающий символ новой строки
if ($input =~ m/^([-+]?[0-9]+)([CF])$/)
{
# Обнаружено совпадение. $1 содержит число, $2 - символ "C" или "F"
$InputNum = $1; # сохранить в именованных переменных,
$type = $2; # чтобы программу было легче читать
if ($type eq "C") { # 'eq' проверяет равенство двух строк
# Температура введена по Цельсию, вычислить по Фаренгейту
$celsius = $InputNum;
$fahrenheit = ($celsius * 9 / 5) + 32;
} else {
# Видимо, температура введена по Фаренгейту.
# Вычислить по Цельсию.
$fahrenheit = $InputNum;
$celsius = ($fahrenheit * 9 / 5) + 32;
}
# Известны обе температуры, переходим к выводу результатов
printf “%.2а С = %.2f F\n”, ;celsius, ;faahrenheit;
} else {
# Регулярное выражение не совпало, вывести предупреждение.
print "Expecting a number, so don't understand \"$input\".\n";
}
Структура нашей программы выглядит так:
if (проверка условия) {
.
.
.
… БОЛЬШОЙ БЛОК, если результат проверки был истинным…
.
.
.
} else {
… Совсем маленький блок, если результат был ложным.
}
Любой студент, изучающий структурное программирование, знает (или должен знать): если команда if состоит из двух ветвей, короткой и длинной, по возможности желательно начать с короткой ветви. При этом else располагается ближе к if, что упрощает чтение программы.
В нашей программе для этого придется заменить проверяемое условие противоположным. Короткой ветвью является «отсутствие совпадения», поэтому условие должно быть истинным при отсутствии совпадения. Одно из возможных решений заключается в замене оператора =~ оператором !~:
$input !~ m/^([-+]?[0-9]+)([CF])$/
Регулярное выражение и проверяемая строка остаются без изменений. Единственное отличие заключается в том, что результат всей комбинации равен false, если регулярное выражение совпадает в строке, и true в обратном случае. При совпадении переменным $1, $2 и т. д. по-прежнему присваиваются значения. Следовательно, эта часть программы принимает следующий вид:
if ($input !~ m/^([-+]?[0-9]+)([CF])$/) {
print "Expecting a number, so don't understand \"$input\".\n";
} else {
# Обнаружено совпадение. $1 содержит число, $2 - символ "C" или "F"
.
.
.
}
В таких нетривиальных языках программирования, как Perl, применение регулярных выражений может переплетаться с логикой самой программы. Давайте внесем в нашу программу три полезных изменения: обеспечим поддержку вещественных чисел, как это было сделано ранее, поддержим ввод суффиксов f и c в нижнем регистре и разрешим, чтобы число отделялось от буквы пробелами. После внесения всех изменений можно будет вводить данные вида 98.6spcf.
Вы уже знаете, как обеспечить возможность ввода вещественных чисел — для этого в выражение включается конструкция [(\.[0-9]*)?]:
if ($input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)([CF])$/)
Обратите внимание: вставка происходит внутри первой пары круглых скобок. Поскольку первые скобки используются для сохранения числа, в них также должна быть включена его дробная часть. Однако появление новой пары круглых скобок, пусть даже предназначенных только для применения квантификатора, приводит к побочному эффекту — содержимое этих скобок также сохраняется в переменной. Поскольку открывающая скобка является второй слева в выражении, дробная часть числа сохраняется в $2. Сказанное поясняет рис. 2.3.
Рис. 2.3. Вложенные круглые скобки
Появление новых круглых скобок в предшествующей части выражения не изменяет смысла выражения [[CF]] напрямую, но отражается на нем косвенно. Дело в том, что круглые скобки, в которые заключено это подвыражение, становятся третьей парой, а это означает, что в присваивании $type необходимо указать $3 вместо $2.
Пробелы между числом и суффиксом вызывают меньше проблем. Мы знаем, что обычный пробел в регулярном выражении соответствует ровно одному пробелу в тексте, поэтому используем [spc*], чтобы разрешить любое количество пробелов (ни один из которых не является обязательным):
if ($input =~ m/^([-+]?[0-9]+(\.[0-9]*)?) *([CF])$/)
Такое решение обеспечивает определенную гибкость, но раз уж мы стремимся создать пример, пригодный для практического использования, давайте включим в регулярное выражение поддержку других разновидностей пропусков. Одной из распространенных разновидностей пропусков являются символы табуляции. Конечно, конструкция [tab*] не позволит использовать пробелы, поэтому мы должны сконструировать символьный класс для обоих разновидностей пропусков: [[spctab]*]. Кстати, чем это выражение принципиально отличается от [[spc*|tab*]]? ref<$M[R2-1]>Подумайте над этим вопросом, затем переверните страницу и проверьте свой ответ.
В этой книге пробелы и табуляции легко различаются благодаря использованным мною обозначениям spc и tab. К сожалению, на экране различить их сложнее. Если вы увидите что-нибудь вроде [ ]*, логично предположить, что это пробел и символ табуляции, но полностью уверенным в этом быть нельзя. Для удобства в регулярных выражениях Perl существует метасимвол [\t]. Он просто совпадает с символом табуляции — единственным преимуществом этого символа перед литералом является его визуальная наглядность, и я часто использую его в своих выражениях. Таким образом, [[spctab]*] превращается в [[spc\t]*].
Существуют и другие вспомогательные метасимволы — [\n] (новая строка), [\f] (подача листа) и [\b] (забой). Постойте-ка! Ведь раньше я говорил, что [\b] означает границу слова. Так какое же из двух значений соответствует символу? Как ни странно, оба!
<$M[R2-8]>Символ \n уже встречался нам в предыдущих примерах, однако он находился внутри строки, а не в регулярном выражении. Строки Perl обладают собственными метасимволами, которые не имеют ничего общего с метасимволами регулярных выражений. Программисты-новички очень часто путают их.
Однако выясняется, что некоторые строковые метасимволы очень похожи на аналогичные метасимволы регулярных выражений. Например, при помощи строкового метасимвола \t можно вставить символ табуляции в строку, а при помощи метасимвола регулярных выражений [\t] в выражение вставляется элемент, совпадающий с символом табуляции.
Такое сходство удобно, но мне даже трудно объяснить, как важно отличать разные типы метасимволов. В простейших случаях вроде \t это кажется несущественным, но, как будет показано при описании различных языков и программ, умение различать метасимволы, используемые в каждой ситуации, играет очень важную роль.
На самом деле мы уже встречались с несколькими случаями конфликтов метасимволов. В главе 1 при работе с egrep регулярные выражения обычно заключались в апострофы. Все регулярное выражение передается в приглашении командной строки, и интерпретатор распознает некоторые из своих метасимволов. Например, для него пробел является метасимволом, отделяющим команду от аргументов, а аргументы — друг от друга. Во многих интерпретаторах апострофы являются метасимволами, которые сообщают интерпретатору о том, что в заключенном между ними тексте не следует распознавать другие метасимволы (в DOS для этой цели используются кавычки).
Апострофы позволяют включать в регулярное выражение пробелы. Без апострофов командный интерпретатор самостоятельно интерпретирует пробелы вместо того, чтобы предоставить egrep возможность интерпретировать их по-своему. Во многих командных интерпретаторах также используются метасимволы $, *, ? и т. д., часто встречающиеся в регулярных выражениях.
Весь этот разговор о метасимволах командного интерпретатора и строковых метасимволах Perl не имеет прямого отношения к регулярным выражениям, но непосредственно связан с применением регулярных выражений в реальных ситуациях. В этой книге нам встретятся многочисленные и порой довольно сложные ситуации, в которых используется многоуровневое взаимодействие метасимволов.
Так как же насчет [\b]? В Perl этот метасимвол обычно совпадает с границей слова, но в символьном классе ему соответствует символ «забой». Граница слова в символьном классе не имеет смысла, поэтому Perl имеет полное право присвоить этому метасимволу какой-то другой смысл. Предупреждение из главы 1 о том, что «субязык» символьных классов отличается от основного языка регулярных выражений, несомненно, относится и к Perl (а также любому другому диалекту регулярных выражений).
чем [[spctab]*] отличается от [[spc*|tab*]]?
bref Ответ на вопрос со с. <$R[P#,R2-1]>
Выражение [(spc*|tab*)] совпадает либо с [[spc*], либо с [tab*]. Иначе говоря, в тексте должны стоять либо пробелы (или ничего), либо символы табуляции (или ничего). Однако это выражение не позволяет комбинировать пробелы с символами табуляции.
[[spctab]*] означает [[spctab]], повторенное любое количество раз. В строке «tabspcspc» оно совпадает трижды — сначала с символом табуляции, а затем с пробелами.
Выражение [[spctab]*]] логически эквивалентно [(spc|tab)*], хотя по причинам, объясненным в главе 4, символьный класс часто работает более эффективно.
При рассмотрении пропусков мы остановились на регулярном выражении [[spc\t]*]. Это неплохо, однако в регулярных выражениях Perl снова находится удобная сокращенная запись. В отличие от метасимвола [\t], который просто представляет литерал-табуляцию, метасимвол [\s] обеспечивает сокращенную запись для целого символьного класса, означающего «любой пропуск». К этому понятию относятся пробелы, табуляции, символы новой строки и возврата курсора. В нашем примере символы новой строки и возврата курсора не используются, однако вводить выражение [\s*] удобнее, чем [[spc\t]*]. Вскоре вы привыкнете к этому метасимволу и будете легко узнавать конструкцию [\s*] даже в сложных выражениях.
Наша команда теперь выглядит так:
$input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CF])$/
Наконец, суффикс должен вводиться не только в верхнем, но и в нижнем регистре. Задача решается включением символов нижнего регистра в символьный класс: [CFcf]. Впрочем, я хочу продемонстрировать еще один способ:
$input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CF])$/i
Ключ<$M[R2-9]> i является модификатором. Его присутствие после m/…/ сообщает Perl о том, что поиск должен осуществляться без учета регистра символов. Символ i является частью не регулярного выражения, а синтаксической оболочки m/…/, и указывает Perl, как именно должно обрабатываться указанное регулярное выражение. Впрочем, постоянно говорить «модификатор i» неудобно, поэтому обычно используется обозначение /i (хотя при использовании модификатора дополнительный символ / не нужен). На страницах книги вам встретятся и другие модификаторы, в том числе модификатор глобального поиска /g в этой главе.
Попробуем запустить новую версию программы:
% perl -w convert
Enter a temperature (i.e. 32F, 100C):
32 f
0.00 C = 32.00 F
% perl -w convert
Enter a temperature (i.e. 32F, 100C):
50 c
10.00 C = 50.00 F
Стоп! При втором запуске мы ввели 50 по Цельсию, которые почему-то были интерпретированы как 50 по Фаренгейту! Просмотрите текст программы. Вы видите, почему это произошло?
if ($input =~ m/^([-+]?[0-9]+(\.[0-9]*)?)\s*([CF])$/i)
{
.
.
.
$type = $3; # Использование $type вместо $3 упрощает чтение программы
if ($type eq "C") { # 'eq' проверяет равенство двух строк
.
.
.
} else {
.
.
.
Мы изменили регулярное выражение, чтобы оно допускало суффикс f в нижнем регистре, но забыли предусмотреть его обработку в оставшейся части программы. Сейчас, если $type не содержит в точности символ C, мы предполагаем, что пользователь ввел температуру по Фаренгейту. Поскольку шкала Цельсия также может обозначаться символом c, необходимо изменить условие проверки $type[3]:
if ($type eq "C" or $type eq "с") {
Теперь программа работает так, как нужно. Приведенные примеры показывают, что применение регулярных выражений может быть взаимосвязано с остальной программой.
Кстати, для проверки символа c также можно было воспользоваться регулярным выражением. Как бы вы это сделали?ref<$M[R2-2]>Переверните страницу и проверьте свой ответ.
Хотя эта глава началась с ускоренного описания Perl, стоит ненадолго отвлечься и особо выделить некоторые аспекты, относящиеся к регулярным выражениям:
l Регулярные выражения Perl отличаются от регулярных выражений egrep; почти каждая программа поддерживает собственный диалект регулярных выражений. Регулярные выражения Perl относятся к тому же типу, но обладают расширенным набором метасимволов.
l Для поиска совпадений регулярного выражения в строке используется конструкция $variable =~ m/…/. Символ m определяет операцию поиска, а символы / ограничивают регулярное выражение (не являясь его частью). Результатом проверки является логическая величина, true или false.
l Концепция метасимволов (символов с особой интерпретацией) не является уникальной особенностью регулярных выражений. Как говорилось выше при обсуждении командных интерпретаторов и строк, заключенных в кавычки, подобная интерпретация встречается во многих контекстах. Знание различных контекстов (командные интерпретаторы, регулярные выражения, строки и т. д.), их метасимволов и возможностей окажет существенную помощь при изучении Perl, Tcl, GNU Emacs, awk, Python и других нетривиальных сценарных языков.
Один из способов проверки односимвольной переменной
bref Ответ на вопрос со с. <$R[P#,R2-2]>
Символ c можно проверить командой $type =~ m/^[cC]$/. Поскольку переменная $type заведомо содержит ровно один символ, в данном случае конструкция [^…$] не обязательна. И если уж на то пошло, можно даже воспользоваться командой $type =~ m/c/i, чтобы модификатор /i выполнил часть работы за нас.
Простота непосредственной проверки символов c и C наводит на мысль, что применение регулярного выражения для этой цели не оправдано. С другой стороны, если бы нам потребовалось обнаружить произвольную комбинацию регистра символов в слове «celsius», то применение регулярного выражения было бы намного логичнее прямого перебора celsius, Celsius, CeLsIuS и т. д. (128 возможных комбинаций!)
По мере изучения Perl вы узнаете и другие, более удачные решения — например, команду lc($type) eq "celsius".
l К числу самых полезных метасимволов, поддерживаемых в регулярных выражениях Perl, принадлежат следующие (некоторые из приведенных метасимволов еще не упоминались в книге):<$M[R2-5]>
\t |
символ табуляции |
\n |
символ новой строки |
\r |
символ возврата курсора |
\s |
класс, совпадающих с любым «пропускным» символом (пробел, табуляция, новая строка, подача листа и т. д.) |
\S |
все, что не относится к [\s] |
\w |
[[a-zA-Z0-9_]] (часто используется в конструкции [\w+] для поиска слов) |
\W |
все, что не относится к [\w], то есть [[^a-zA-Z0-9_]]. |
\d |
[[0-9]], то есть цифра |
\D |
все, что не относится к [\d], то есть [[^0-9]] |
l При наличии модификатора /i поиск производится без учета регистра символов. Хотя в тексте обычно используется обозначение «/i», после завершающего ограничителя в действительности ставится только символ i.
l При обнаружении совпадения в переменные $1, $2, $3 заносится текст, совпавший с соответствующими подвыражениями в круглых скобках. Подвыражения нумеруются в соответствии с позицией открывающих круглых скобок слева направо, начиная с 1. Подвыражения могут быть вложенными — например, [(UnitedspcStates(spcofspcAmerica)?)].
Круглые скобки могут применяться только для группировки, но и в этом случае соответствующая специальная переменная заполняется текстом совпадения. С другой стороны, они часто используются специально для присваивания значений нумерованным переменным. Этот способ позволяет извлекать данные из строки при помощи регулярных выражений.
Во всех примерах, рассмотренных выше, мы ограничивались поиском, и, в отдельных случаях, «извлечением» информации из строки. Сейчас мы перейдем к подстановке, или поиску с заменой — возможности, поддерживаемой Perl и многими другими программами.
Как вы уже знаете, конструкция $var =~ m/выражение/ сопоставляет регулярное выражение с содержимым переменной и возвращает true или false в зависимости от результата. Аналогичная конструкция, $var =~ s/выражение/замена/, представляет собой следующий шаг в работе с текстом: если регулярное выражение совпадает в строке $var, фактически совпавший текст заменяется заданной строкой. При этом используется такое же регулярное выражение, как и в m/…/, но строка замены (между средним и последним символом /) интерпретируется по правилам строк, заключенных в кавычки. Это означает, что она может содержать ссылки на переменные (в частности, очень удобно использовать переменные $1, $2 и т. д., ссылающиеся на компоненты найденного совпадения).
Таким образом, конструкция $var =~ s/…/…/ изменяет значение переменной $var (впрочем, если совпадение не обнаружено, замена не производится и переменная сохраняет прежнее значение). Например, если переменная $var содержит строку JeffspcFriedl, то при выполнении команды
$var =~ s/Jeff/Jeffrey/;
в переменную $var заносится текст JeffreyspcFriedl. Но если переменная $var изначально содержала строку JeffreyspcFriedl, то после выполнения этой команды она примет вид JeffreyreyspcFriedl. Обычно при подобных заменах используются метасимволы границ слов. Как упоминалось в первой главе, некоторые версии egrep поддерживают метасимволы [\<] и [\>] для обозначения начала и конца слова. В Perl для этой цели существует универсальный метасимвол [\b]:
$var =~ s/\bJeff\b/Jeffrey/;
А теперь каверзный вопрос — в конструкции s/…/…/, как и в конструкции m/…/, могут использоваться модификаторы (например, /i). К какому результату приводит выполнение следующей команды:
$var =~ s/\bJeff\b/Jeff/i;
ref<$M[R2-3]>Переверните страницу и проверьте свой ответ.
Рассмотрим несерьезный пример, демонстрирующий использование переменной в строке замены. Предположим, вы используете следующий шаблон письма<$M[R2-10]>:
Dear <FIRST>,
You have been chosen to win a brand new <TRINKET>! Free!
Could you use another <TRINKET> in the <FAMILY> household?
Yes <SUCKER>, I bet you could! Just respond by.....
Чтобы заполнить шаблон данными конкретного получателя, вы присваиваете переменным необходимые значения:
$given = 'Tom';
$family = 'Cruise';
$wunderprize = '100% genuine faux diamond';
Что делает команда $var =~ s/\bJeff\b/Jeff/i?
bref Ответ на вопрос со с. <$R[P#,R2-3]>
Из-за того, как сформулирован этот вопрос, он действительно может оказаться каверзным. Если бы регулярное выражение имело вид [\bJEFF\b], [\bjeff\b] или даже [\bjEfF\b], смысл этой команды был бы очевиден. При наличии модификатора /i слово «Jeff» ищется без учета регистра символов. После этого оно заменяется словом «Jeff», записанным именно этими символами (модификатор /i не влияет на текст замены, хотя существуют и другие модификаторы, о которых речь пойдет в главе 7).
После подготовки шаблон заполняется следующими командами:
$letter =~ s/<FIRST>/$given/g;
$letter =~ s/<FAMILY>/$family/g;
$letter =~ s/<SUCKER>/$given $family/g;
$letter =~ s/<TRINKET>/fabulous $wunderprize/g;
Каждая команда ищет в тексте простой маркер, и обнаружив его, заменяет текстом, который должен присутствовать в итоговом сообщении. В первых двух командах текст замены просто берется из переменной (по аналогии со строкой, заключенной в кавычки и содержащей только имя переменной — например, "$given"). Третья команда заменяет совпавший текст эквивалентом "$given $family", а четвертая — строкой "fabulous $wunderprize". Если вы пишете всего одно сообщение, эти переменные можно опустить и непосредственно ввести нужный текст, но показанный метод позволяет автоматизировать процесс рассылки (например, если данные получателей загружаются из списка).
Модификатор «глобального поиска» /g нам еще не встречался. Он говорит о том , что после первой подстановки команда s/…/…/ должна пытаться обнаружить другие совпадения (и произвести дополнительные замены). Модификатор /g необходим, если вы хотите, чтобы каждая команда производила все возможные замены, не ограничиваясь первым найденным совпадением.
Результат вполне предсказуем:
Dear Tom,
You have been chosen to win a brand new fabulous 100% genuine faux diamond! Free!
Could you use another fabulous 100% genuine faux diamond in the Cruise household?
Yes Tom Cruise, I bet you could! Just respond by.....
Рассмотрим другой пример<$M[R2-6]> — проблему, с которой я столкнулся во время написания на Perl программы для работы с биржевыми котировками. Я получал котировки вида «9.0500000037272». Конечно, настоящее значение было равно 9.05, но из-за особенностей внутреннего представления вещественных чисел Perl выводил их в таком виде, если при выводе не использовалось форматирование. В обычной ситуации я бы просто воспользовался функцией printf для вывода числа с двумя разрядами в дробной части, как в примере с преобразованием температур, но в данном случае этот вариант не годился. Дело в том, что дробная часть, равная 1/8, должна выводиться в виде «.125», и в этом случае необходимы три цифры вместо двух.
В формальном виде моя задача звучала так: «Всегда выводить первые две цифры дробной части, а третью — лишь в том случае, если она не равна нулю. Все остальные цифры игнорируются». В результате число 12.3750000000392 или уже правильное 12.375 возвращается в виде «12.375», а 37.500 сокращается до «37.50». Именно то, что нам требовалось.
Но как реализовать это требование? Строка, в которой выполняется поиск, хранится в переменной $price, поэтому мы воспользуемся командой $price =~ s/(\.\d\d[1-9]?)\d*/$1/. Начальная конструкция [\.] нужна для того, чтобы совпадение начиналось с десятичной точки. Затем два метасимвола [\d\d] совпадают с двумя цифрами, следующими за точкой. Подвыражение [[1-9]?] совпадает c ненулевой цифрой, если она следует за первыми двумя. Все совпавшие до настоящего момента символы образуют часть, которую мы хотим оставить, поэтому они заключаются в круглые скобки для сохранения в переменной $1. Затем значение $1 используется в строке замены. Если ничего больше не совпало, строка заменяется сама собой — не слишком интересно. Однако вне круглых скобок $1 продолжается поиск других символов. Эти символы не включаются в строку замены и поэтому они фактически удаляются из числа. В данном случае «удаляемый» текст состоит из всех дальнейших цифр, то есть [\d*] в конце регулярного выражения.
Запомните этот пример. Мы вернемся к нему в главе 4, при изучении механизма поиска совпадений. Из этого примера следует ряд интересных уроков.
Во время работы над этой главой я столкнулся еще с одним простым, но вполне реальным примером. Я подключился к компьютеру на другом берегу Тихого океана, причем сеть работала на редкость медленно. Реакция на простое нажатие клавиши Enter следовала только через минуту. Я должен был внести несколько мелких исправлений в файл, чтобы заработала важная программа. Все, что мне требовалось — заменить в файле каждый вызов sysread на read. Исправлений было немного, но при такой замедленной реакции сама идея запуска полноэкранного редактора выглядела безумием.
Приведу команду, которая автоматически внесла все необходимые изменения:
% perl -p -i -e 's/sysread/read/g' имя_файла
Команда выполняет программу Perl s/sysread/read/g (да, это действительно целая программа — об этом свидетельствует ключ -e, после которого в командной строке следует программа). Кроме того, в командной строке указаны ключи Perl -p и -i, а также имя файла, с которым работает программа. При такой комбинации ключей подстановка выполняется в каждой строке файла, а после завершения подстановки результаты записываются обратно в файл.
Обратите внимание: в команде не указано имя переменной (var =~…), поскольку заданные ключи обеспечивают ее неявное применение последовательно к каждой строке файла. Кроме того, ключ /g гарантировал замену всех экземпляров в одной строке.
Хотя я применил эту программу к одному файлу, с таким же успехом можно было указать в командной строке имена нескольких файлов, и Perl применил бы подстановку к каждой строке каждого файла. В этом случае одна простая команда произвела бы массовое редактирование в множестве файлов.
Рассмотрим еще один пример. Допустим, у нас имеется файл с сообщением электронной почты, и мы хотим подготовить файл с ответом. В процессе подготовки каждая строка исходного сообщения должна быть процитирована в ответе, чтобы мы могли легко вставить свой текст в каждую часть сообщения. Кроме того, необходимо удалить из заголовка исходного сообщения лишние строки, а также подготовить заголовок ответного сообщения.
Предположим, сообщение выглядит так:
From elvis Thu Feb 29 11:15 1997
Received: from elvis@localhost by tabloid.org (6.2.12) id KA8CMY
Received: from tabloid.org by gateway.net.net (8.6.5/2) id N8XBK
Received: from gateway.net.net Thu Feb 29 11:16 1997
To:jfriedl@ora.com (Jeffrey Friedl)
From: elvis@tabloid.org (The King)
Date: Thu, Feb 29 1997 11:15
Message-Id: <1997022939939.KA8CMY@tabloid.org>
Subject: Be seein' ya around
Content-type: text
Reply-To: elvis@hh.tabloid.org
X-Mailer: Madam Zelda's Psychic Orb [version 2.4 PL23]
Sorry I haven't been around lately. A few years back I checked
into that ole heartbreak hotel in the sky, ifyaknowwhatImean.
The Duke says "hi".
Elvis
Некоторые поля заголовка (дата, тема и т. д.) представляют интерес для нас, но остальные поля необходимо удалить. Если сценарий, который мы собираемся написать, называется mkreply, а исходное сообщение хранится в файле king.in, шаблон ответа строится следующей командой:
% perl -w mkreply king.in > king.out
(На всякий случай напоминаю — ключ -w включает выдачу предупреждений; см. с. <$R[P#,R2-4]>)
Мы хотим, чтобы итоговый файл king.out выглядел примерно так:
To: elvis@hh.tabloid.org (The King)
From: jfriedl@ora.com ((Jeffrey Friedl)
Subject: Re: Be seein' ya around
On Thu, Feb 29 1997 11:15 The King wrote:
|> Sorry I haven't been around lately. A few years back I checked
|> into that ole heartbreak hotel in the sky, ifyaknowwhatImean.
|> The Duke says "hi".
|> Elvis
Проанализируем постановку задачи. Для построения нового заголовка необходимо знать адрес получателя (в данном случае elvis@hh.tabloid.org), настоящее имя получателя (The King), имя и адрес отправителя, а также тему. Кроме того, для вывода вступительной строки перед телом сообщения необходимо знать дату.
Работа делится на три фазы:
l Извлечение данных из заголовка сообщения
l Вывод заголовка ответа
l Вывод строк сообщения с префиксом |>spc.
Пожалуй, я немного опережаю события — чтобы обработать данные, необходимо сначала прочитать их в программе. К счастью, в Perl эта задача легко решается при помощи волшебного оператора <>. Когда эта странная конструкция присваивается обычной переменной, она содержит следующую строку входных данных. Входные данные берутся из файлов, имена которых передаются сценарию Perl в командной строке (файл king.in в приведенном примере).
Не путайте оператор <> с командой перенаправления данных > имя_файла командного интерпретатора или операторами Perl >=/<=. Это всего лишь своеобразный аналог функции getline() в языке Perl.
После того, как все входные данные будут прочитаны, <> возвращает удобное неопределенное значение (интерпретируемое как логическое значение false), поэтому при обработке файлов часто применяется следующая конструкция:
while ($line = <>) {
…работаем с $line…
}
Похожая конструкция будет использована и в нашей задаче, но в соответствии с ее постановкой заголовок необходимо обрабатывать отдельно. Заголовок состоит из всех строк, находящихся перед первой пустой строкой; далее следует тело сообщения. Чтобы ограничиться чтением заголовка, можно воспользоваться следующим фрагментом:
# Обработка заголовка
while ($line = <>) {
if ($line =~ m/^\s*$/) {
last; # Прервать цикл while, продолжить ниже
}
# Обработать строку заголовка
}
# Обработка прочих строк сообщения
.
.
.
Поиск пустой строки, завершающей заголовок, осуществляется при помощи регулярного выражения [^\s$]. Выражение ищет начало строки (имеется у всех строк), за которым следует любое количество пропускных символов (хотя на самом деле их быть не должно), после чего строка завершается. При этом нельзя воспользоваться простой командой $line eq "" по причинам, которые будут объяснены ниже. Ключевое слово last прерывает цикл while, останавливая обработку заголовка.
Итак, внутри цикла, после проверки пустой строки, со строкой заголовка можно сделать все, что потребуется. Мы должны выделить из нее такие данные, как тема и дата сообщения.
Для выделения темы используется распространенный прием, который мы будем часто использовать в будущем:
if ($line =! m/^Subject: (.*)/) {
$subject = $1;
}
В этом фрагменте мы ищем строки, начинающиеся с символов «Subject:spc». После того, как эта часть выражения совпадет, следующее подвыражение [.*] совпадает со всеми остальными символами в строке. Поскольку подвыражение [.*] заключено в круглые скобки, тема сообщения заносится в переменную $1. В нашем примере содержимое этой переменной просто сохраняется в переменной $subject. Конечно, если регулярное выражение не совпадает в строке (а в большинстве строк дело обстоит именно так), условие команды if не выполняется, и для этой строки значение переменной $subject не присваивается.
Аналогично происходит поиск полей Date и Reply-To:
if ($line =! m/^Date: (.*)/) {
$date = $1;
}
if ($line =! m/^Reply-To: (.*)/) {
$reply_address = $1;
}
С полем From: придется потрудиться побольше. Во-первых, нам нужна строка, которая начинается с «From:», а не первая строка, начинающаяся с «Fromspc». Речь идет о строке
From: elvis@tabloid.org (The King)
В ней содержится адрес, а также имя отправителя, заключенное в круглые скобки. Начнем с выделения имени.
Чтобы пропустить адрес, мы используем выражение [^From:spc(\S+)]. Как упоминалось выше, [\S] совпадает со всеми символами, которые не являются пропусками (см. с. <$R[P#,R2-5]>), поэтому [\S+] совпадает до первого пропуска (или до конца целевого текста). В данном случае это адрес отправителя. После успешного совпадения мы хотим выделить текст, заключенный в круглые скобки. Конечно, для этого нужно сначала найти сами скобки, поэтому мы используем конструкцию [\(…\)] (экранируя скобки для того, чтобы отменить их особую метасимвольную интерпретацию). Внутри скобок требуется найти все символы — за исключением других круглых скобок! Задача решается выражением [[^()]*] (вспомните: метасимволы символьных классов отличаются от метасимволов «обычных» регулярных выражений; внутри символьного класса круглые скобки не имеют специальной интерпретации, поэтому экранировать их не нужно).
Будьте внимательны с [.*]
Выражение [.*] часто обозначает «последовательность любых символов», поскольку точка может совпадать с чем угодно (или в некоторых программах, обычно включая Perl — с чем угодно, кроме символа новой строки и/или нуль-символа). Звездочка же означает, что допускается любое количество символов, но ни один не является обязательным. Такая конструкция весьма полезна.
Однако в ней таятся «подводные камни», с которыми может столкнуться пользователь, не понимающий применения этой конструкции в контексте большого выражения. Данная тема подробно рассматривается в главе 4.
Итак, объединив все сказанное, мы получаем:
[^From:spc(\S+)spc\(([^()]*)\)]
Чтобы вы не запутались в многочисленных круглых скобках, на рис. 2.4 структура этого выражения изображена более наглядно.
Рис. 2.4. Вложенные круглые скобки; $1 и $2
Когда регулярное выражение, показанное на рис. 2.4, совпадает, имя отправителя сохраняется в переменной $2, а возможный адрес отправителя — в переменной $1:
if ($line =! m/^From: (\S+) \(([^()]*)\)/) {
$reply_address = $1;
$from_name = $2;
}
Не все сообщения электронной почты содержат строку заголовка Reply-To, поэтому содержимое $1 используется в качестве предварительного обратного адреса. Если позднее в заголовке найдется поле Reply-To, переменной $reply_address будет присвоено новой значение. В результате фрагмент обработки заголовка выглядит так:
while ($line = <>) {
if ($line =~ m/^\s*$/ ) {
last; # Прервать цикл while, продолжить ниже
}
if ($line =! m/^Subject: (.*)/) {
$subject = $1;
}
if ($line =! m/^Date: (.*)/) {
$date = $1;
}
if ($line =! m/^Reply-To: (.*)/) {
$reply_address = $1;
}
if ($line =! m/^From: (\S+) \(([^()]*)\)/) {
$reply_address = $1;
$from_name = $2;
}
}
Каждая строка заголовка проверяется по всем регулярным выражениям, и если она совпадает с одним из них, соответствующей переменной присваивается значение. Многие строки заголовка не совпадут ни с одним регулярным выражением. Такие строки попросту игнорируются.
После завершения цикла while выводится заголовок ответа:
print "To: $reply_address ($from_name)\n"
print "From: Jeffrey Friedl <jfriedl\@ora.com\n";
print "Subject: Re: $subject\n";
print "\n" ; # Пустая строка, отделяющая заголовок
# от основного текста сообщения
Обратите внимание на включение в тему префикса Re:, являющегося неформальным признаком ответа. Непосредственно перед выводом основного текста сообщения добавляется команда:
print "On $date $from_name wrote:\n";
Для всех остальных входных данных (основного текста сообщения) мы просто выводим каждую строку с добавлением префикса |>spc:
while ($line = <>) {
print "|> $line";
}
Символ новой строки здесь не нужен, поскольку переменная $line наследует его из входных данных.
Кстати говоря, команду вывода строки с префиксом цитирования, print "|> $line", можно записать с использованием регулярного выражения:
$line = ~s/^/|> /;
print $line;
Подстановка ищет выражение [^] и, конечно, находит его в начале строки. Впрочем, реальные символы при этом не совпадают, поэтому подстановка заменяет «ничто» в начале строки комбинацией «|>spc» — то есть фактически вставляет «|>spc» в начало строки. Такое экзотическое применение регулярных выражений в данном случае не оправдано, но в конце главы будет продемонстрировано нечто похожее (кстати, эта конструкция уже встречалась в самом начале главы, хотя вы вряд ли обратили на нее внимание).
Если уж мы рассматриваем реальный пример, вероятно, следует указать на его реальные недостатки. Во-первых, как упоминалось выше, примеры этой главы демонстрируют общие принципы использования регулярных выражений, а Perl — всего лишь удобное средство. Использованный код Perl не всегда является наиболее эффективным, но он достаточно наглядно показывает, как работать с регулярными выражениями. «Настоящая» Perl-программа для решения этой задачи, вероятно, содержала бы не более двух строк[4].
Кроме того, в реальном мире такие простые задачи встречаются редко. Строка From: может кодироваться в нескольких разных форматах; в нашей программе обрабатывается лишь один частный случай. Если поле заголовка хоть частично расходится с шаблоном, переменной $from_name не присваивается значение, и она остается неопределенной. Одно из возможных решений — изменить регулярное выражение и предусмотреть в нем обработку разных форматов адреса/имени, но в данной главе заниматься этим не стоит (решение приведено в конце главы 7). В качестве предварительной меры после проверки исходного сообщения (и перед выводом шаблона) можно вставить следующий фрагмент[5]:
if ( not defined($reply_address)
or not defined($from_name)
or not defined($subject)
or not defined($date)
{
die "couldn't glean the required information";
}
Функция Perl defined проверяет, имеет ли переменная определенное значение, а функция die выдает сообщение об ошибке и завершает программу.
Кроме того, наша программа предполагает, что строка From: расположена в заголовке перед строкой Reply-To:. Если строка From: окажется на втором месте, она сотрет адрес отправителя $reply_address, взятый из строки Reply-To:.
Электронная почта генерируется разными программами, каждая из которых следует собственным представлениям о стандарте, поэтому обработка электронной почты может оказаться весьма непростой задачей. Как выяснилось при попытке запрограммировать некоторые действия на Pascal, без регулярных выражений эта задача становится невероятно сложной — настолько, что мне было проще написать на Pascal пакет для работы с регулярными выражениями в стиле Perl, чем пытаться сделать все непосредственно на Pascal! Я воспринимал силу и гибкость регулярных выражений как нечто привычное, пока не столкнулся с ситуацией, когда они вдруг стали недоступными. Мне пришлось туго.
Надеюсь, задача с повторяющимися словами из главы 1 пробудила в вас интерес к регулярным выражениям. В самом начале главы я подразнил вас загадочным набором символов, которые я назвал решением задачи. Теперь, когда вы хотя бы немного разбираетесь в Perl, нетрудно узнать знакомые элементы — <>, три s/…/…/ и print. И все же непонятного остается еще больше! Если в этой главе вы впервые встретились с Perl (и до этой книги никогда не работали с регулярными выражениями), вероятно, вы бы предпочли заняться чем-нибудь попроще.
Но как мне кажется, это регулярное выражение не такое уж сложное. Прежде чем подробно рассматривать программу, стоит вспомнить постановку задачи, описанную в начале главы 1, и посмотреть на результат тестового запуска:
% perl -w FindDbl ch01.txt
ch01.txt: check for doubled words (such as this this), a common problem with
ch01.txt: despite capitalization differences, such as with 'The
ch01.txt: the...', as well as allow differing amounts of whitespace
ch01.txt: Wide Web pages, such as to make a word bold: 'it is <B>very</B>
ch01.txt: very important...'
ch01.txt: /\<(1,000,000|million|thousand thousand)/. But alternation can’t
ch01.txt: of this chapter. If you knew the the specific doubled word
Перейдем к рассмотрению программы. На этот раз я воспользуюсь некоторыми удобными средствами современных версий Perl — такими, как комментарии и свободные интервалы в регулярных выражениях. Если не считать этих синтаксических излишеств, версия программы на следующей странице идентична приведенной в начале главы. Я кратко объясню программу и ту логику, на которой основана ее работа, но за дополнительной информацией рекомендую обращаться к электронной документации Perl (а если ваши вопросы связаны с регулярными выражениями — то к главе 7). В дальнейшем описании термин «волшебный» означает «связанный с возможностями Perl, которые вам, возможно, еще не известны».
$/ = ".\n"; # (1) Особый режим чтения; фрагменты завершаются
# комбинацией символов "точка-новая строка"
while (<>) # (2)
{
next unless s # (3)
{ # (Начало регулярного выражения)
# Отдельное слово
\b # Начало слова
( [a-z]+ ) # Сохранить слово в $1 (и \1)
### Дальше может следовать произвольное количество пропусков
### и/или тегов
( # Сохранить в $2
( # ($3 - группировка для конструкции выбора)
\s # пропуски (в том числе и символы новой строки)
| # -или-
<[^>]+> # теги в угловых скобках
)+ # По крайней мере один из перечисленных элементов,
# но может быть и больше.
)
### Снова искать первое слово:
(\1\b) # \b гарантирует, что найденное слово
# не находится внутри другого слова.
# (Конец регулярного выражения)
}
# После регулярного выражения следует строка замены
# с модификаторами /i, /g и /x.
“\e[7m;1\e[m;2\e[7m;4\e[m”.igx; # (4)
s/^([^\e]*\n)+//mg; # (5) Удалить непомеченные строки
s/^/$ARGV: /mg; # (6) Начинать строку с имени файла
print;
}
1. Поскольку решение задачи с повторяющимися словами должно работать даже в том случае, если повторяющиеся слова находятся в разных строках файла, мы не можем использовать обычный режим построчной обработки, как в примере с электронной почтой. Присваивание специальной переменной $/ (да, это действительно переменная!) переводит последующий оператор <> в волшебный режим, при котором он возвращает не отдельные строки, а фрагменты, приблизительно совпадающие с абзацами. Возвращаемое значение представляет собой одну последовательность символов, которая может состоять из нескольких логических строк.
2. Вы обратили внимание — значение, возвращаемое оператором <>, ничему не присваивается? В условиях цикла while оператор <> волшебным образом присваивает строку специальной переменной $_, используемой в качестве операнда по умолчанию во многих функциях и операторах. Эта переменная содержит строку, с которой по умолчанию работает s/…/…/ и которая выводится командой print. Использование переменной по умолчанию делает программу более компактной, но и менее понятной для новичков, поэтому я рекомендую пользоваться явными операндами до тех пор, пока вы не почувствуете себя более уверенно.
3. Символ s в этой строке относится к оператору подстановки s/…/…/, обладающему существенно большими возможностями, чем мы видели до настоящего момента. Одна из замечательных особенностей заключается в том, что для определения границ регулярного выражения и строки замены не обязательно использовать символ / — можно использовать и другие символы, как в приведенной в данном примере конструкции s{выражение}"замена". Чтобы увидеть модификатор /x, используемый при подстановке, необходимо перейти к п.4. Модификатор /x позволяет включать в регулярное выражение (но не в строку замены!) комментарии и свободные интервалы. Почти два десятка строк регулярного выражения состоят в основном из комментариев, а «настоящее» регулярное выражение с точностью до байта идентично приведенному в начале главы.
Команда next unless перед командой подстановки заставляет Perl отменить обработку текущей строки (и перейти к следующей), если подстановка ничего не дает. Нет смысла продолжать обработку строки, в которой не были найдены повторяющиеся слова.
4. Строка замены в действительности имеет вид "$1$2$4" с несколькими промежуточными Escape-последовательностями ANSI, обеспечивающими цветовое выделение двух повторяющихся слов. Последовательность \e[7m начинает выделение, а \e[m — завершает его (\e в строках и регулярных выражениях Perl является сокращенным обозначением символа, с которого начинаются Escape-последовательности ANSI).
Присмотревшись к круглым скобкам в регулярном выражении, вы поймете, что "$1$2$4" соответствует совпавшей части. Поэтому, если не считать добавления Escape-последовательностей, вся команда замены фактически является (относительно медленной) пустой операцией.
Мы знаем, что $1 и $4 содержат одинаковые слова (собственно, для этого и была написана программа!), поэтому в замене можно было использовать одну из этих переменных. Тем не менее, слова могут различаться регистром символов, поэтому я включил в строку замены обе переменные.
5. После того, как замена пометит все повторяющиеся слова в фрагменте (возможно, состоящем из нескольких логических строк), мы удаляем из выходных данных те логические строки, в которых отсутствует признак Escape-последовательности \e (и оставляем лишь те, в которых присутствуют повторяющиеся слова)[6]. Модификатор /m, использованный в этой и следующей подстановке, заставляет подстановку волшебным образом интерпретировать целевую строку (переменную по умолчанию $_, упоминавшуюся выше) как совокупность логических строк. В результате интерпретация символа ^ означает уже не просто «начало строки», а «начало логической строки», и может совпасть где-нибудь в середине фрагмента, если это «где-нибудь» находится в начале логической строки. Регулярное выражение [^([^\e]*\n)+] находит последовательности «не-служебных» символов, завершающиеся символом новой строки. Регулярное выражение, использованное в этой подстановке, удаляет эти последовательности. В результате во входных данных остаются лишь логические строки, содержащие символ \e — то есть строки с повторяющимися словами.
6. Волшебная переменная $ARGV выводит имя входного файла. При наличии модификаторов /m и /g эта подстановка включает имя входного файла в начало каждой логической строки, оставшейся в фрагменте выходных данных после удаления.
Наконец, команда print выводит оставшуюся часть строки вместе с Escape-последовательностями ANSI. Цикл while повторяет эти действия для всех строк (в действительности фрагментов текста, примерно соответствующих абзацам), прочитанных из входных данных.
Как я подчеркнул в начале этой главы, Perl — всего лишь инструмент, выбранный для демонстрации концепций. Это очень удобный инструмент, но я хочу снова подчеркнуть, что эту задачу с такой же легкостью можно решить и на другом языке. В главе 3 приведено аналогичное решение для GNU Emacs. А для непосредственного сравнения внизу приведено решение на языке Python. Даже если раньше вы никогда не встречались с Python, эта программа все равно дает представление о несколько ином подходе к обработке регулярных выражений.
Решение задачи с повторяющимися словами на языке Python
import sys; import regex; import regsub
## Подготовить три регулярных выражения
reg1 = regex.compile(
'\\b([a-z]+\)\(\([\n\r\t\f\v ]\|<[^>]+>\)+\)\(\\1\\b\)',
regex.casefold)
reg2 = regex.compile('^\([^\033]*\n\)+')
reg3 = regex.compile('^\(.\)')
for filename in sys.argv[1:]: # Для каждого файла...
try:
file = open(filename) # попытаться открыть файл
except IOError, info:
print '%s: %s' % (filename, info[1]) # Если не получилось,
# сообщить об ошибке
continue # и прервать итерацию
data = file.read() # Прочитать весь файл в data, применить
# регулярные выражения и вывести
data = regsub.gsub(reg1, '\033[7m\\1\033[m\\2\033[7m\\4\033[m', data)
data = regsub.gsub(reg2, '', data)
data = regsub.gsub(reg3, filename + ': \\1', data)
print data,
Большинство изменений связано с чисто механическими операциями. Язык Perl разрабатывался для обработки текстов и поэтому волшебным образом решает многие задачи за вас. Python обладает в высшей степени четким и последовательным синтаксисом, но это означает, что многие рутинные задачи (открытие файлов и т. д.) приходится решать вручную[7].
Внешне диалект регулярных выражений Python отличается от диалектов Perl и egrep обилием символов \. Например, [(] не является метасимволом — для группировки и сохранения используется конструкция [\(…\)]. Кроме того, Python не поддерживает сокращенную запись префикса Escape-последовательностей \e, поэтому в выражение этот символ вставляется непосредственно в виде ASCII-кода \033. Если не считать одной особенности [^], рассматриваемой ниже, все эти отличия несущественны; регулярные выражения функционально идентичны тем, которые были использованы в примере Perl[8].
Другая интересная особенность заключается в том, что в строке замены, функции gsub («global substitution», то есть «глобальная подстановка» — аналог оператора Perl s/…/…/), используется то же обозначение \\1, что и в самом регулярном выражении. Для обращения к этой же информации за пределами регулярного выражения и строки замены в Python используется другое обозначение, regex.group(1). Сравните с Perl: в регулярном выражении используется [\1], за его пределами — $1. Похожие концепции, разные решения.
Однако есть и принципиальное отличие — в Python метасимвол [^] интерпретируется так, что последний символ новой строки считается началом пустой строки. Если бы третье регулярное выражение полностью совпадало с тем, которое было использовано в программе Perl, в конце выходных данных выводилась бы лишняя строка вида имя_файла:. Чтобы обойти это затруднение, я просто потребовал, чтобы третье регулярное выражение совпадало с какими-то символами после [^] и производило тождественную замену. В результате за последней логической строкой совпадение не происходит.
[1] Спасибо Уильяму Ф. Мэттону (William F.Matton) и его профессору за удачную аналогию.
[2] Хотя все примеры этой главы работают и в более ранних версиях, я настоятельно рекомендую использовать Perl версии 5.002 и выше. В частности, я не советую работать с архаичной версией 4.026, если у вас нет для этого каких-то особых причин.
[3] В старых версиях Perl вместо оператора or использовался оператор ||.
[4] Возможно, я слегка преувеличиваю, но как показывает первая программа этой главы, в Perl большие вычислительные возможности сосредоточиваются в очень малом объеме программы. Отчасти это обусловлено мощным механизмом обработки регулярных выражений.
[5] В этом фрагменте использованы конструкции, появившиеся лишь в Perl версии 5 и делающие пример более наглядным. Пользователи более старых версий вместо not должны использовать !, а вместо or — «||».
[6] Такое решение предполагает, что входной файл не содержит служебных символов ANSI. В противном случае программа может включить в выходные данные ошибочные строки.
[7] Именно за это поклонники Perl любят Perl и ненавидят Python. А поклонники Python — наоборот…
[8] Ладно, признаю: одно отличие все же есть. Метасимвол Perl [\s] предполагает, что решение о том, какие символы относятся к пропускам, принимается при компиляции библиотеки C. В моем регулярном выражении Python ([[\n\r\t\f\vspc]]) символы, относящиеся к категории пропусков, перечислены явно.