Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>
Perl имеет солидную коллекцию операторов и возможностей по разбору и выборке данных. В этой статье я собираюсь построить разборщик для небольшой базы данных, хранимой в полу-свободной форме, вроде списка рассылки.
Сначала давайте посмотрим на пример таких данных:
Name: Randal L. Schwartz
Company: Stonehenge Consulting Services
Street: 4470 SW Hall Suite 107
City: Beaverton
State: Oregon
Zip: 97005
Phone: 503-777-0095
Name: John Big-booty
City: San Angeles
State: California
Zip: 93021
Phone: 291-555-2213
Company: Lips, Inc.
Street: 4221 Wayback Lane
City: Springfield
State: Kansas
Zip: 65554
Каждая запись отделена от следующей пустой строкой. Заметьте, что не все поля присутствуют в каждой записи, значит нам придётся это учесть при чтении данных в базу. (Пожалуйста, обратите внимание, что эти записи служат лишь иллюстрацией, любые совпадения с реальными людьми, мёртвыми или живыми --- чистая случайность).
Я собираюсь положить данные в список ассоциативных массивов. (Это было не возможно в старых версиях Perl, так что если у вас эти примеры не работают, то убедитесь, что у вас версия Perl не ниже 5.000). Каждый ассоциативный массив представляет собой одну запись из базы данных. Ключи в этом ассоциативном массиве являются названиями полей (такие как ``Name'' или ``City''), а значениями являются значения соответствующих полей.
Так, для начала нам надо разбить данные на записи. Простейший путь, это
использовать преимущества ``режима параграфа'' во время чтения файла. В
режиме параграфа, каждая ``строка'', читаемая из файла, представляет собой
несколько строк, до следующей пустой строки или конца файла. (Разве это не
удобно, что есть такой режим чтения в Perl для таких данных? Кто-нибудь,
возможно, мог бы обвинить меня в выборе такой структуры, чтобы
соответствовать такому режиму чтения в Perl, но я не жалуюсь :-) Режим
параграфа включается присваиванием переменной $/ пустой строки:
#!/usr/bin/perl
$/ = "";
while (<>) {
## one entry per $_ here
}
Ух ты! Мы уже почти сделали (нифига!). Текст в $_ будет
выглядеть как несколько строк, разделённых \n. Нам надо разобрать эти
данные, что наиболее легко сделать используя многострочный режим оператора
соответствия. Многострочный режим соответствия (указывается символом ``m''
в конце регулярного выражения) позволяет ''стрелке вверх'' -- ``^''
соответствовать не только началу строки, но и любому символу новой строки в
строке. Теперь, чтобы получить поля ``Name'', ``Street'' и ``City'', мы
могли бы написать что-то вроде:
#!/usr/bin/perl
$/ = "";
while (<>) {
%entry = (); # initialize empty entry
if (/^Name: (.*)/m) {
$entry{"Name"} = $1;
}
if (/^Street: (.*)/m) {
$entry{"Street"} = $1;
}
if (/^City: (.*)/m) {
$entry{"City"} = $1;
}
## save %entry here
}
Как вы можете видеть, текст для разбора записи выглядит повторяющимся, и я ещё не для всех имён полей написал. Это было первое приближение, так что, давайте, выкинем это и попробуем что-нибудь более общее.
Легче рассматривать данные, как ``поле: значение'', и получить оба одновременно. Попробуем в этом направлении:
#!/usr/bin/perl
$/ = "";
while (<>) {
# for each entry:
%entry = (); # initialize
foreach (split /\n/) {
# for each line in entry:
$entry{$1} = $2
if /^(.*): (.*)$/;
}
## save %entry here
}
Ага! Это уже лучше. Тем не менее, здесь есть ошибочка. Вы можете сказать, что было до начала чтения? Ничего? Хорошо, предположим значение содержит двоеточие внутри:
Company: White Elephants: A division of Trunks-R-Us
Первое ``.*'' будет соответствовать части строки до второго двоеточия, потому, что по умолчанию, квантификаторы регулярных выражений ''жадные''. Чтобы это исправить, просто поставим ``?'' перед первым квантификатором, который говорит, что ищется соответствие настолько малого числа символов, насколько возможно (быть ``скупым''), а не настолько много, насколько возможно (``жадным''). (Для экономии места, я не буду повторять всё программу, смотрите изменения ниже).
Заметьте, что порядок данных или пропуск каких-либо полей исключены. Я мог бы поставить город перед именем, или ещё где.
Теперь у меня есть правильный %entry для каждой записи. И мне
надо это где-то сохранить. Я не могу сохранить настоящий ассоциативный
массив в список, но я могу сохранить указатель на ассоциативный
массив, используя ссылку. Первой мыслью, вы могли бы сделать так:
#!/usr/bin/perl
$/ = "";
@entries = (); # master list
while (<>) {
# for each entry:
%entry = (); # initialize
foreach (split /\n/) {
# for each line in entry:
$entry{$1} = $2
if /^(.*?): (.*)$/;
}
# save %entry to @entries:
push @entries, \%entry;
}
И это на самом деле получится список ссылок на ассоциативные
массивы. Тем не менее, это список ссылок на один и тот же массив
%entry! Вместо этого, нам надо делать новый анонимный
ассоциативный массив для каждой записи. Есть пара способов сделать
это. Один из них --- использовать данные из %entry внутри
инициализации нового ассоциативного массива:
#!/usr/bin/perl
$/ = "";
@entries = (); # master list
while (<>) {
# for each entry:
%entry = (); # initialize
foreach (split /\n/) {
# for each line in entry:
$entry{$1} = $2
if /^(.*?): (.*)$/;
}
# save %entry to @entries:
push @entries, {%entry};
}
Другой способ, возможно более элегантный, создавать новый анонимный
массив и работать с ним, избавившись от %entry.
#!/usr/bin/perl
$/ = "";
@entries = (); # master list
while (<>) {
# for each entry:
$ref_entry = {}; # anon hash
foreach (split /\n/) {
# for each line in entry:
$$ref_entry{$1} = $2
if /^(.*?): (.*)$/;
}
# save this entry to @entries:
push @entries, $ref_entry;
}
Это способ нравится мне больше, даже не смотря на синтаксис
разыменования ссылки на анонимный ассоциативный массив, выглядящий
немного жутко в середине цикла. Синтаксис $$ref_entry{$1}
говорит, что надо использовать $ref_entry как ``имя''
ассоциативного массива, и смотреть на ключ $1 в этом ассоциативном
массиве.
Какой бы способ вы не предпочли, мы теперь имеем то, ради чего всё это начали --- список ссылок на анонимные ассоциативные массивы, представляющие оригинальные записи.
Своим первым трюком я буду печатать данные в форме списка рассылки. Чтобы это сделать, мне придётся пройти по данным, при этом искать те, которые содержат полные адреса. Давайте попробуем:
#!/usr/bin/perl
[parsing code from above goes here]
foreach $ref (@entries) {
$name = $$ref{"Name"};
$company = $$ref{"Company"};
$street = $$ref{"Street"};
$city = $$ref{"City"};
$state = $$ref{"State"};
$zip = $$ref{"Zip"};
next unless defined $street and defined $city and defined $state;
print "$name\n$street\n";
print "$city, $state $zip\n";
}
авторский текст был 'next unless defined $address;', я заменил его как явно ошибочный. -- Прим. переводчика
Здесь я ищу только те записи, которые имеют адрес, потому, что в моей
базе данных есть записи, в которых указан только номер
телефона. Запись $$ref{"Something"} указывает использовать
$ref как ``имя'' ассоциативного массива. Напоминаю, что каждый
элемент списка @entries является ссылкой на ассоциативный массив.
Записи будут выводиться в порядке, в котором я их записал. Что, если я захочу их выводить в порядке почтовых индексов? Не проблема (хорошо, не большая проблема) --- просто отсортируем список записей перед использованием.
#!/usr/bin/perl
[parsing code from above]
@entries = sort {
$$a{"Zip"} <=> $$b{"Zip"}
} @entries;
[printing code from above]
В этом случае используется определённый пользователем (то есть мной)
порядок сортировки списка @entries. Определяемой пользователем
функции сортировки передаётся два элемента сортируемого списка (здесь
@entries) как переменные $a и $b. Поскольку
каждый из этих элементов ссылается на ассоциативный массив, то я могу
раскрыть ссылку на него, чтобы получить элемент ``Zip'' из каждого.
Если бы я захотел что-то более сложное, например, чтобы первым был штат и затем город в штате, то мне пришлось бы написать немного больше:
#!/usr/bin/perl
[parsing code from above]
@entries = sort {
$$a{"State"} cmp $$b{"State"} or
$$a{"City"} cmp $$b{"City"}
} @entries;
[printing code from above]
Здесь левая часть оператора
Своим последним фокусом я дам вам полную программу, которая печатает только телефонные номера тех людей или компаний, которые имеют телефонные номера. Сортировка в порядке телефонных номеров:
#!/usr/bin/perl
$/ = "";
@entries = (); # master list
while (<>) {
# for each entry:
$ref_entry = {}; # anon hash
foreach (split /\n/) {
# for each line in entry:
$$ref_entry{$1} = $2
if /^(.*?): (.*)$/;
}
# save this entry to @entries:
push @entries, $ref_entry;
}
@entries = sort {
$$a{"Phone"} cmp $$b{"Phone"}
} @entries;
foreach $ref (@entries) {
$phone = $$ref{"Phone"};
next unless defined $phone;
$name = $$ref{"Name"};
$company = $$ref{"Company"};
print "$name ($company) $phone\n";
}
Эта программа построена из кусочков, обсуждавшихся в предыдущих фрагментах, лишь немного подправлена для достижения новой цели.
Богатство языковых возможностей Perl, которое, возможно, может быть пугающим сначала, позволяет обладать огромной гибкостью в дальнейшем, если потратите время на их изучение. К счастью, вы увидели несколько новых мощных трюков в этой статье. Продолжайте чтение статей в Perl Column, чтобы узнать новые мощные трюки и основные технологии.