Перевод 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, чтобы узнать новые мощные трюки и основные технологии.