Next Previous Contents

Unix Review Column 4 -- Небольшая база данных

Randal Schwartz

Сентябрь 1995

Перевод 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]

Здесь левая часть оператора или будет вычислена первой, если операция cmp возвращает -1 или +1, то правая часть оператора ``или'' не вычисляется. Это происходит в случае, если штаты различные. Если штаты одинаковые, то должна вычислиться правая часть (так как левая часть равна 0, что означает ``ложь''), возвращая значение сравнения между городами (трудная задача в настоящей жизни).

Своим последним фокусом я дам вам полную программу, которая печатает только телефонные номера тех людей или компаний, которые имеют телефонные номера. Сортировка в порядке телефонных номеров:


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


Next Previous Contents