06-10-2008 18:56
![]() |
В этой главе рассматриваются: |
В данных, которые поступают к нам, для их обработки, наблюдается явная диспропорция, с перевесом в сторону запись-ориентированных данных. В этой главе мы будем рассмаотривать некоторые наиболее общие методы, используемые при обработке такого рода данных.
Мы уже рассматривали примеры простейших запись-ориентированных данных. Файл данных компакт-дисков, который мы рассматривали в предыдущих главах, имел одну строку данных на каждый компакт-диск моей коллекции. Каждая из таких строк данных -- это запись. Как мы увидим позже, записи могут быть больше, или меньше, чем одна строка, однако начнем мы с более детального рассмотрения файлов, в которых одна запись -- это одна строка.
Perl способен весьма легко разобраться с запись-ориентированными данными, а в особенности с простейшими записями, то есть с тем самым типом, который мы собираемся обсудить в данном разделе. Совсем недавно мы рассмотрели идиому построчного чтения из файла, с использованием следующих конструкции
while (<FILE>) {
chomp; # удалим признак новой строки
# каждая строка раз за разом присваивается переменной $_
}
Давайте более детально посмотрим на то, что же собственно выполняется здесь Perl-ом и делает нашу жизнь удобнее.
Наиболее важная часть конструкции состоит в использовании <FILE>, для считывания данных
из файлового хэндла FILE, который, по всей видимости, был назначен файлу, где-то выше в программе,
с помощью вызова функции open. Этот оператор файлового чтения может вернуть два различных результата,
в зависимости от того, используется ли он в скалярном или списочном контексте.
Когда он вызывается в скалярном контексте, то возвращает следующую запись из файлового хэндла. Тут же встает
очевидный вопрос, -- что же собой представляет запись? Ответ состоит в том, что записи разделяются последовательностью
символов, называемых (весьма логично) разделителями записей. Это значение хранится в переменной $/. Ее значение
по-умолчанию -- символ новой строки \n (который, в свою очередь, преобразуется в соответствующие символы, конкретной
операционной системы), однако он всегда может быть изменен, на любую другую строку символов. Такой подход будет весьма
детально рассмотрен позже, а на данный момент вполне достаточным будет значение по-умолчанию.
Будучи вызван в списочном контексте, оператор файлового чтения возвращает список, в котором каждый элемент является записью из входного файла.
То есть, вы можете вызвать оператор файлового чтения одним из следующих способов:
my $next_line = <FILE>; my @whole_file = <FILE>;
В обоих этих примерах важно понимать, что каждая запись, будь то запись хранящаяся в
$next_line, либо записи в @whole_file, помимо собственно данных, в самом конце будут
содержать также и значение переменной $/. (Исключая, может быть, последнюю строку в файле).
Зачастую может потребоваться избавиться от этих значений, и самым простой способ добиться этого,
состоит в использовании функции chomp. Функции chomp, в качестве аргумента, передается
либо скаляр, либо массив, и она удаляет значение $/ из конца скаляра, либо каждого элемента
массива. Если аргумент не был передан, то chomp работает над переменной $_. (В версиях
Perl, предшествовавших 5-ой, функция chomp отсутствовала. Вместо этого мы использовали функцию
chop, которая просто удаляла последний символ из строки, без какой-бы то ни было проверки того,
что в нем находилось. И так как такой подход иногда все же бывает полезным, то chop все еще
сохранилась в Perl, однако в большинстве случаев использование chomp более приемлемо).
Теперь, когда мы уже чуть больше понимаем об операторе файлового ввода и chomp, давайте
посмотрим, сможем ли мы построить нашу стандартную конструкцию обработки данных. Первый вариант,
обрабатывающий каждую строку файла, может выглядеть похожим на следующее:
my $line;
while ($line = <FILE>) {
chomp $line;
...
}
Хорошее начало, однако в него затесалась хитрая и незаметная ошибка. Условное выражение, в
while-цикле, проверяет на истину скалярную переменную $line. Этой переменной присваивается
следующая строка, полученная их FILE. Вообще говоря, это нормально, однако бывают некоторые
случаи, когда вполне нормальная строка в файле, может быть вычислена как false. Из них,
наиболее очивидным являются случай, когда последняя строка в файле содержит значение 0 (ноль),
и не имеет символов окончания строки. (Это должна быть последняя строка, так как все остальные
строки имеют символы окончания строки, что уже само по себе является достаточным условием, чтобы
вычислиться как true). В случае, когда переменная $line будет содержать значение 0,
которое вычислится как false, последняя строка файла не будет обработана.
Хотя эта ошибка и не вполне очевидна, тем не менее все же стоит найти решение, которое не будет
иметь этой проблемы. Это решение достаточно простое, и состоит в проверке того, является ли $line
определенной, вместо того, чтобы оценивать на ее предмет совпадения с true. Содержимое переменной
считается определенным, если оно не является особым Perl-овым значением undef. Любая переменная,
которая содержит значение, которое вычисляется как false, будет все же определенной. Определено
или нет значение переменной, может быть проверено с помощью функции @@defined@. Когда оператор файлового
ввода достигает конца файла, то он возвращает значение undef. Таким образом, мы можем переписать
наш первый вариант в нечто следующее:
my $line;
while (defined($line = <FILE>)) {
chomp $line;
...
}
этот код сделает именно то, что нам нужно. И все же есть возможность сделать пару улучшений, хотя это скорее делание кода более Perl-овым, нежели исправление ошибок.
Первое изменение, состоит в использовании Perl-овой переменной по-умолчанию $_. Большое
количество Perl-кода может быть упрощено с использованием $_. Такой подход не внесет
большого количества изменений в код. Нам больше не нужно будет определять $line, а также
мы можем воспользоваться тем, что chomp использует $_ по-умолчанию. Наш код теперь будет
выглядеть так:
while (defined($_ = <FILE>)) {
chomp;
...
}
Последняя часть оптимизации такова, что порой даже трудно догадаться, каким образом работает
эта синтаксическая изюминка, заложенная авторами Perl, когда они реализовали такой, общий
подход к решению нашей задачи. Так как оператор файлового ввода всего лишь условное выражение
в цикле while, то результат оператора магическим образом присваивается переменной $_,
а результирующее значение проверяется на предмет ее определенности (вместо того, чтобы проверять
не вычисляется ли оно в true). Это означает, что вы можете написать так:
while (<FILE>)) {
chomp;
...
}
здесь мы возвращаемся к нашему исходному коду (однако, к счастью, с более ясным пониманием все сложности, которая кроется в глубине такого просто выглядящего кода). Отметим, что эта, последняя часть оптимизации, зависит от двух вещей:
|
1 |
Оператор файлового ввода должен быть единственным оператором в условном выражении, то есть вы не можете написать чего-либо похожего на это: while (<FILE> and $_ ne 'END') { # ЭТО НЕ СРАБОТАЕТ!
...
}
|
|
2 |
Условное выражение должно быть частью while-цикла, то есть вы не можете написать чего-либо вроде этого: if (<FILE>) { # ЭТО ТОЖЕ НЕ СРАБОТАЕТ!
print;
} else {
print "Нет данных\n";
}
|
Когда вы в цикле перебираете файл, как мы это делали совсем недавно, зачастую полезно знать,
какую строку, в данный момент, вы обрабатываете. Эта полезная информация хранится в переменной
$. (В действительности $. содержит текущий номер строки в файловом хэндле, из которого вы
считывали последний раз. Такая особенность позволяет воспользоваться $. даже если у вас открыто
более одного файла. Это столь же важная переменная, как и переменная определяющая конец строки ($/),
поэтому, чуть позже, мы рассмотрим ее более детально). Значение сбрасывается, когда файловых хэндл
закрывается, это, в частности, означает что следующий код работоспособен:
open FILE, 'input.txt' or die "Не удается открыть файл ввода: $!\n";
while (<FILE>) {
# выполняем некую обработку
}
print "Обработано $. записей.\n";
close FILE;
а такой код с ошибкой, и всегда будет печатать ноль.
# ЭТОТ КОД НЕ РАБОТАЕТ
open FILE, "input.txt" or die "Не удается открыть файл ввода: $!\n";
while (<FILE>) {
# выполняем некую обработку
}
close FILE;
print "Обработано $. записей.\n";
В большинстве этих примеров я отошел от использования STDIN, лишь для того, чтобы показать,
как эти приемы будут работать с произвольным файловым хэндлом. Чтобы окончить этот раздел,
я приведу очень краткий пример использования STDIN, который добавляет номера строк, к любому
файлу, переданному нашей программе.
#!/usr/local/bin/perl -w use strict; print "$.: $_" while <STDIN>;
Итак, теперь, когда мы знаем каким образом получить наши записи из входного потока (либо по одной записи либо целиком за один раз в некий массив), встает вопрос -- что же мы можем сделать с этими данными? Конечно, ответ на такой вопрос большей степени зависит от того, каким собственно должен быть ваш конечный результат, однако даже тут уже есть несколько идей.
Скорее всего внутри вашей записи будут индивидуальные элементы данных (называемые, также, полями), и вам потребуется расщепить запись, чтобы получить доступ к этим полям. Поля в записи, могут быть определены различными способами, однако большинство из них попадает в одну из двух больших групп. В первой группе используется метод, в котором начало и конец конкретного поля обозначается последовательностью символов, которые заведомо не появятся в составе данных полей. Такой метод известен как разграниченные и разделенные данные (прим.перев.: separated and delimited data). (Строго говоря, есть существенная разница в понятиях разделенные и разграниченные данные. Разделенные данные имеют некую символьную последовательность между полями, а разграниченные, имеют символьную последовательность в начале и конце каждого поля. Однако, на практике, методы, которые имеют дело с такими данными, весьма похожи друг на друга, и многие специалисты зачастую используют эти термины взаимозаменяемо.) В другом методе, каждое поле формируется так, чтобы оно всегда занимало некоторое, определенное количество символов, и, если его размер меньше этого размера, то оно дополняется пробелами или нулями. Такой метод известен как данные с фиксированной длиной (прим.перев.: fixed-width data). Более детально, данные с фиксированной длиной, мы будем рассмотривать в следующей главе, а сейчас ограничимся рассмотрением лишь разграниченных и разделенных данных.
Мы уже видели разграниченные данные. Пример с компакт-дисками, который мы рассматривали в предыдщих
главах, является примером файла данных с данными разграниченными символом табуляции. В каждой строке
представлен один компакт-диск, а внутри строки различные поля отделены друг от друга символами табуляции.
Очевидным способом, работающим с такими данными, является тот, который мы использовали ранее, то есть
применение функции split, для расщепления записи на индивидуальные поля, например, так:
my $record = <STDIN>; chomp $record; my @fields = split(/\t/, $record);
После этого поля становятся элементами массива @fields. Зачастую пользуются другим, более
привычным способом, моделирования записи данных, -- хэшем. Например, чтобы построить хэш %cd,
основываясь на данных нашего файла, можно написать что-либо такое:
my $record = <STDIN>;
chomp $record;
my %cd;
($cd{artist}, $cd{title}, $cd{label}, $cd{year}) = split (/\t/, $record);
Теперь мы можем обратиться к индивидуальным полям внутри записи так:
my $label = $cd{label};
my $title = $cd{title};
и так далее.
В главе 3, когда вводили данные из файла с записями о компакт-дисках, этот код был упрощен до такого вида:
my @fields = qw/artist title label year/;
my $record = <FILE>;
chomp $record;
my %cd;
@cd{@fields} = split(/\t/, $record);
В этом примере мы испльзуем срез хэша, чтобы сделать присвоение значениям хэша более удобным. Другим,
побочным явлением, будет то, что поддержка такого кода будет намного проще. Если потребуется добавить
еще одно поле в исходный файл, то единственным изменением, которое придется сделать над процедурой
ввода, будет всего лишь добавление еще одного элемента в массив @fields.
Теперь у нас есть простой и эффективный способ считать простейшие запись-ориентированные данные, а затем расщепить каждую запись данных на, составляющие ее, индивидуальные поля.
Считав данные, затем выполнив над ними необходимую обработку, несомненно, следующей вашей потребностью
будет, отвечающая некоторым правилам, запись данных. Здесь, совершенно очевидным выбором убдет
использование функции print, однако существуют и другие варианты, и даже более того, print
имеет свои тонкости, которые сделают вашу жизнь много проще.
Perl-переменной ($/), содержащей разделитель записей ввода, существует другая, сходная ей, переменная,
($\), которая содержит разделитель записей вывода. Обычно эта переменная устанавливается в undef,
что означает, что вы совершенно вольны, в управлении процессом разделения записей. Если вы установите ее,
в другое, отличное от undef, значение, то такое значение будет добавлено к каждой записи, в вашем
результирующем файле, для каждого вызова оператора print. Если вы знаете, что в вашем результирующем
файле, каждая запись должна быть отделена от другой символом новой строки, то вместо написания такого кода:
foreach (@output_records) {
print "$_\n";
}
можно написать что-либо такое:
{
local $\ = "\n";
foreach (@output_records) {
print;
}
}
(Заметим, что мы локализовали изменение переменной $\, таким образом мы никогда, по
неосмотрительности, не нарушим работу операторов print, в других частях программы.)
Эту переменную употребляют редко, так как ее использование не столь эффективно.
Другими переменными, которые много более полезны, являются разделитель полей вывода ($,) и
разделитель списка вывода ($"). Разделитель полей вывода печатается между элементами
списка, передаваемого оператору print, а разделитель списка вывода печатается между элементами
в списке, который интерполируется в строке, с двойными кавычками. Эти концепции опасным образом
похожи друг на другу, поэтому давайте рассмотрим сможем ли мы прояснить эту ситуацию. В Perl, функция
print работает над списком. Этот список может быть передан в функцию различными способами. Вот
пара примеров:
print 'Этот список имеет один элемент'; print 'Этот', 'список', 'имеет', 'пять', 'элементов';
В первом примере список, передающийся в print, имеет всего лишь один элемент. Во втором примере,
список имеет пять элементов, которые разделены запятой. Разделитель полей вывода ($,), управляет
тем, что печатается между индивидуальными элементами. По-умолчанию, эта переменная установлена в
пустую строку (во втором примере будет напечатано Этотсписокимеетпятьэлементов). Если бы мы, перед
исполнением оператора print, изменили бы значение переменной $, на пробельный символ, то мы
получили бы более читаемый результат. Такой код:
$, = ' '; print 'Этот', 'список', 'имеет', 'пять', 'элементов';
производит такой результат:
Этот список имеет пять элементов
Такой подход может быть полезен, если ваши результирующие данные хранятся в нескольких переменных.
Например, если наши данные о компакт-дисках хранились бы в переменных с названиями $band, $title,
$label и $year, и мы хотели бы создать файл, с разделителями -- символ табуляции, то можно было бы
написать следующее:
$\ = "\n"; $, = "\t"; print $band, $title, $label, $year;
этот код автоматически поместил бы символ табуляции между каждым полем, а также символом новой строки в конце записи.
Другим способом, которым список довольно часто передается в print, является массив. Наверняка вы
встречали код такого вида:
my @list = qw/Это список элементов/; print @list;
в данном случае элементы списка @list печатаются "слипнувшись" (Этосписокэлементов). Наиболее
общим способом избавиться от такого поведения является использование функции join:
my @list = qw/Это список элементов/;
print join(' ', @list);
такой код поместит пробел между каждым печатающимся элементом.
Другим, более элегантным способом разобраться с этой проблемой, будет использование переменой разделитель
списка вывода ($"). Эта переменная управляет тем, что печатается между элементами массива, когда
массив находится в двойных кавычка. Значение по-умолчанию -- пробел. Это означает, что если мы изменим
наш исходный код на такой
my @list = qw/Это список элементов/; print "@list";
то мы добъемся того, что между элементами нашего списка будут напечатаны пробелы. Чтобы печатать данные,
разделенные символом табуляции, необходимо лишь поместить в переменную $" символ табуляции (\t).
В третьей главе, когда мы считывали файл с данными о компакт-дисках, мы сохраняли данные в массив хэшей.
Самым простым способом, отпечатать такие данные, будет использование, например, такого кода:
my @fields = qw/name title label year/;
local $" = "\t";
local $\ = "\n";
foreach (@CDs) {
my $_;
print "@CD{@fields}";
}
Вспомните, -- синтаксис оператора print, один из следующих:
print; print LIST; print FILEHANDLE LIST;
В первом варианте печатается содержимое переменной $_, в файловый хэндл по-умолчанию (обычно
это STDOUT). Во втором варианте, содержимое LIST печатается в файловый хэндл по-умолчанию.
И, наконец, в третьем варианте, содержимое LIST печатается в FILEHANDLE.
Заметим, я сказал, что файловый хэндл по-умолчанию обычно STDOUT. Если вы печатаете различные
части информации, в различные файловые хэндлы, то можно изменять значение по-умолчанию, использованием
функции select. (Или, правильнее говоря, одной из функций select. Perl имеет две функции, с
названием select, и понимает, которую вы имеете в виду, по количеству аргументов, переданному ей.
Другая функция, которую мы не рассматриваем в этой книге, так как она используется в сетевом
программировании, имеет четыре аргумента.) Если вы сделаете вызов select без аргументов, то она
вернет имя выбранного, на данный момент, файлового хэндла вывода, то есть
print select;
обычно превратится в print main::STDOUT. Если вы осуществите вызов select с именем файлового
хэндла, то она заменит текущий файловый хэндл вывода по-умолчанию на новый, переданный ей. В качестве
результата она вернет ранее выбранный файловый хэндл, таким образом вы можете сохранить его и позже
восстановить. Если потребуется записать некоторое количество данных в конкретный файл, то можно
использовать, например, такой код:
open FILE, '>out.txt' or die "Не удается открыть out.txt: $!";
my $old = select FILE;
foreach (@data) {
print;
}
select $old;
Между двумя вызовами select, значение файлового хэндла по-умолчанию изменяется на FILE,
и все операторы print, без конкретного указания файлового хэндла будут печатать в FILE.
Заметим, что когда мы закончили печать, то вернули значение файлового хэндла по-умолчанию, в его
прежнее значение (которое мы сохранили в переменной $file). Перед тем как изменить, вы не
исходить из предположения, что значением файлового хэндла по-умолчанию является STDOUT, так как
остальные части вашей программы могли уже его изменить.
Другой полезной переменной, когда занимаешься записью данных, является $|. Присвоением этой
переменной ненулевого значения, можно добиться того, что буфер вывода будет тут же сброшен после
выполнения оператора print (или write). Этим можно достичь эффекта того, что выходной поток
будет выглядеть так, как если бы он небуферизирован. Эта переменная действует над выбранным файловым
хэндлом по-умолчанию. Если вам потребуется отменить буферизирование любого другого файлового хэндла, то
вам потребуется выбрать его, изменить значение $|, а затем перевыбрать предыдущий файловый хэндл,
используя код, например, такого вида:
my $file = select FILE; $| = 1; select $file;
Этот код работает, однако он не настолько компактен, как может быть, поэтому в большинстве Perl-программ, вместо этого, вы увидите такой код:
select((select(FILE), $| = 1)[0]);
Наверное это один из самых, странно выглядящих участков Perl-кода, с которыми вы столкнулись за последнее время, однако он достаточно прост, если к нему присмотреться пристальнее.
Центральная часть кода, -- это построение списка. Первый элемент списка -- это значение, возвращенное
из select(FILE), которое будет значением ранее выбранного файлового хэндла. В качестве побочного
эффекта, эта часть кода выберет FILE, в качестве нового файлового хэндла по-умолчанию.
Вторым элементом списка будет результат вычисления $| = 1, который всегда 1. В качестве побочного
эффекта, этот код будет отменять буферизацию текущего файлового хэндла по-умолчанию (которым, на данный
момент, является FILE). Далее, этот код получает первый элемент этого списка (который является
ранее выбранным файловым хэндлом) и передает его в select, другими словами говоря, возвращая значение
файлового хэндла по-умолчанию в его предыдущее состояние.
<< Глава 5. Неструктурированные данные | Data Munging With Perl | >>