29-08-2008 12:56
![]() |
В этой главе рассматриваются: |
Большое количество процессов обработки данных используют поиск по шаблону. В действительности следует сказать так, -- подавляющее большинство процессов обработки данных использует тот или иной поиск по шаблону, тем или иным способом. В Perl львиная доля поисков по шаблону производится с использованием регулярных выражений (прим.перев.: regular expressions или regexp. На самом деле, часто считается, что Perl злоупотребляет регулярными выражениями). Другими словами, -- очень важно понимать как собственно их использовать. В этой главе будет представлен обзор регулярных выражений в Perl, и того, как они могут быть использованы в процессах обработки данных, однако начнем с того, что вкратце рассмотрим пару-тройку методов поиска по шаблонам, не использующие регулярные выражения.
В Perl имеются функций обработки строк, и они, зачастую, эффективнее и значительно проще в использовании, чем использование методов, основанных на регулярных выражениях, которые мы обсудим позже. Когда мы решаем, как решить конкретную задачу, всегда стоит посмотреть, можно ли использовать более простой метод, прежде чем сразу же перейти к решению с регулярными выражениями.
Если требуется извлечь конкретную часть строки, то вы можете использовать функцию substr. Эта функция
получает два обязательных параметра: строку над которой требуется выполнить действия и
смещение, с которого надо начинать; а также два опциональных параметра: требуемая длина подстроки и другая строка,
которой предполагается заменить подстроку. Если третий параметр пропущен, то подстрока будет включать все символы
исходной строки, начиная с указанного смещения и до конца. Смещение первого символа в исходной строке равно 0 (или,
точнее, это значение специальной переменной , которая, однако, изначально установлена в ноль, и нет особых
причин изменять его, -- ваши строки всегда должны начинаться с позиции нуля). Если смещение отрицательно, то отсчет
начинается с конца строки. Вот несколько простых примеров:
my $string = 'Alas poor Yorick. I knew him Horatio.'; my $sub1 = substr($string, 0, 4); # $sub1 содержит 'Alas' my $sub2 = substr($string, 10, 6); # $sub2 содержит 'Yorick' my $sub3 = substr($string, 29); # $sub3 содержит 'Horatio.' my $sub4 = substr($string, -12, 3); # $sub4 содержит 'him'
Многие языки программирования имеют функцию, которая производит подстроки сходным образом, однако только в Perl-овой
функции substr ее результат может выступать как "левое значение" (прим.перев.: lvalue). Таким образом, вы можете
присваивать ей значения вроде этого:
my $string = 'Alas poor Yorick. I knew him Horatio.'; substr($string, 10, 6) = 'Robert'; substr($string, 29) = 'as Bob'; print $string;
в итоге мы получим такой результат:
Alas poor Robert. I knew him as Bob
Отметим, второе присвоение в примере, -- оно демонстрирует, что подстрока и текст, который ее замещает, не одинаковой длины. Perl сам позаботится о всех необходимых манипуляциях со строками. Вы даже можете сделать что-то типа такого:
my $short = 'Short string'; my $long = 'Very, very, very, very long'; substr($short, 0, 5) = $long;
в результате $short будет содержать текст "Very, very, very, very long string".
Еще две функции, которые могут быть полезны для такого рода манипуляций с текстом --
index и rindex. Эти функции выполняют очень схожие действия -- index находит
первое вхождение строки в другую строку, а rindex находит ее последнее вхождение. Обе эти
функции возвращают целое значение, указывающее позицию в исходной строке (и снова, позиция
начинается с 0, или значения ), с которой начинается данная подстрока. Обе функции получают
опциональный третий параметр, который является позицией, с которой должен начинаться поиск. Вот
несколько простых примеров:
my $string = 'To be or not to be.'; my $pos1 = index($string, 'be'); # $pos1 == 3 my $pos2 = rindex($string, 'be'); # $pos2 == 16 my $pos3 = index($string, 'be', 5); # $pos3 == 16 my $pos4 = index($string, 'not'); # $pos4 == 9 my $pos5 = rindex($string, 'not'); # $pos5 == 9
Ничего удивительного в том, что $pos3 == 16, так как мы начинаем поиск после позиции 5,
а pos4 и pos5 равны, так как в нашей исходной строке только одно упоминание о строке 'not'.
Конечно можно использовать комбинацию всех трех функций, чтобы решить более сложные задачи. Например, если у вас есть некоторая строка, и требуется извлечь из ее середины то, что содержится между квадратными скобками ([ и ), то можно написать что-то вроде этого:
my $string = 'Text with an [important bit in brackets'; my $start = index($string, '['); my $end = rindex($string, ']'); my $keep = substr($string, $start+1, $end-$start-1);
хотя в данном случае, решение, основанное на регулярных выражениях, будет более читабельным.
Другой наиболее общей потребностью является изменение регистра текстовой строки --
изменить все символы в верхний регистр, все символы в нижний, либо их комбинация. Perl имеет
функции для того, чтобы удовлетворить все эти потребности. Этими функциями являются uc
(преобразующая всю символы строки в верхний регистр), ucfirst (преобразует первый символ
строки в верхний регистр), и lcfirst (преобразует первый символ строки в нижний регистр).
Неопытный программист, при использовании этих функций, может попасться в некоторые ловушки. Первая из них
связана с функциями ucfirst и lcfirst. Важно отметить, что они делают в точности то, что сказано, --
воздействуют на первый символ данной строки. Я видел код вроде такого:
$string = ucfirst 'UPPER'; # Не работает...
здесь программист ожидал получить в результате строку 'Upper'. Корректный код должен был выглядеть так:
$string = ucfirst lc 'UPPER';
Вторая ловушка состоит в том, что эти функции будут придерживаться вашего местного языкового набора символов
(прим.перев.: local language character set), и чтобы получить корректный результат, необходимо переключить Perl-овую
поддержку "локАли" (прим.перев.: Perl's locale support), включая в вашу программу строку use locale.
В этом разделе мы внимательно рассмотрим регулярные выражения. Это один из наиболее мощных инструментов Perl, из числа тех, которые удобны для целей обработки данных, но, вместе с тем, это также и наиболее сложная, для человеческого понимания, тема.
"Регулярные выражения" (regular expressions) -- это весьма формально звучащий термин из области компьютерной науки, который наверное меньше бы пугал людей, если бы мы просто назвали его "поиск по шаблону" (pattern matching), так как в основном это то, о чем мы и говорим.
Если у вас есть некоторые данные, и требуется узнать, имеется или не имеется некоторая строка внутри набора данных, то вам потребуется сконструировать регулярное выражение, которое опишет данные, которые вы разыскиваете, а потом посмотреть, будет ли оно соответствовать вашим данным. Позже, в данной главе, мы детально рассмотрим каким образом конструировать регулярное выражение, чтобы оно соответствовало вашим данным. Но сначала, мы сделаем обзор тех вещей, которые вы можете сделать с помощью регулярных выражений.
Многие инструменты, занимающиеся обработкой текста, поддерживают регулярные выражения. Все UNIX-инструменты, такие как
vi, sed, grep и awk, поддерживают их в той, или иной мере. Даже некоторые Windows-инструменты, вроде
Microsoft Word, позволяют вам искать текст, используя простейшие регулярные выражения. По сравнению с каждым из этих
инструментов, Perl имеет наиболее мощную поддержку регулярных выражений. Помимо прочего, регулярные выражения Perl
позволяют найти следующее:
Свежие версии Perl некоторым образом расширяют стандартный набор регулярных выражений,
причем некоторые все еще находятся в экспериментальной стадии на момент написания данной
книги. Чтобы выявить эти расширения, обратитесь к документации, поставляемой с вашей версией Perl, --
для этого смотрите страницу документации perlre.
Чтобы, в Perl, превратить строку символов в регулярное выражение необходимо заключить ее в символы наклонной черты (/). Вот пример:
/regular expression/
это регулярное выражение, которое будет соответствовать строке "regular expression".
Внутри регулярного выражения большинство символов будет соответствовать им самим (таким образом, буква "a" в регулярном выражении будет соответствовать символу "a" в целевой строке), если их значение не изменено присутствием различных метасимволов. Вот список метасимволов, которые могут быть использованы в регулярных выражениях Perl:
\ | ( ) [ { ^ $ * + ? .
Любой из этих метасимволов может быть использован, в регулярном выражении, для соответствия самому себе, если предваряется символом обратной косой черты (\). Вы увидите, что обратная косая черта сама по себе метасимвол, поэтому, чтобы получить соответствие литеральной обратной косой черте вам потребуется записать две обратные косые черты в вашем регулярном выражении, например /foo\\bar/ соответствует "foo\bar".
Символ точки (.) соответствует любому символу.
Обычная escape-последовательность, которая знакома вам по многим другим языкам программирования, здесь также доступна. Символ табуляции сопоставляется как \t, новая строка -- \n, возврат каретки -- \r, а проброс бумаги -- \f.
Можно написать соответствие любому символу из группы символов (известной в Perl как класс символов), заключая символы внутрь квадратных скобок ([ и ]). Если группа символов представляет собой последовательность символов вашего языкового набора, то возможно использовать знак дефис (-), чтобы обозначить диапазон символов. Другими словами регулярное выражение
/[aeiouAEIOU]/
будет соответствовать любой гласной, а
/[a-z]/
будет соответствовать любому латинскому символу в нижнем регистре.
Чтобы найти соответствие любому символу, который не входит в класс символов, поместите знак крышки (^) в начало группы, таким образом
/[^aeiouAEIOU]/
соответствует любому не-гласному символу (заметим, что это не значит, что совпадение будет лишь для согласных символов, так как, совпадение коснется символов пунктуации, пробелов, управляющих символов, и даже расширенных ASCII-символов, вроде ñ, «, или é).
Имеется некоторое количество предопределенных классов символов, которые могут быть обозначены
использованием escape-последовательностей. Любая цифра сопоставляется с помощью \d. Любой символ слова
(то есть цифры, буквы верхнего и нижнего регистров, а также символ подчеркивания) сопоставляется с помощью
\w, а любой пробельный символ (пробел, табуляция, возврат каретки, перевод строки или проброс бумаги) --
с помощью \s. Инверсии этих классов также определены. Любая не-цифра сопоставляется с помощью \D,
любой "не-буквенный" символ -- \W, а любой "не-пробельный" символ -- \S.
Символ вертикальной черты (|) используется для обозначения альтернативных шаблонов. Такое регулярное выражение, как
/regular expression|regex/
будет соответствовать либо строке "regular expression", либо "regex". Круглые скобки (( и )) могут быть использованы для группировки строк, поэтому
/regexes are cool|rubbish/
будет соответствовать строкам "regexes are cool" или "rubbish", а
/regexes are (cool|rubbish)/
будет соответствовать "regexes are cool" или "regexes are rubbish".
Побочный эффект от использования символов группировки (круглых скобок), состоит в том, что если строка совпадает с
регулярным выражением, то части строки, которые соответствуют секциям в круглых скобках запоминаются в специальных
переменных $1, $2, $3 и т.д. Например, после совпадения строки с предыдущим регулярным выражением, переменная
$1 будет содержать строку "cool" или "rubbish". Дополнительные примеры мы увидим ниже, в данной главе.
Помимо прочего, возможно квантифицировать число, появлений строки, с использованием символов +, * или ?.
Помещая знак + после символа (или после строки символов в круглых скобках, либо класса символов) мы достигаем того,
что такой элемент может появляться в строке один или более раз, * -- допускает появление элемента ноль или более раз, а
? -- позволяет появление элемента ноль или один раз (то есть, такой элемент становится опциональным). Вот примеры:
/so+n/
будет совпадать с "son", "soon" или "sooon", в то время как
/so*n/
будет соответствовать "sn", "son", "soon" и "sooon" и т.д., а
/so?n/
будет соответствовать "sn" и "son".
Сходным образом для группы символов
/(so)+n/
будет соответствовать "son", "soson" или "sososon" и т.д., в то время как
/(so)*n/
будет совпадать с "n", "son", "soson" и "sososon" и т.д., а
/(so)?n/
будет соответствовать "n" и "son".
Можно указать и другое количество повторений выражения, для этого надо воспользоваться синтаксисом {n,m}.
В этом синтаксисе за выражением, который будет повторяться, должны следовать фигурные скобки, содержащие до
двух чисел, разделенных запятой. Числа указывают минимальное и максимальное количество раз, которые должен быть
повторено выражение. Например, в регулярном выражении
/so{1,2}n/
символ "o" будет срабатывать, если он появится от одного до двух раз, то есть совпадением будет "son" или "soon",
но не "sooon". Если первое число пропущено, то оно предполагается равным нулю, а если пропущено второе число, то
предполагается, что для совпадения нет придела числу повторений. Тут вы должны заметить, что формы +, * и ?, которые
мы использовали ранее, строго говоря не являются необходимыми, так как могут быть записаны как {1,}, {0,} и {0,1}.
Если только одно из чисел появляется без запятой, то выражение будет срабатывать если шаблон повторяется в точности указанное
число раз.
Имеется возможность закрепить часть вашего регулярного выражения в различных местах данных. Если вы желаете, чтобы регулярное
выражение совпадало только лишь с началом ваших данных, то можно воспользоваться символом крышки (^). Сходным образом,
знак доллара ($), совпадает с концом ваших данных. Для того, чтобы "отловить" строку email-заголовка, который состоит из
таких строк, как "From", "To" или "Subject", за которыми следует символ двоеточия, опциональный пробел, и некоторый
дополнительный текст, можно воспользоваться регулярным выражением вроде этого (его можно также написать и так --
/^.+?: ?.+$/, однако мы еще не рассматривали "нежадный" синтаксис (прим.перев.: nongreedy):
/^[^:]+: ?.+$/
которое совпадает с началом строки, за которым следует как минимум один символ не-двоеточия, а за ним идет двоеточие, необязательный пробел и как минимум один, любой символ до конца строки.
Другим специальным возможным условием является закрепление на границах слова. Условие \b совпадает только с началом
или концом слова (то есть, между символами \w и \W, а его инверсия \B совпадает только внутри слова (то есть,
между двумя символами \w). Например, если мы желаем "поймать" "son", но не имена, вроде "Jonhson" и
"Robertson", то должны использовать, например, такое регулярное выражение:
/\bson\b/
а если нас интересуют появления "son"-а в конце других слов, то должны использовать такое:
/\Bson\b/
Свежие версии Perl включают в себя усложнения регулярных выражения, позволяющие определить более сложные правила, которыми можно "отловить" ваши строки. Полный список таких улучшений вы можете найти в своей Perl-документации, однако вот наиболее значимые дополнения:
(?:...) -- эта группа скобок действует таким же образом как и обычные квадратные скобки, однако, когда происходит
совпадение, то их содержимое не присваивается переменным $1, $2 и т.д.
(?=...) -- известно как положительный предварительный просмотр (прим.перев.: positive lookahead). Оно позволяет проверить
будет ли то, что между скобками, существовать в строке, но в действительности такой шаблон не поглощает следующую часть совпадающей строки.
(?!...) -- это негативный предварительный просмотр (прим.перев.: negative lookahead), который является противоположностью
положительному. Вы получите совпадение, если то, что в скобках не совпадает со строкой.
Большинство регулярных выражений используются в Perl-программах одним из двух способов. Простейший способ -- проверяем, совпадает ли строка данных с регулярным выражением, а чуть-чуть более сложный -- заменяем части строки данных другими строками.
Чтобы проверить совпадение строки с регулярным выражением в Perl, используем оператор совпадения, -- который обычно называется
m//, хотя вполне возможно, когда вы им воспользуетесь, он будет выглядеть совсем не так.
По умолчанию, оператор совпадения работает с переменной $_. Особенно хорошо это работает, когда вы перебираете массив
значений. Например, представим себе, что у вас есть текстовый файл, содержащий email-сообщения, и необходимо распечатать все
его строки, содержащие заголовки "From". Вы можете написать что-либо похожее на это:
open MAIL, 'mail.txt' or die "Не удается открыть mail.txt: $!";
while (<MAIL>) {
print if m/^From:/;
}
Цикл while считывает в переменную $_, одну за другой, строки файла. Оператор совпадения проверяет строки, начиная с их
начала (символ ^ -- соответствующий началу строки), на наличие подстроки "From", и возвращает истину для совпадающих
строк. Затем такие строки печатаются на STDOUT.
И последний мазок, относительно оператора совпадения, состоит в том, что во многих случаях m необязателен, то есть мы можем
переписать оператор совпадения в нашем скрипте таким образом:
print if /^From:/;
так, как вы еще успеете заметить, в большинстве скриптов и делается. Кроме того, возможно использовать иной разделитель, нежели
символ косой черты (/), правда в этом случае m становится обязательным. Чтобы увидеть почему может потребоваться
такой вариант, рассмотрим пример:
open FILES 'files.txt' or die "Не удается открыть files.txt: $!";
while (<FILES>) {
print if /\/davec\//;
}
В этом скрипте, мы выполняем нечто очень похожее на предыдущий пример, однако в этом случае мы сканируем список файлов,
и печатаем те, которые входят в каталог davec. Известно, что у каталога символ-разделитель -- косая черта
(/), поэтому необходимо "экранировать" (прим.перев.: escape) такие символы в регулярном выражении, с помощью обратной
косой черты, что в конечном счете сделает его не-элегантным (иногда такое написание называют "синдромом зубочистки").
Чтобы решить эту проблему, Perl предлагает нам выбрать свой собственный разделитель регулярного выражения. Он может быть
любым символом пунктуации, однако если в качестве разделяющего символа, будет выбран один из парных, --
то ((, {, [ или <) должен быть выбран для открывающих регулярное выражение, а (), }, ] или >) -- для
закрывающих, в противном случае необходимо использовать тот же самый символ. Другими словами, теперь мы можем переписать
нашу строку проверки на совпадение таким образом
print if m(/davec/);
или так
print if m|/davec/|;
или даже так
print if m=/davec/=;
и любой из этих вариантов легче читать чем оригинал. Заметим, что во всех случаях мы используем m в начале выражения.
Когда совпадение успешно, Perl заполняет некоторые специальные переменные. Для каждой шаблон-группы в скобках, Perl
устанавливает свою переменную. Первая группа скобок заносится в $1, вторая -- в $2, и т.д. Группы скобок могутт быть
вложенными, поэтому порядок присвоения таким переменным зависит от порядка скобки, открывающей группу. Вернемся к нашему
прежнему примеру с email-заголовками, -- если мы имеем текстовый файл с email-ами, и желаем отпечатать все заголовки, то
можем пожелать сделать что-либо похожее на это (в данном упрощенном примере, для удобства изложения, игнорируем, что
email-заголовки могут продолжаться на следующих строках, и что тело email может содержать символ ":"):
open MAIL, 'mail.txt' or die "Неудается открыть mail.txt: $!";
while (<MAIL>) {
if (/^([^:]+): ?(.+)$/) {
print "Заголовок $1 имеет значение $2\n";
}
Мы добавили пару наборов скобок в исходное регулярное выражение, и тем самым сохранили имя заголовка и его значение,
в переменные $1 и $2, а значит в следующей строке вполне можем их распечатать. Если оператор совпадения вычисляется
в списочном контексте, то он возвращает значения $1, $2 и т.д. в массив. Другими словами, можно переписать предыдущий
пример так:
open MAIL, 'mail.txt' or die "Can't open mail.txt: $!";
my ($header, $value);
while (<MAIL>) {
if (($header, $value) = /^([^:]+): ?(.+)$/) {
print "Заголовок $header имеет значение $value\n";
}
}
Есть и другие переменные, которые устанавливаются Perl-ом, в случае успешного совпадения. Они включают в себя
$&, -- устанавливается в часть строки, совпадающей со всем регулярным выражением, $` -- устанавливается в
часть строки до той части, которая совпала с регулярным выражением, и $' -- устанавливается в часть строки после
той части, которая совпала с регулярным выражением. Другими словами после выполнения следующего кода:
$_ = 'Matching regular expressions'; m/regular expression/;
$& будет содержать строку "regular expression", $` -- будет содержать "Matching ", а $' -- будет иметь
значение "s". Очевидно, эти переменные наиболее полезны когда ваше регулярное выражение не является неизменяемой строкой.
Использование этих переменных имеет небольшой неприятный побочный эффект. Если вы не используете эти переменные, то и Perl их не использует. Однако если вы начнете их использовать, хотя бы в одном сопоставлении своей программы, то Perl тут же начнет обновлять их на каждое совпадение. Другими словами, использование этих переменных может возыметь свой эффект на производительность.
Совершенно очевидно, что не всякая строка, которую вы желаете проверить на соответствие регулярному выражнию, обязана быть
в $_, поэтому Perl предоставляет оператор связывания, который связывает операцию сопостоавления регулярному выражению и
другую переменную. Оператор должен выглядеть так:
$string =~ m/regular expression/
Этот оператор ищет совпадение строке "regular expression", внутри текстового содержимого переменной $string.
Существуют опциональные модификаторы, которые могут применяться к оператору совпадения, чтобы изменить способ его работы.
Эти модификаторы помещаются после закрывающего разделителя. Наиболее часто используемым модификатором является i, который
приводит к тому, что проверка на совпадение становится нечувствительной к регистру, поэтому
m/hello/i
будет совпадать с "hello", "HELLO", "Hello" или любыми другими комбинациями символов в разных регистрах. Ранее мы выидели регулярнре выражение, совпадающее с гласными, которое выглядело так
/[aeiouAEIOU]/
Теперь же мы можем применить модификатор i, и переписать выражение так
/[aeiou]/i
Следующие два модификатора -- s и m, которые приводят к тому, что совпадение трактует данные либо как
одну, либо как несколько строк. В режиме нескольких строк "." будет совпадать с символом новой строки (такой режим
не установлен по-умолчанию). Кроме этого ^ и $ будут совпадать с началом и концом любой строки соответственно.
Чтобы получить совпадение с началом и концом всех данных целиком, можно использовать закрепления \A и \Z
соответственно.
И наконец, модификатор x, -- он позволяет поместить пробел и коментарий внутри регулярного выражения.
Регулярные выражения, недавно рассмотренные нами были достаточно простыми, однако, обычно, они намного больше,
что и дало Perl-у репутацию языка электрических помех (прим.перев.: line noise). Если мы еще раз взглянем на
на регулярное выражение, которое использовали для поиска совпадений с email-заголовками, -- с каким из них проще
разобраться? С этим
m/^[^:]+\s?.+$/
или, например, этим?
m/^ # начало строки [^:]+ # как минимум один символ не-двоеточия : # двоеточие \s? # возможный пробельный символ .+ # как минимум один любой символ $/x # конец строки
А ведь это всего лишь простой пример!
Операция замены строки выглядит, на удивление, похожим на оператор поиска совпадений, и работает схожим образом.
Оператор обычно вызывается как s///, хотя, как и оператор поиска совпадений, в действительности может принимать
множество форм. Простейшим способом использования оператора замены строки является оператор замены всех появлений
одной строки на другую. Например, замена "Dave" на "David", может быть выполнена таким кодом:
s/Dave/David/;
Первое выражение (Dave) вычисляется как регулярное выражение. Второе выражение -- строка,
которая замещает собой все, что совпадает с регулярным выражением в исходной строке данных.
Такая замена строки может содержать любые переменные, которые устанавливаются при успешном выполнении совпадения.
Другими словами, можно переписать предыдущий пример таким образом:
s/(Dav)e/${1}id/
Как и в случае с оператором совпадения, эта операция по-умолчанию работает с содержимым переменной $_,
однако вы всегда можете связать эту операцию с другой переменной, использованием оператора =~.
Все модификаторы оператора совпадения (i, s, m и x) работают сходным образом и в операторе
замены, однако дополнительно к ним имеются и другие. По-умолчанию, замена производится только лишь над
первым совпадением в строке данных. Например:
my $data = "This is Dave's data. It is the data belonging to Dave"; $data =~ s/Dave/David/;
приведет к тому, что $data будет содержать строку "This is David's data. It is the data belonging to Dave".
Второе появление Dave не затрагивается изменением. Чтобы изменить все появления строки необходимо использовать
модификатор g.
my $data = "This is Dave's data. It is the data belonging to Dave"; $data =~ s/Dave/David/g;
Теперь все работает так, как и ожидалось, и в результате $data содержит строку
"This is David's data. It is the data belonging to David."
Другими, двумя новыми, модификаторами, которые используются лишь с оператором замены, и когда поисковая строка, либо строка замены, содержит переменные или исполняемый код. Рассмотрим пример:
my ($new, $old) = @ARGV;
while (<STDIN>) {
s/$old/$new/g;
print;
}
в котором используется очень простой фильтр замены. Он получает, в качестве аргументов, две строки.
Первая строка -- искомый текст, а вторая -- текст, который его заменит. Этот код считывает все, что
передается ему на STDIN, и заменяет одну строку на другую. И это, конечно, работает... но
не самым эффективным образом. Каждый раз, когда начинается очередной виток цикла, Perl не уверен,
что содержимое переменной $old не изменилось, а посему каждый раз компилирует регулярное выражение.
Но мы то с вами знаем, что в переменной $old неизменное значение. Таким образом, мы можем дать знать
об этом и Perl-у, добавляя к оператору замены модификатор o. Это сообщает Perl-у, что он спокойно может
откомпилировать регулярное выражение всего лишь один раз, а затем, при каждом новом витке цикла, повторно
использовать ту же самую компилированную версию выражения. Для этого мы должны изменить строку замены таким
образом
s/$old/$new/go;
Есть и еще один модификатор, который необходимо рассмотреть, и этот модификатор -- e. Когда используется
такой модификатор, то строка замены трактуется как исполняемый код, и такой код передается в eval.
Значение, возвращаемое после eval, используется в качестве замещающей строки. (В действительности не все так просто,
так как возможно множественные экземпляры модификатора e, и строка замены вычисляется для каждого из них).
В качестве примера приведем весьма странный способ распечатать таблицу квадратов:
foreach (1 .. 12) {
s/(\d+)/print "$1 в квадрате равен ", $1*$1, "\n"/e;
}
результат будет таким:
1 в квадрате равняется 1 2 в квадрате равняется 4 3 в квадрате равняется 9 4 в квадрате равняется 16 5 в квадрате равняется 25 6 в квадрате равняется 36 7 в квадрате равняется 49 8 в квадрате равняется 64 9 в квадрате равняется 81 10 в квадрате равняется 100 11 в квадрате равняется 121 12 в квадрате равняется 144
Окончим наш обзор регулярных выражений скриптом, который осуществляет перевод с английского на американский язык. Чтобы облегчить себе задачу, сделаем несколько предположений.
Мы будем предполагать, что каждое английское слово имеет только один американский перевод. Кроме того, для удобства, мы будем игнорировать ситуации, когда английская фраза должна быть заменена совершенно другой американской, такой как, например, "car park" и "parking lot". Наш перевод мы будем сохранять в текстовом файле, так как в него легко добавлять. Программа будет выглядеть примерно так:
1: #!/usr/bin/perl -w
2: use strict;
3:
4: while (<STDIN>) {
5:
6: s/(\w+)/translate($1)/ge;
7: print;
8: }
9:
10: my %trans;
11: sub translate {
12: my $word = shift;
13:
14: $trans{lc $word} ||= get_trans(lc $word);
15: }
16:
17: sub get_trans {
18: my $word = shift;
19:
20: my $file = 'american.txt';
21: open(TRANS, $file) || die "Can't open $file: $!";
22:
23: my ($line, $english, $american);
24: while (defined($line = <TRANS>)) {
25: chomp $line;
26: ($english, $american) = split(/\t/, $line);
27: do {$word = $american; last; } if $english eq $word;
28: }
29: close TRANS;
30: return $word;
31: }
Строки 1 и 2 -- стандартный способ начать Perl-скрипт.
Цикл начинающийся на строке 4, считывает строки с STDIN, и помещает их в переменную $_.
Строка 6 выполняет бОльшую часть работы. Она просматривает группы символов, составляющих слова. Каждый раз,
когда она обнаруживает их, то сохраняет слово в переменной $1. Строка замены, -- это результат выполнения
кода translate($1). Обратите внимание на два модификатора: g, который означает, что все слова в
строке будет преобразованы, а e предписывает Perl выполнить строку замещения перед заменой в исходной строке.
Строка 7 печатает значение $_, которое теперь уже переведенный текст. Заметим, что когда print используется без
аргументов, то он по-умолчанию печатает содержимое переменной $_, что в нашем случае нам и требуется.
Строка 10 определяет кэширующий хэш, который используется функцией translate, для хранения слов с известным переводом.
Функция translate, которая начинается на строке 11, использует кэширующий алгоритм схожий с "маневром орков".
Если текущее слово не найдено в хэше %trans, то она вызывает get_trans, чтобы получить перевод слова.
Отметим, что мы всегда работаем со словами в нижнем регистре.
Строка 17 -- это начало функции get_trans, которая будет считывать любые необходимые слова из файла, содержащего
список переводимых слов.
Строка 20 определяет имя файла переводов, а строка 21 пытается открыть его. Если файл не может быть открыт, то программа оканчивает свою работу с сообщением об ошибке.
Строка 24 начинает цикл, в котором переменная $line, раз за разом, заполняется строкой из файла переводов,
а строка 25 удаляет из нее символ новой строки.
Строка 26 разбивает строку, используя в качестве разделителя символ табуляции, который отделяет английские и американские слова.
Строка 27 присваивает переменной $word американское слово, если обнаружено совпадение с английским словом.
Строка 29 закрывает файл.
Строка 20 возвращает либо перевод, либо исходное слово, если, в ходе перебора строк файла, перевод не найден.
Такой подход гарантирует, что либо функция всегда вернет правильное слово, либо хэш %trans будет содержать запись
для каждого требуемого нам слова. Если бы мы не сделали этого, то для каждого слова, не нуждающегося в переводе,
мы не имели щаписи в хэше, и были бы принуждены раз за разом осуществлять поиск в файле переводов. А в нашем случае,
каждое уникальное слово разыскивается в файле переводов только один раз.
Чтобы попробовать этот скрипт, создайте файл american.txt, содержащий по строке на каждое слово,
которое требуется перевести. Каждая строка должна содержать английское слово, затем символ табуляции, и,
наконец, эквивалентное американское слово. Например:
hello<TAB>hiya pavement<TAB>sidewalk
Создайте другой файл, содержащий текст, которые желаете перевести. В своем тесте я использовал такой
Hello. Please stay on the pavement.
а запускал программу, с использованием такой командной строки
translate.pl < in.txt
и в результате получил следующее
hiya. Please stay on the sidewalk.
Если вы желаете сохранить переведенный текст в другой текстовый файл, то можете запустить программу с использованием такой командной строки
translate.pl < in.txt > out.txt
И снова, здесь мы используем преимущество модели UNIX-фильтр, описанной в Главе 2.
Это не очень полезный скрипт. Например, он хоть и переводит, но некорректно обрабатывает слова с заглавной буквой. В следующем разделе мы рассмотрим нечто чуть более мощное.
Давайте рассмотрим еще несколько примеров из реальной жизни задач по обработке данных, в которых можно воспользоваться
регулярными выражениями. В этих примерах, в качестве наших входных данных, мы будем использовать хорошо известный, в
мире UNIX, стандарт файлов данных. Таким файлом является /etc/passwd, который хранит список пользователей UNIX-системы.
Этот файл является запись-ориентированным файлом с символами-разделителями "двоеточие". Это означает, что каждая строка
в таком файле представляет собой одного пользователя, а различные части информации о каждом пользователе отделены друг
от друга двоеточием. Типичная строка в одном из таких файлов может выглядеть так:
dave:Rg6kuZvwIDF.A:501:100:Dave Cross:/home/dave:/bin/bash
Семь частей (полей) этой строки имеют следующее значение:
Точное значние некоторых из этих полей может быть не столь очевидным для не-UNIX пользователей, однако оно вполне очевидно, для понимания следующих примеров.
Давайте начнем написание процедуры, считывающей данные во внутренние структуры данных. Такая
процедура затем может быть использована в любом из последующих примеров. И, как всегда, для
гибкости мы предположим, что данные поступают через STDIN.
sub read_passwd {
my %users;
my @fields = qw/name pword uid gid fullname home shell/;
while (<STDIN>) {
chomp;
my %rec;
@rec{@fields) = split(/:/);
$users{$rec->{name}} = \%rec;
}
return \%users;
}
Сходным образом, как и во всех процедурах ввода данных, которые мы уже реализовали, эта процедура считывает данные в некоторую структуру и возвращает ссылку на нее. В данном случае, в качестве основной структуры данных, мы выбрали хэш, по той причине, что пользователи в системе не упорядочены, и скорее всего нам потребуется получить информацию по конкретному пользователю. Хэш позволяет с легкостью достичь этой цели. Тут же встает вопрос, -- какой ключ для хэша будет лучшим выбором? Ответ целиком зависит от того, каким образом в дальнейшем планируется манипулировать данными, однако, в нашем случае мы выбрали имя пользователя (username). В остальных случаях, возможно более полезным выбором будет пользовательский ID (uid). Все остальные столбцы скорее всего будут плохим выбором, так как они не гарантируют уникальности на всем списке пользователей. (Вряд ли путь к пользовательскому домашнему каталогу будет неуникальным, однако возможно представить себе вариант, когда два или более пользователя разделяют один и тот же домашний каталог).
Итак, мы остановились на хэше, ключами которого являются имена пользователей. Что же будет являться
значениями нашего хэша? В этот раз я решил использовать хэш другого рода, ключами которого являются
названия полей данных (они определены в массиве @fields), а значениями их действительные значения.
Другими словами, наша процедура ввода считывает строку за строкой с STDIN, расщепляет ее на столбцы,
и помещает значения непосредственно в хэш %rec. После этого, ссылка на %rec сохраняется в главный
хэш %users. Заметьте, что %rec лексическая переменная, а значит ее область действия внутри цикла
while, таким образом при каждой итерации мы получаем совершенно новую переменную, и, другими словами,
новую ссылку. Если бы %rec был определен снаружи цикла, то он бы был всегда одной и той же переменной,
и, при каждой итерации, мы бы перезаписывали одно и то же место в памяти.
Создавая хэш для каждой строки, во входном файле, и присваивая его корректной записи в %users, наша
процедура в конце своей работы возвращает ссылку на %users. Теперь мы готовы выполнить какую-нибудь
настоящую работу.
Для начала, создадим список полных имен (прим.перев.: real names) для каждого из пользователей
операционной системы. И так как все это слишком уж просто для нас, то тут же внесем в алгоритм
пару улучшений. Во-первых, в списке пользователей из /etc/passwd есть несколько особых
учетных записей, которые не являются реальными пользователями. Это root (суперпользователь),
lp (user ID, который обычно используется для выполнения административных задач,
связанных с обслуживанием принтера), а также другие учетные записи, ориентированные на выполнение
прочих задач. Предполагая, что мы можем обнаружить такие записи, по факту отсутствия информации
в поле "полное имя", мы не будем включать их из результирующий список. Во-вторых, будем предполагать,
что в исходном файле полные имена следуют формату <имя> <фамилия>. Мы будем печатать их в виде
<фамилия>, <имя>, а также сортировать их по фамилии. Вот этот скрипт:
1: use strict;
2:
3: my $users = read_passwd();
4:
5: my @names;
6: foreach (keys %) {
7: next unless $users->{fullname};
8:
9: my ($forename, $surname) = split(/\s+/, $users->{fullname}, 2);
10:
11: push @names, "$surname, $forename";
12: }
13:
14: print map { "$_\n" } sort @names;
Большая часть этого скрипта вполне понятна. Объясним ключевые строки:
Строка 6 раз за разом получает значение следующего ключа хэша %users.
Строка 7 пропускает записи, которые не имеют полного имени, т.е. игнорируют специальные учетные записи.
Строка 9 расщепляет полное имя, используя пробельный разделитель. (Заметим, что мы сделали некоторые предположения относительно формата полных имен. Этот алгоритм предполагает, что первое слово в имени -- имя, а все остальное -- фамилия. Если полное имя не следует этому формате, то все будет очень плохо. Например, представьте себе, что случится если именами будут "Dame Elizabeth Taylor" или "Randal L. Schwartz.". Т.е. очень важно знать как выглядят ваши данные.). Эта строка ограничивает число элементов в результирующем списке.
Строка 11 меняет местами имя и фамилию, и помещает результат в другой массив.
Строка 14 печатает массив имен в сортированном порядке.
Предположим теперь, что нам необходимо сформировать отчет о пользователях, которые
используют Bourne shell (/bin/sh). Наверное мы задумали отослать им всем email,
в котором предложим им использовать более современный bash. Мы можем написать
нечто похожее на это:
1: use strict;
2:
3: my $users = read_passwd();
4:
6: foreach (keys %) {
7: print "$_\n" if $users->{shell} eq '/bin/sh';
8: }
И снова скрипт весьма прост. Основная работа выполняется в строке 7. Эта строка сравнивает
значение в $users->{shell} со строкой "/bin/sh", и если обнаруживает совпадение,
то печатает значние текущего ключа (которое явлется именем пользователя). Заметим, что можно
выбрать вариант, с использованием регулярного выражения, например, такого вида:
print "$_\n" if $users->{shell} =~ m|^/bin/sh$|
Если производительность критична, то можно оценить скоростные характеристики этих двух решений, и выбрать наилучшее. Другими словами решение, которое вы выберете суть персонального предпочтения.
Использование регулярных выражений для преобразования данных -- это весьма мощный инструментарий,
и, конечно, как и всякий продвинутый механизм, он открыт для злоупотреблений. В качестве примера,
кратко рассмотрим модуль Text::Bastardize, который доступен на CPAN по следующему URL:
http://search.cpan.org/search?dist=Text-Bastardize. Этот модуль используя невинную часть текста
воспроизводит ее различными все более и более эксцентричными способами. Полный набор доступных
преобразований в текущей (на момент написания книги 0.06) версии следующий:
изменяя "you" на "u", и "are" на "r", а также производит множество других преобразований.
английского языка, в котором первый слог слова перемещается в конец слова, и добавляется звук "ay".
обитателями Internet (the d00dz who deal in k3wl war3z).
буквой, которая отстоит от кодируемой на 13 позиций в алфавите. Этот метод зачастую используется в новостных группах, чтобы маскировать потенциальное раскрытие сюжета кинофильмов, либо может оскорбить случайного читателя.
некоторых гласных звездочками.
но все буквы кроме первой и последней удаляются, и замещаются числом удаленных букв.
Конечно вряд ли такой модуль будет использоваться для чего-либо, кроме как для демонстрации примеров по механизму преобразования текста, но тем не менее это очень хороший пример, и ознакомление с кодом этого модуля может быть очень и очень полезным.
В качестве примера использования этого модуля приведем скрипт, который выполняет все описанные преобразования, над
частью текста, который был получен с STDIN. Отметим, что преобразуемая часть текста устанавливается с помощью
функции charge:
#!/usr/perl/bin/perl -w
use strict;
use Text::Bastardize;
my $text = Text::Bastardize->new;
print 'Say something: ';
while (<STDIN>) {
chomp;
$text->charge($_);
foreach my $xfm (qw/rdct pig k3wlt0k rot13 rev censor n20e/) {
print "$xfm: ";
print eval "\$text->$xfm";
print "\n";
}
}
Лучшим местом, где можно получить исчерпывающую информацию о регулярных выражениях,
является страница руководства perlre, которая поставляется с каждым дистрибутивом
Perl. Обратиться к ней можно, набрав
perldoc perlre
в вашей командной строке.
Кроме того, вы можете получить и дополнительную информацию, в книге Mastering Regular Expressions, автор Jeffrey Friedl (издательство O'Reilly).
substr, index и uc.
m//) и замены (s///) текста.
<< Глава 3. Полезные Perl-идиомы | Data Munging With Perl | '''Часть вторая. Обработка данных''' >>