www.web-mastering.ru - ресурс для веб-мастера
реклама на нашем сайте ($5 / месяц)
статей: 208
электронных книг: 8
прислать новую статью/книгу

Глава 17. Работа с пользовательскими базами данных.

на главную
к началу раздела

урок: предыдущий следующий

DBM-базы данных и DBM-хеши.

В большинстве UNIX-систем есть стандартная библиотека, которая называется DBM. Эта библиотека представляет собой простую систему управления базами данных, которая позволяет программам записывать набор пар ключ-значение в пару файлов. В этих файлах хранятся значения базы данных в промежутках между вызовами программ, использующих ее, и эти программы могут вводить в базы данных новые значения, обновлять существующие и удалять старые.

Библиотека DBM довольно проста, но, учитывая ее доступность, некоторые системные программы активно используют эту библиотеку для своих довольно скромных нужд. Например, sendmail (а также ее варианты и производные) хранит базу данных aliases (соответствие адресов электронной почты и имен получателей) как DBM-базу данных. Самое популярнее ПО телеконференций Usenet использует DBM-базу данных для хранения информации о текущих и недавно просмотренных статьях. Главные файлы базы данных Sun NTS (урожденной YP) также хранятся в формате DBM.

Perl обеспечивает доступ к такому же механизму DBM довольно умным способом: посредством процесса, похожего на открытие файла, с DBM-базой данных можно связать хеш. Этот хеш (называемый DBM-массивом) используется для доступа к DBM-базе данных и внесения в нее изменений.

Создание нового элемента в этом массиве влечет за собой немедленное изменение в базе данных. Удаление элемента приводит к удалению значения из DBM-базы данных и т.д.*

Размер, количество и вид ключей и значений в DBM-базе данных ограничены. В зависимости от того, какой версией библиотеки DBM вы пользуетесь, эти же ограничения могут иметь место и для DBM-массива. Подробности см. на man-странице AnyDBM_File. В общем, если вы сумеете сделать так, чтобы и ключи, и значения упаковывались не больше чем в 1000 символов с произвольными двоичными значениями, то все будет нормально.


* Это, по сути дела, просто особый случай использования общего механизма tie. Если вам понадобится что-нибудь более гибкое, обратитесь к man-страницам AnyDBM_File(3), DB_File(3) и perltie(/).

Открытие и закрытие DBM-хешей.

Чтобы связать DBM-базу данных с DBM-массивом, применяется функция dbmopen, которая используется следующим образом:

dbmopen(%ИМЯ МАССИВА, "имя_DВМ-фармата", $режим)

Параметр %имя_массива — это имя Perl-хеша. (Если в данном хеше уже есть значения, они выбрасываются.) Хеш соединяется с DBM-базой данных, заданной параметром имя_DMB-файла. Она обычно хранится на диске в виде пары файлов с именами имя_DBM-файла.dir и имя_ОВМ-файла.pag.

Параметр $режим — это число, которое соответствует битам прав доступа к названным двум файлам, если файлы создаются заново. Обычно оно указывается в восьмеричном формате; часто используемое значение 0644 предоставляет право доступа только для чтения всем, кроме владельца, который имеет право на чтение и запись. Если эти файлы существуют, данный параметр не действует. Например:

dbmopen(%FRED, "mydatabase", 0644); # открыть %FRED на mydatabase

Этот вызов связывает хеш %fred с файлами mydatabase. dir и ту database.pag, расположенными в текущем каталоге. Если эти файлы не существуют, они создаются с правами доступа 0644, которые модифицируются с учетом текущего значения, установленного командой umask.

Функция dbmopen возвращает значение "истина", если базу данных можно открыть или создать; в противном случае возвращается "ложь" — точно так же, как при вызове функции open. Если вы не хотите создавать файлы, используйте вместо параметра $режим значение undef. Например:

dbmopen(%A,"/etc/xx",undef) || die "cannot open DBM /etc/xx";

Если, как в данном случае, (^awibi/etc/xx.dirvi/etc/xx.pagoTKpbnb нельзя, то вызов dbmopen возвращает значение "ложь" без попытки создать эти файлы.

DBM-массив остается открытым в течение выполнения всей программы. Когда программа завершается, разрывается и связь с DBM-базой данных. Эту связь можно разорвать и способом, близким к закрытию дескриптора файла — с помощью функции dbmclose:

dbmclose(%A);

Как и функция close, dbmclose возвращает значение "ложь", если что-нибудь происходит не так, как надо.

Использование DBM-хеша.

После открытия базы данных обращения к DBM-хешу преобразуются в обращения к базе данных. Изменение значения в хеше или ввод в него нового значения вызывает немедленную запись соответствующих элементов в файлы на диске. Например, после открытия массива %fred из предыдущего примера мы можем обращаться к элементам базы данных, вводить в нее новые элементы и удалять существующие:

$FRED{"fred"} = "bedrock"; # создать (или обновить) элемент
delete $FRED{"barney"}; # удалить элемент базы данных
foreach $key (keys %FRED) { # пройти по всем значениям
print "$key has value of $FRED{$key}\n";
}

Последний цикл должен просмотреть весь файл на диске дважды: один раз для выборки ключей, а второй — для поиска значений, соответствующих этим ключам. Если вы просматриваете DBM-хеш, то более эффективным способом с точки зрения эксплуатации диска является использование операции each, которая делает всего один проход:

while (($key, $value) = each(%FRED)) {
print "$key has value of $value\n";
}

Если вы обращаетесь к системным DBM-базам данных, например к базам данных, созданным системами sendmail и NIS, вы должны иметь в виду, что в плохо написанных С-программах в конце строк иногда стоит символ NUL (\0). Программам библиотеки DBM этот NUL не нужен (они обрабатывают двоичные данные с помощью счетчика байтов, а не строки с символом NUL на конце), поэтому он хранится как часть данных. В таком случае вы должны добавлять символ NUL в конец своих ключей и отбрасывать NUL, стоящий в конце возвращаемых значений, иначе данные не будут иметь смысла.

Например, чтобы найти имя merlyn в базе данных псевдонимов, можно сделать так:

dbmopen(%ALI, "/etc/aliases", undef) || die "no aliases?";
$value = $ALI {"merlyn\0"}; # обратите внимание на добавленный NUL
chop ($value); # удалить добавленный NUL
print "Randal's mail is headed for: $value\n"; # показать результат

В вашей версии UNIX база данных псевдонимов может храниться не в каталоге /etc, а в каталоге /usr/lib. Чтобы выяснить, где именно она хранится, придется провести маленькое расследование. Новые версии sendmail этим NUL-дефектом не страдают.

Базы данных произвольного доступа с записями фиксированной длины.

Еще одна форма хранения данных — файл на диске, предназначенный для записей фиксированной длины. В этой схеме данные состоят из ряда записей одинаковой длины. Нумерация этих записей либо не имеет значения, либо определяется по какой-нибудь схеме индексации.

Например, у нас может быть ряд записей со следующими данными: 40 символов — имя, один символ — инициал, 40 символов — фамилия и двухбайтовое целое — возраст. Таким образом, длина каждой записи составляет 83 байта. Если бы мы читали все эти данные в базе данных, то делали бы это порциями по 83 байта до тех пор, пока не добрались до конца. Если бы мы хотели перейти к пятой записи, то мы пропустили бы четыре раза по 83 байта (332 байта) и прочитали бы непосредственно пятую запись.

Perl поддерживает программы, которые используют файл с подобными записями. Помимо того, что вы уже знаете, понадобятся еще несколько операций:

  1. Открытие файла на диске для чтения и записи.
  2. Переход в этом файле на произвольную позицию.
  3. Выборка данных фиксированной длины, а не до следующего символа новой строки.
  4. Запись данных блоками фиксированной длины.

В функции open перед спецификацией, задающей способ открытия файла (для чтения или записи), необходимо записать знак плюс, указав таким образом, что данный файл в действительности открывается и для чтения, и для записи. Например:

open(A, "+<b"); # открыть файл b для чтения-записи (ошибка, если файл отсутствует)
open(C, "+>d"); # создать файл d с доступом для чтения-записи
open(E, "+>f"); # открыть или создать файл f с доступом для чтения-записи

Отметим, что все, что мы сделали — это добавили знак плюс к спецификации, задающей направление ввода-вывода данных в файл.

Открыв файл, мы должны перейти на определенную позицию в нем. Это делается с помощью функции seek, которая принимает те же три параметра, что и библиотечная программа. Первый параметр — это дескриптор файла, а второй параметр задает смещение, которое интерпретируется в совокупности с третьим параметром. Как правило, в качестве третьего параметра ставится нуль, чтобы второй параметр задавал абсолютную позицию для следующего чтения из файла или записи в файл. Например, чтобы перейти к пятой записи в дескрипторе файла names (как описано выше), можно сделать так:

seek(NAMES,4*83,0);

После перемещения указателя в файле на нужную позицию следующая операция ввода или вывода будет начинаться с этой позиции. Для вывода используйте функцию print, но не забудьте, что записываемые данные должны иметь строго определенную длину. Чтобы сформировать запись правильной длины, можно воспользоваться функцией pack:

print NAMES pack("A40 A A40 s", $first, $middle, $last, $age);

В данном случае pack задает 40 символов для $first, один символ — для $middle, еще 40 символов — для $last и короткое целое (два байта) для $age. Определенная таким образом запись будет иметь в длину 83 байта и начинаться с текущей позиции в файле.

Наконец, нам нужно узнать, как выбрать конкретную запись. Конструкция <names> возвращает все данные, начиная с текущей позиции і до следующего символа новой строки, однако в нашем случае предполагается, что данные занимают 83 байта и, вероятно, символ новой строки непосредственно в записи отсутствует. Поэтому вместо нее мы используем функцию read, которая по внешнему виду и принципу работы очень похожа на свою UNIX-коллегу:

$count = read(NAMES, $buf, 83);

Первый параметр функции read — дескриптор файла. Второй параметр — это скалярная переменная, в которую будут записаны прочитанные данные. Третий параметр задает количество байтов, которые нужно прочитать. Возвращает функция read количество фактически прочитанных байтов; как правило, оно равно затребованному количеству байтов, если только дескриптор файла открыт и если вы не находитесь слишком близко к концу файла.

Получив эти 83-символьные данные, разбейте их на компоненты с помощью функции unpack:

($first, $middle, $last, $age) = unpack("A40 A A40 s", $buf);

Как видно, строки, определяющие формат, в функциях pack и unpack — одинаковы. В большинстве программ эту строку заносят в переменную, указываемую в начале программы, и даже вычисляют с помощью функции pack длину записей, а не используют везде константу 83:

$names = "A40 A A40 s";
$names_length = length(pack($names)); # вероятно, 83

Базы данных с записями переменной длины (текстовые).

Многие системные базы данных ОС UNIX (и довольно большое число пользовательских баз данных) представляют собой наборы понятных человеку текстовых строк, каждая из которых образует одну запись. Например, каждая строка файла паролей соответствует одному пользователю системы, а строка файла хостов — одному хост-имени.

Корректируются эти базы данных в основном с помощью простых текстовых редакторов. Процедура обновления базы данных состоит из чтения ее в какую-то временную область (память или другой дисковый файл), внесения необходимых изменений и либо записи результата обратно в исходный файл, либо создания нового файла с тем же именем, с одновременным удалением или переименованием старой версии. Этот процесс можно рассматривать как разновидность копирования: данные копируются из исходной базы данных в новую ее версию с внесением изменений в процессе копирования.

Perl поддерживает редактирование такого типа в строчно-ориентированных базах данных методом редактирования на месте. Редактирование на месте — это модификация способа, посредством которого операция "ромб" (<>) считывает данные из списка файлов, указанного в командной строке. Чаще всего этот режим редактирования включается путем установки аргумента командной строки -і, но его можно запустить и прямо из программы, как показано в приведенных ниже примерах.

Чтобы запустить режим редактирования на месте, присвойте значение скалярной переменной $^I. Оно играет важную роль и будет сейчас рассмотрено.

Когда используется конструкция о и переменная $ЛI имеет значение, отличное от undef, к списку неявных действий, которые выполняет операция "ромб", добавляются шаги, отмеченные в приведенном ниже коде комментарием ## inplace ##:

$ARGV = shift @ARGV;
open(ARGV,"<$ARGV");
rename($ARGV,"$ARGV$^I"); ## INPLACE ##
unlink($ARGV); ## INPLACE ##
open(ARGVOUT,">$ARGV"); ## INPLACE ##
select(ARGVOUT); ## INPLACE ##

В результате в операции "ромб" при чтении используется старый файл, а запись в дескриптор файла по умолчанию осуществляется в новую копию этого файла. Старый файл остается в резервной копии, суффикс имени файла которой равен значению переменной $^I. (При этом биты прав доступа копируются из старого файла в новый.) Эти шаги повторяются каждый раз, когда новый файл берется из массива @argv.

Типичные значения переменной $^I — .bak или ~, т.е. резервные файлы создаются почти так же, как это делается в текстовом редакторе. Странное и полезное значение $^I — пустая строка (""), благодаря которой старый файл после редактирования аккуратно удаляется. К сожалению, если система при выполнении вашей программы откажет, то вы потеряете все свои старые данные, поэтому значение "" рекомендуется использовать только храбрецам, дуракам и излишне доверчивым.

Вот как можно путем редактирования файла паролей заменить регистрационный shell всех пользователей на /bin/sh'.

@ARGV = ("/etc/passwd"); # снабдить информацией операцию "ромб"
$^I = ".bak"; # для надежности записать /etc/passwd.bak
while (<>) { # основной цикл, по разу для каждой строки файла
# /etc/passwd
s#:shell$#:/bin/sh#; # заменить shell на /bin/sh
print; # послать выходную информацию в ARGVOUT: новый
# /etc/passwd
}

Как видите, эта программа довольно проста. Однако ее можно заменить всего лишь одной командой с несколькими аргументами командной строки, например:

perl -p -i.bak -e s#:shell$#:/bin/sh# /etc/passwd

Ключ -p охватывает вашу программу циклом while, который включает оператор print. Ключ -і устанавливает значение переменной $^I. Ключ -e определяет следующий аргумент как фрагмент Perl-кода для тела цикла, а последний аргумент задает начальное значение массива @argv.

Более подробно аргументы командной строки рассматриваются в книге Programming Perl и на man-странице perlrun.

Упражнения.

Ответы к упражнениям приведены в приложении А.

  1. Создайте программу, которая открывает базу данных псевдонимов sendmail и выводит на экран все ее элементы.
  2. Создайте две программы: одну для чтения данных с помощью операции о, разбивки их на слова и обновления DBM-файла с запоминанием числа экземпляров каждого слова, а вторую для открытия этого DBM-файла и отображения результатов, рассортированных по количеству экземпляров каждого слова в убывающем порядке. Выполните первую программу по отношению к нескольким файлам и посмотрите, осуществляет ли вторая программа правильную сортировку.

 

Copyright © 2004 www.web-mastering.ru