HTML page

Глава 8 Содержимое файлов

Содержимое файлов

Введение

До революции UNIX всевозможные источники и приемники данных не имели ничего общего. Чтобы две программы пообщались друг с другом, приходилось идти на невероятные ухищрения и отправлять в мусор целые горы перфокарт. При виде этой компьютерной Вавилонской башни норой хотелось бросить программирование и подыскать себе менее болезненное хобби - например, податься в секту флаггелаитов. В наши дни этот жестокий и нестандартный стиль программирования в основном ушел в прошлое. Современные операционные системы всячески стараются создать иллюзию, будто устройства ввода/вывода, сетевые подключения, управляющие данные процессов, другие программы, системные консоли и даже терминалы пользователей представляют собой абстрактные потоки байтов, именуемые файлами. Теперь можно легко написать программу, которая нисколько не заботится о том, откуда взялись ее входные данные и куда отправятся результаты. Поскольку чтение и запись данных осуществляется через простые байтовые потоки, любая программа может общаться с любой другой программой. Трудно переоценить всю элегантность и мощь такого подхода. Пользователи перестают зависеть от сборников магических заклинаний JCL (или СОМ) и могут собирать собственные нестандартные инструменты, используя простейшее перенаправление ввода/вывода и конвейерную обработку. Интерпретация файлов как неструктурированных байтовых потоков однозначно определяет круг возможных операций. Вы можете читать и записывать последовательные блоки данных фиксированного размера в любом месте файла, увеличивая его размер при достижении конца. Чтение/запись блоков неременной длины (например, строк, абзацев и слов) реализуется в Perl на базе стандартной библиотеки ввода/вывода С. Что нельзя сделать с неструктурированным файлом? Поскольку вставка и удаление байтов возможны лишь в конце файла, вы не сможете вставить или удалить записи, а также изменить их длину. Исключение составляет последняя запись, которая удаляется простым усечением файла до конца предыдущей записи. В остальных случаях приходится использовать временный файл или копию файла в памяти. Если вам приходится часто заниматься этим, вместо обычных файлов лучше подойдет база данных (см. главу 14 "Базы данных"). Самый распространенный тип файлов - текстовые файлы, а самый распространенный тип операций с ними - построчное чтение и запись. Для чтения строк используется оператор о (или его внутренняя реализация, readline), а для записи - функция print. Эти способы также могут применяться для чтения или записи любых блоков с конкретным разделителем. Строка представляет собой запись с разделителем "\п". При достижении конца файла оператор о возвращает undef или ошибку, поэтому его следует использовать в цикле следующего вида:
while (defined ($line = )) {
chomp $line;
$size = length $line;
print "$size\n"; # Вывести длину строки
}
Поскольку эта операция встречается довольно часто, в Perl для нее предусмотрена сокращенная запись, при которой строки читаются в $_ вместо $line. Пере менная $_ используется по умолчанию и в других строковых операциях и вообще куда удобнее, чем может показаться на первый взгляд:
while () {
chomp;
print length, "\n"; # Вывести длину строки
}
В скалярном контексте оператор о читает следующую строку. В списковом контексте он читает оставшиеся строки:
@lines = ;

При чтении очередной записи через файловый манипулятор о увеличивает значение специальной переменной $. (текущий номер входной записи). Переменная сбрасывается лишь при явном вызове close и сохраняет значение при повторном открытии уже открытого манипулятора. Заслуживает внимания и другая специальная переменная - $/, разделитель входных записей. По умолчанию ей присваивается "\п", маркер конца строки. Ей можно присвоить любое желаемое значение - например, "\0" для чтения записей, разделяемых нуль-байтами. Для чтения целых абзацев следует присвоить $/ пустую строку, "". Это похоже на присваивание "\п\п", поскольку для разделения записей используются пустые строки, однако "" интерпретирует две и более смежных пустых строки как один разделитель, а "\п\п" в таких случаях возвращает пустые записи. Присвойте $/ неопределенное значение, чтобы прочитать остаток файла как одну скалярную величину:
undef $/;
$whole file = ; # Режим поглощения
Запуск Perl с флагом -0 позволяет задать $/ из командной строки:
% perl -040 -е '$word = о; print "First word is $word\n";'

Цифры после -О определяют восьмеричное значение отдельного символа, который будет присвоен $/. Если задать недопустимое значение (например, -0777), Perl присваивает $/ неопределенное значение undef. Если задать -00, $/ присваивается "". Ограничение в один восьмеричный символ означает, что вы не сможете присвоить $/ многобайтовую строку - например, "%%\п" для чтения файлов программы fortune. Вместо этого следует воспользоваться блоком BEGIN:
% perl -ne 'BEGIN < $/="%%\n" } chomp; print if /unix/i' fortune.dat

Запись строк и других данных выполняется функцией print. Она записывает своп аргументы в порядке указания и по умолчанию не добавляет к ним разделители строк или записей: print HANDLE "One", "two", "three"; # "Onetwothree" print "Baa baa black sheep.\n";
# Передается выходному манипулятору
# по умолчанию
Между манипулятором и выводимыми данными не должно быть запятых Если поставить запятую, Perl выдает сообщение об ошибке "No comma allowed after filehandle". По умолчанию для вывода используется манипулятор STDOUT. Для выбора другого манипулятора применяется функция select (см. главу 7 "Доступ к файлам"). Во всех системах строки разделяются виртуальным разделителем "\п", который называется переводом строки (newline). He существует такого понятия, как символ перевода строки. Это всего лишь иллюзия, которая по общему сговору поддерживается операционной системой, драйверами устройств, библиотеками С и Perl. Иногда это приводит к изменению количества символов в прочитанных или записываемых строках. Подробности заговора изложены в рецепте 8.11. Записи фиксированной длины читаются функцией read. Функция получает три аргумента: файловый манипулятор, скалярную переменную и количество читаемых байт. Возвращается количество прочитанных байт, а в случае ошибки - undef. Для записи используется функция print:
$rv = read(handle, $buffer, 4096)
or die "Couldn't read from HANDLE : $!\n";
it $rv - количество прочитанных байт, # $buffer содержит прочитанные данные
Функция truncate изменяет длину файла, который задается с помощью манипулятора или по имени; Функция возвращает true, если усечение прошло успешно, и false в противном случае:
truncate(HANDLE, $length)
or die "Couldn't truncate: $!\n"; .;
truncate("/tmp/$$.pid", $length)
or die "Couldn't truncate: $!\n";

Для каждого файлового манипулятора отслеживается текущая позиция в файле. Операции чтения/записи выполняются именно в этой позиции, если при открытии не был указан флаг 0_APPEND (см. рецепт 7.1). Чтобы узнать текущую позицию файлового манипулятора, воспользуйтесь функцией tell, а чтобы задать ее - функцией seek. Поскольку стандартная библиотека ввода/вывода стремится сохранить иллюзию того, что "\п" является разделителем строк, вы не сможете обеспечить переносимый вызов seek для смещений, вычисляемых посредством подсчета символов. Вместо этого seek следует вызывать только для смещений, возвращаемых tell:

$pos = tell(datafile);
print "I'm $pos bytes from the start of DATAFILE.\n";
Функция seek получает три аргумента: файловый манипулятор, новое смещение (в байтах) и число, определяющее интерпретацию смещения. Если оно равно О, смещение отсчитывается от начала файла (в соответствии со значениями, возвращаемыми tell); I - от текущей позиции (положительное число означает прямое перемещение в файле, а отрицательное - обратное); 2 - от конца файла.
seek(LOGFILE, 0, 2) or die "Couldn't seek to the end: $!\n";
seek(DATAFILE, $pos, 0) or die "Couldn't seek to $pos: $!\n";
seek(OUT, -20, 1) or die "Couldn't seek back 20 bytes: $!\n";

Все сказанное выше относится к буферизованному вводу/выводу. Другими словами, операции о, print, read, seek и tell используют буферы для повышения скорости. В Perl также предусмотрены небуферизованные операции ввода/ вывода: sysopen, sysread, syswrite, sysseek и close. Буферизация, sysopen и close рассматриваются в главе 7. Функции sysread и syswrite отличаются от своих аналогов, о и print. Они получают одинаковые аргументы - файловый манипулятор; скалярную переменную, с которой выполняется чтение или запись; и количество читаемых или записываемых байт. Кроме того, они могут получать необязательный четвертый аргумент - смещение внутри скалярной переменной:
$written = syswrite(datafile, $mystring, length($mystring));
die "syswrite failed: $!\n" unless $wntten == length($mystring);
$read = sysread(infile, $block, 256, 5);
warn "only read $read bytes, not 256" if 256 != $read;

Функция syswrite посылает содержимое $mystring в DATAFILE. При вызове sysread из INFILE читаются 256 символов, сохраняемых с шестого символа ь $block, при этом первые пять символов остаются без изменений. И sysread и syswrite возвращают фактическое количество переданных байт; оно может не совпадать с тем, которое пытались передать вы. Например, файл содержал меньше данных, чем вы рассчитывали, и чтение получилось укороченным. Может быть, произошло переполнение носителя, на котором находился файл. А может быть, процесс был прерван на середине записи. Stdio заботится о завершении записи в случае прерывания, но при вызовах sysread и syswrite этим придется заняться вам. Пример приведен в рецепте 9.3. Функция sysseek является небуферизованной заменой для seek и tell. Она получает те же аргументы, что и seek, но возвращает новую позицию при успешном вызове или undef в случае ошибки. Текущая позиция внутри файла определяется следующим образом:
$pos = sysseek(handle, 0, 1); # Не изменять позицию
die "Couldn't sysseek: $!\n" unless defined $pos;

Мы описали базовые операции с файлами, которые находятся в вашем распоряжении. Искусство программирования как раз и заключается в применении простейших операций для решения сложных проблем - например, определения количества строк в файле, перестановки строк, случайного выбора строки из файла, построения индексов и т. д.

8.1. Чтение строк с символами продолжения

Проблема

Имеется файл с длинными строками, которые делятся на две и более строки. Символ \ означает, что данная строка продолжается на следующей. Вы хотите объединить разделенные строки. Подобное разделение длинных строк на короткие встречается в make-файлах, сценариях командного интерпретатора, конфигурационных файлах и многих языках сценариев.

Решение

Последовательно объединяйте прочитанные строки, пока не встретится строка без символа продолжения:
while (defined($line = ) ) { chomp $line;
if ($line =~ s/\\$//) { $line .= ;
redo unless eof(FH);
} # Обработать полную запись в $line
}

Комментарий

Рассмотрим пример входного файла:
DISTFILES = $(dist_common) $(sources) $(headers) \ .
$(TEXINFOS) $(INFOS) $(MANS) $(DATA) DEP_DISTFILES = $(dist_common) $(sources)
$(HEADERS) \
$(TEXINFOS) $(INFO_DEPS) $(MANS) $(DATA) \
$(EXTRA_DIST)
Вы хотите обработать текст, игнорируя внутренние разрывы строк. В приведенном примере первая запись занимает две строки, вторая - три строки и т. д. Алгоритм работает следующим образом. Цикл while читает строки, .которые могут быть, а могут и не быть полными записями, - они могут заканчиваться символом \ (и переводом строки). Оператор подстановки s/// пытается удалить \ в конце строки. Если подстановка заканчивается неудачей, значит, мы нашли строку без \. В противном случае мы читаем следующую запись, приписываем ее к накапливаемой переменной $line и возвращаемся к началу цикла while с помощью redo. Затем выполняется команда chomp. У файлов такого формата имеется одна распространенная проблема - невидимые пробелы между \ и концом строки. Менее строгий вариант подстановки выглядит так:
if ($line =- s/\\\s*$//) {
# Как и прежде
}
К сожалению, даже если ваша программа проищет мелкие погрешности, существуют и другие, которые этого не делают. Будьте снисходительны к входным данным и строги - к выходным.

> Смотри также ---------------------------'o---
Описание функции chomp в perlfunc(1); описание ключевого слова redo в разделе "Loop Control" perlsyn(1).

8.2. Подсчет строк (абзацев, записей) в файле

Проблема

Требуется подсчитать количество строк в файле.

Решение

Во многих системах существует программа we, подсчитывающая строки в файле:
$count = 'we -I < $file';
die "we failed: $?" if $?;
chomp($count);

Кроме того, можно открыть файл и последовательно читать строки до конца, увеличивая значение счетчика:
open(FILE, "< $file") or die "can't open'$file: $!";
$count++ while ;
# $count содержит число прочитанных строк
Самое быстрое решение предполагает, что строки действительно завершаются "\n":
$count += tr/\n/\n/ while sysread(file, $_, 2 ** 16);

Комментарий

Хотя размер файла в байтах можно определить с помощью -s $file, обычно полученная цифра никак не связана с количеством строк. Оператор -s рассматривается в главе 9 "Каталоги". Если вы не хотите или не можете перепоручить черную работу другой программе, имитируйте работу we - самостоятельно откройте и прочитайте файл:
open(FILE, "< $file") or die "can't open $file: $!";
$count++ while ;
# $count содержит число прочитанных строк
Другой вариант выглядит так:
open(FILE, "< $file") or die "can't open $file: $!";
for ($count=0; ; $count++) { }

Если вы не читаете из других файлов, можно обойтись без переменной $count. Специальная переменная $. содержит количество прочитанных строк с момента последнего явного вызова close для файлового манипулятора:
while ;
$count = $.;

В этом варианте все записи файла последовательно читаются без использования временных переменных. Чтобы подсчитать абзацы, присвойте перед чтением глобальному разделителю входных записей $/ пустую строку (""), и тогда оператор о будет считывать не строки, а целые абзацы:
$/=''; # Включить режим чтения абзацев
open(FILE, $file) or die "can't open $file: $!";
1 while ;
$para_count = $.;


> Смотри также -------------------------------
Описание специальной переменной $/ в perlvaf(1) введение главы 9; страница руководства wc(1).

8.3. Обработка каждого слова в файле

Проблема

Требуется выполнить некоторую операцию с каждым словом файла, по аналогии с функцией to reach.

Решение

Разделите каждую строку но пропускам с помощью функции split: while (<>) {
for $chunk (split) {
# Сделать что-то с $chunk
}
}
Или воспользуйтесь оператором т//д для последовательного извлечения фрагментов строки:
while (<>) {
while ( /(\w[\w'-]*)/g ) {
# Сделать что-то с $1
}
}

Комментарий

Сначала необходимо решить, что же подразумевается под "словом". Иногда это любые последовательности символов, кроме пропусков; иногда - идентификаторы программ, а иногда - слова английского языка. От определения зависит и используемое регулярное выражение. Два варианта решения, приведенные выше, работают по-разному. В первом варианте шаблон определяет, что не является словом. Во втором варианте все наоборот - шаблон решает, что им является. На основе этой методики нетрудно подсчитать относительные частоты всех слов в файле. Количество экземпляров каждого слова сохраняется в хэше: # Подсчет экземпляров слов в файле %seen =();
while (<>) {
while ( /(\w['\w-]*)/g ) { $seen{lc $1}++;
}
}
# Отсортировать выходной хэш по убыванию значений foreach
$word ( sort { $seen{$b} <=> $seen{$a}-} keys %seen)
{ pnntf "%5d %s\n", $seen{$word}, Sword;
}
Чтобы программа подсчитывала количество строк вместо слов, уберите второй цикл while и замените его на $seen{lc $_}++:
# Подсчет экземпляров строк в файле %seen =();
while (<>) {
$seen{lc $_}++;
} foreach $line ( sort { $seen{$b} <=> $seen{$a} } keys %seen ) {
printf "%5d %s", $seen{$line}, $line;
}

Порой слова могут выглядеть довольно странно - например, "M.I.Т", "Micro-$oft", "o'clock", "49ers", "street-wise", "and/or", "&", "c/o", "St.", "TschuB" или "Nino". Помните об этом при выборе шаблона. В двух последних примерах вам придется включить в программу директиву use locale и использовать метасимвол \w в текущем локальном контексте.

> Смотри также -------------------------------
Описание функции split в perlfunc(1) рецепты 6.3; 6.23.

8.4. Чтение файла по строкам или абзацам в обратном направлении

Проблема

Требуется обработать каждую строку или абзац файла в обратном направлении.

Решение

Прочитайте все строки в массив и организуйте обработку элементов массива от конца к началу:
@lines = ;
while ($line = pop @lines) { # Сдел чтo итп-то с $line
}
Или занесите строки в массив в обратном порядке:
@lines = reverse ;
foreach $line (@lines) { # Сделать что-то с $line
}

Комментарий

Ограничения, связанные с доступом к файлам (см. введение), не позволяют последовательно читать строки с конца файла. Приходится читать строки в память и обрабатывать их в обратном порядке. Конечно, расходы памяти при этом будут по крайней мере не меньше размера файла. В первом варианте массив строк перебирается в обратном порядке. Такая обработка является деструктивной, поскольку при каждой итерации из массива выталкивается последний элемент. Впрочем, то же самое можно сделать и недеструктивно:
for ($i = $slines; $i != -1; $i--) {
$line = $lines[$i];
}

Во втором варианте генерируется массив строк, изначально расположенных в обратном порядке. Его тоже можно обработать недеструктивно. Мы получаем массив с обратным порядком строк, поскольку присваивание @lines обеспечива- ет вызов reverse в списковом контексте, что, в свою очередь, обеспечивает списковый контекст для оператора . В списковом контексте о возвращает список всех строк 4)айла. Показанные решения легко распространяются на чтение абзацев, достаточно изменить значение
$/:
# Внешний блок обеспечивает существование временной локальной копии $/
{
local $/ = '';
@Daraaraphs = reverse ;
}
foreach $paragraph @paragraphs) { # Сделать что-то
}


Смотри также -------------------------------
Описание функции reverse в perlfunc(1); описание специальной переменной $/ в perlvar(1); рецепты 4.10; 1.6.

8.5. Чтение из дополняемого файла

Проблема

Требуется читать данные из непрерывно растущего файла, однако при достижении конца файла (текущего) следующие попытки чтения завершаются неудачей.

Решение

Читайте данные, пока не будет достигнут конец файла. Сделайте паузу, сбросьте флаг EOF и прочитайте новую порцию данных. Повторяйте, пока процесс не прервется. Флаг EOF сбрасывается либо функцией seek:
for (::) {
while () { .... }
sleep $SOMETIME;
seek(FH, 0, 1);
}

либо методом clearer r модуля IO::Handle:
use 10::Seekable:
for (;;) {
while () { .... }
sleep $SOMETIME;
FH->clearerr();
}

Комментарий

При достижении конца файла во время чтения устанавливается внутренний флаг, который препятствует дальнейшему чтению. Для сброса этого флага проще всего воспользоваться методом clearerr, если он поддерживается (присутствует в модулях IO::Handle и FileHandle). Кроме того, можно вызвать метод POSIX: : clearerr:
$naptime = 1;
use 10::Handle;
open (LOGFILE, "/tmp/logfile") or die "can't open /tmp/logfile: $!";
for (;;) {
while () { print } # Или другая операция
sleep $naptime;
LOGFILE->clearerr(); # Сбросить флаг ошибки ввода/вывода
}

Если простейший вариант в вашей системе не работает, воспользуйтесь функцией seek. Приведенный выше фрагмент с seek пытается переместиться на 0 байт от текущей позиции, что почти всегда завершается успехом. Текущая позиция при этом не изменяется, но зато для манипулятора сбрасывается признак конца файла, благодаря чему при следующем вызове будут прочитаны новые данные. Если и этот вариант не работает (например, из-за того, что он полагается ни так называемую "стандартную" реализацию ввода/вывода библиотек С), попробуйте следующий фрагмент - он явно запоминает старую позицию в файле и напрямую возвращается к ней:
for (;;) {
for ($curpos = tell(LOGFILE); ; $curpos = tell(LOGFILE)) {
# Обработать $_
}
sleep $naptime;
seek(LOGFILE, $curpos, 0);
# Вернуться к прежней позиции
}
Некоторые Файловые системы позволяют удалить файл во время чтения из него. Вероятно, в таких случаях нет смысла продолжать работу с файлом. Чтобы программа в подобных ситуациях завершалась, вызовите stat для манипулятора и убедитесь в том, что количество ссылок па него (третье поле возвращаемого списка) не стало равным нулю:
exit if (stat(LOGFILE))[3] == О
Модуль File::stat позволяет записать то же самое в более попятном виде:
use File::stat;
exit if stat(*LOGFILE)->nlink == 0;


> Смотри также --------------------------------
Описание функции seek в perlfunc(1) документация по стандартным модулям POSIX и IO::Seekable; страницы руководства tail(1) и stclio(3).

8.6. Выбор случайной строки из файла

Проблема

Требуется прочитать из файла случайную строку.

Решение

Воспользуйтесь функцией rand и переменной $, (текущим номером строки): srand:
rand($.) < 1 && ($line = $_) while 0;
# $line - случайно выбранная строка

Комментарий

Перед вами - изящный и красивый пример неочевидного решения. Мы читаем все строки файла, но не сохраняем их в памяти. Это особенно важно для больших файлов. Вероятность выбора каждой строки равна 1/N (где N - количество прочитанных строк). Следующий фрагмент заменяет хорошо известную программу fortune:
$/ = "%%\n";
$data = '/usr/share/games/fortunes';
srand;
rand($.) < 1 && ($adage = $_) while о;
print $adage;
Если вам известны смещения строк (например, при наличии индекса) и их о'" щее количество, можно выбрать случайную строку и перейти непосредственно ; ее смещению в файле. Впрочем, индекс доступен далеко не всегда. Приведем более формальное пояснение работы данного алгоритма. Функция rand ($. ) выбирает случайное число от 0 до текущего номера строки. Строка с номером N сохраняется в возвращаемой переменной с вероятностью 1/N. Таким образом, первая строка сохраняется с вероятностью 100 %, вторая - с вероятностью 50 %, третья - 33 % и т. д. Вопрос лишь в том, насколько это честно для любого положительного целого N. Начнем с конкретных примеров, а затем перейдем к абстрактным. Разумеется, для файла из одной строки (N=1) все предельно честно: первая строка сохраняется всегда, поскольку 1/1 = 100 %. Для файла из двух строк N = 2. Первая строка сохраняется всегда; когда вы достигаете второй строки, она с вероятностью 50 % заменяет первую. Следовательно, обе строки выбираются с одинаковой вероятностью, и для N = 2 алгоритм тоже работает корректно. Для фай/п из трех строк N = 3. Третья строка сохраняется с вероятностью 1/3 (33 %). Вероятность выбора одной из двух первых строк равна 2/3 (66 %). Но как показаиг выше, две строки имеют одинаковую вероятность выбора (50 %). Пятьдесят процентов от 2/3 равны 1/3. Таким образом, каждая из трех строк файла выбирается с вероятностью 1/3. В общем случае для файла из N+1 строк последняя строка выбирается с вероятностью 1/(N+1), а одна из предыдущих строк - N/(N+1). Деление N/(N+1) на N дает вероятность 1/(N+1) для каждой из N первых строк и те же 1/(N+1) для строки с номером N+1. Следовательно, алгоритм корректно работает для любого положительного целого N. Нам удалось случайным образом выбрать из файла строку со скоростью, пропорциональной количеству строк в файле. При этом максимальный объем используемой памяти даже в худшем случае равен размеру самой длинной строки.

> Смотри также --------------------------------
Описание специальной переменной $, в perlvar{1); рецепты 2.7-2.8.

8.7. Случайная перестановка строк

Проблема

Требуется скопировать файл и случайным образом переставить строки копии.

Решение

Прочитайте все строки в массив, перетасуйте элементы массива (см. рецепт 4.17) и запишите полученную перестановку:
# Используется функция shuffle из главы 4
while (INPUT) {
push(@lines, $_);
}
@reordered = shuffle(@lines);
foreach (@reordered) {
print OUTPUT $_;
}

Комментарий

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

> Смотри также -------------------------------
Рецепты 2.7-2.8; 4.17.

8.8. Чтение строки с конкретным номером

Проблема

Требуется извлечь из файла строку с известным номером.

Решение

Простейший выход - читать строки до обнаружения нужной:
# Выборка строки с номером
$OESIRED_LINE_NUMBER $. = 0;
do { $LINE = } until $. == $desired_line_number || eof;

Если подобная операция должна выполняться многократно, а файл занимает не слишком много места в памяти, прочитайте его в массив:
@lines = ;
$LINE = $lines[$desired_line_number];

Если вы собираетесь многократно извлекать строки по номеру, а файл не помещается в памяти, постройте индекс смещений для отдельных строк и переходите к началу строки 4iy"KHiien seek:
# Применение : build_index(*МАНИПУЛЯТОР_ДАННЫХ, *МАНИПУЛЯТОР_ИНДЕКСА)
sub build_index {
my $data_file = shift;
my $index_file = shift;
my $offset = 0;
while (<$data_file>) {
print $index_file pack("N", $offset);
$offset = tell($data_file);
}
}
# Применение : line_with_index(*МАНИПУЛЯТОР_ДАННЫХ, *МАНИПУЛЯТОР_ИНДЕКСА,$НОМЕР_СТРОКИ)
# Возвращает строку или undef, если НОМЕР_СТРОКИ выходит за пределы файла sub
line_with_index {
my $data_file = shift
my $index_file = shift
my $line_number = shift
my $size; # Размер элемента индекса
my $i_offset; # Смещение элемента в индексе
my Sentry; # Элемент индекса
my $d_offset; # Смещение в файле данных
$size = length(pack("n", 0));
$i_offset = $size * ($line_number-1);
seek($index_file, $i_offset, 0) or return;
read($index_file, $entry, $size);
$d_offset = unpack("n", sentry);
seek($data_file, $d_offset, 0);
return scalar(<$data_file>);
}
# Применение:
open(FILE, "< $file") or die "Can't open $file for reading: $!\n";
open(INDEX, "+>$file.idx")
or die "Can't open Sfile.idx for read/write: $!\n";
build_index(*FILE, *INDEX);
$line = line_with_index(*file, "index, $seeking);
При наличии модуля DB_ File можно воспользоваться методом DB_RECNO, который связывает массив с файлом (по строке па элемент массива):
use DB_File;
use Fcnti;
$tie = tie($!\n";
# Извлечь строку
$line = $lines[$sought-1];

Комментарий

Каждый вариант имеет свои особенности и может пригодиться в конкретной ситуации. Линейное чтение легко программируется и идеально подходит для коротких файлов. Индексный метод обеспечивает ускоренную выборку, по требует предварительного построения индекса. Он применяется в случаях, когда индексируемый файл редко изменяется по сравнению с количеством просмотров. Механизму DB_File присущи некоторые начальные издержки, зато последующая выборка строк выполняется намного быстрее, чем при линейном чтении. Обычно он применяется для многократных обращений к большим файлам. Необходимо знать, с какого числа начинается нумерация строк - с 0 или 1. Переменной $. присваивается 1 после чтения первой строки, поэтому при линейном чтении нумерацию желательно начинать с 1. В индексном механизме широко применяются смещения, и нумерацию лучше начать с 0. DB_File интерпретирует записи файла как элементы массива, индексируемого с 0, поэтому строки также следует нумеровать с 0. Ниже показаны три реализации одной и той же программы, print_line. Программа получает два аргумента - имя файла и номер извлекаемой строки. Версия print_line из примера 8.1 просто читает строки файла до тех пор, пока не найдет нужную. Пример 8.1. printJine-vl
#!/usr/bin/perl -w
# print_line-v1 - линейное чтение
@ARGV == 2 or die "usage: print_line filename line_number\n";
($filename, $line_number) = @>ARGV;
open(INFILE, "< $filename") or die "Can't open $filename for reading: $!\n";
while () {
$line = $_;
last if $. == $line_number;
}
if ($. != $line_number) {
die "Didn't find line $line_number in $filename\n";
} print;

Версия из примера 8.2 сначала строит индекс. При большом количестве обращений индекс строится один раз, а затем используется во всех последующих чтениях. Пример 8.2. print_line-v2
#!/usr/bin/perl -w
# print_line-v2 - построение индекса
# Функции build_index и line_with_index приведены выше.
@ARGV == 2 or
die "usage: print_line FILENAME LINE_NUMBER";
($filename, $line_number) = @argv;
open(ORIG, "< $filename") vor die "Can't open $filename for reading: $!":
# Открыть индекс и при необходимости построить его
# Если две копии программы замечают, что индекс не существует,
# они могут одновременно попытаться построить его.
# Проблема легко решается с применением блокировки.
$indexname = "$filename.index";
sysopen(IDX, $indexname, 0_CREAT|0_RDWR)
or die "Can't open $indexname for read/write: $!";
build_index(*ORIG, *IDX) if -z $indexname;
$line = line_with_index(*ORIG, *IDX, $line_number);
die "Didn't find line $line_number in $filename" unless defined $line;
print $line;

Версия с модулем DB_File из примера 8.3 похожа на волшебство.
Пример 8.3. print_line-v3
#!/usr/bin/perl -w
# print_line-v3 - решение с применением DB_File use DB_File;
use Fcnti;
@ARGV == 2 or
die "usage: print_line FILENAME LINE_NUMBER\n";
($filename, $line_number) = @ARGV;
$tie = tie(@lines, "DB_File", $filename, O.RDWR, 0666, $DB_RECNO) or die "Cannot open
file $filename: $!\n";
unless ($line_number < $tie->length) {
die "Didn't find line $line_nu(nber in $filename\n"
}
print $lines[$line_number-1]; # Легко, правда?


[> Смотри также -------------------------------
Описание функции tie в perlfunc(1); описание специальной переменной $. в perlvar(1), документация по стандартному модулю DB_File.


copyright 2000 Soft group