UnixMountainSkiFun

Unix Горы Лыжи

04-05-2008 16:04

Глава 2. Наиболее общие приемы обработки данных


В этой главе рассматриваются:

  • Процессы обработки данных
  • Проектирование структур данных
  • Инкапсуляция бизнес-правил
  • Модель UNIX-фильтра
  • Написание контрольных журналов

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

2.1 Раздельные входные, обрабатывающие и выходные процессы

Будучи написаны в псевдокоде, большинство задач обработки данных выглядят весьма схожим образом. На самом верхнем уровне, псевдокод будет выглядеть примерно таким образом:

Прочитать входные данные (Read input data)
Выполнить обработку (Munge data)
Записать выходные данные (Write output data)

Очевидно, что каждую из этих трех подзадач, перед тем как начать писать код, придется разбить на более мелкие детали; тем не менее, даже если посмотреть на проблему с этого высокого уровня, то можно отметить некоторые важные и полезные принципы обработки данных. Предположим, что мы комбинируем данные из нескольких систем в одну базу данных. В этом случае наши различные источники данных запросто могут предоставлять данные в совершенно различных форматах, однако же они все должны быть преобразованы в один и тот же формат, который и будет передан приемнику данных. Жить станет намного легче, если мы сможем написать одну подпрограмму выдачи результата (прим.перев.: output routine), которая будет обрабатывать результат работы подпрограмм ввода данных (прим.перев.: data inputs routine). Чтобы это стало возможным, структуры данных, в которые мы будем хранить данные, перед вызовом объединенных процедур выдачи данных должны быть одного и того же формата. Это означает, что процедуры обработки данных должны формировать данные в одинаковом формате, без разницы от того, с каким из приемников данных мы будем иметь дело. Самым простым способом добиться этого будет использование одинаковых процедур обработки данных для каждого источника данных. Чтобы это стало возможным, структуры данных, которые являются результатом различных процедур ввода данных, должны быть одинакового формата. Может появиться соблазн пойти и дальше, и предпринять попытку комбинирования процедур ввода данных, однако по причине того, что источники данных имеют совершенно различный формат, это вряд ли представится возможным. На рисунках 2.1 и 2.2 показано, что вместо того, чтобы писать три различные подпрограммы для каждого, из источников данных, нам требуется всего лишь написать процедуры ввода данных, с комбинированными процедурами выдачи и обработки данных.

Рисунок 2.1 Раздельные процессы обработки и выдачи результата

Рисунок 2.2 Объединенные процессы обработки и выдачи результата

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

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

2.2 Разрабатывайте структуры данных аккуратно

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

2.2.1 Пример: И снова файл компакт-дисков

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

Решение 1: простой хэш

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

 my %years;
 while (<STDIN>) {
   chomp;
   my $year = (split /\t/)[3];

   $years++;
 }

 foreach (sort keys %years) {
   print "В $_ году было выпущено $years CD.\n";
 }

Этот скрипт предоставляет решение нашей задачи вполне эффективным способом. Структура данных, которую мы построили, весьма проста, она показана на Рисунке 2.3.

Рисунок 2.3 Исходная разработка структуры данных

Решение 2: добавляем гибкость

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

 my %years;
 while (<STDIN>) {
   chomp;
   my ($artist, $title, $label, $year) = split /\t/;

   my $rec = {artist => $artist,
              title  => $title,
              label  => $label};
   push @ , $rec;
 }

 foreach my $year (sort keys %years) {
   my $count = scalar @;
   print "В $year году было выпущено $count CD.\n";
   print "Это были:\n";
   print map { "$_->{title} от $_->{artist}\n" } @;
 }

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

Заметим, что хотя мы в данном скрипте это и не используем, но тем не менее хэш хранит еще и название звукозаписывающей фирмы. Пусть звукозаписывающая фирма и не требуется в нашей текущей версии скрипта, но тем не менее вполне возможно, что она может потребоваться в самом ближайшем будущем. И если такое случится, то нам больше не потребуется делать каких-либо изменений в секции ввода нашего скрипта, так как мы уже имеем в нашем хэше все необходимые данные. И в этом состоит еще один важный принцип обработки данных -- если вы получаете элемент данных, то следовательно Вы можете сохранить его в вашей структуре данных. Это может быть сказано и по-другому, более кратко, -- "Ничего не выбрасывайте". Улучшенная структура данных показана на рисунке 2.4.

Рисунок 2.4 Улучшенная разработка структуры данных

Решение 3: отделение грамматического разбора от обработки данных

Что же теперь случится, если требования изменятся настолько серьезно, что теперь, в другом отчете, нам потребуется отобразить еще и количество исполнителей? Наш нынешний скрипт перестает быть пригодным вовсе. Ни одна из его частей не может быть повторно использована в попытке достичь решения нашей новой задачи. Похоже, что нам придется полностью переосмыслить нашу стратегию буквально с самого начала.

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

Таким образом нам придется еще раз внимательно обдумать изначальный вопрос -- какого же вида структура должна быть, чтобы она лучшим образом представляла наши данные внутри программы? Давайте еще раз рассмотрим наши данные. Все, что мы имеем, это список записей, каждая из которых имеет явно выраженный набор атрибутов. Мы можем использовать либо хэш, либо массив, чтобы моделировать наш список записей, и точно такой же выбор (хэш или массив) для моделирования каждой индивидуальной записи. В данном конкретном случае, чтобы моделировать данные, мы будем использовать массив хэшей (или, выражаясь более строго, массив ссылок на хэши). Кто-то может возразить, что могут быть и альтернативные варианты, скажем другие комбинации массивов и хэшей, однако тот, что выбрали мы, кажется мне более естественным. И тогда наша программа будет выглядеть чем-то вроде этого:

 my @CDs;
 sub input {
   my @attrs = qw(artist title label year);
   while (<STDIN>) {
     chomp;
     my %rec;
     @rec{@attrs} = split /\t/;
     push @CDs, \%rec;
   }
 }

Третья и теперь уже последняя структура данных представлена на рисунке 2.5.

Рисунок 2.5 Итоговая структура данных

Дополнительно: использование нашей гибкой структуры данных

Основываясь на такой структуре данные, теперь мы можем написать некоторое количество процедур обработки данных, которые будут формировать отчеты, определенного вида. Например, чтобы сформировать наш самый первый отчет, по количеству компакт-дисков, выпущенных в определенном году, мы должны написать процедуру типа такой:

 sub count_cds_by_year {
   my %years;

   foreach (@CDs) {
     $years{$_->{year}}++;
   }

   return \%years;
 }

Эта процедура возвращает ссылку на хэш, структура которого идентична структуре нашего исходного хэша, и тем самым может быть отпечатана, с использованием кода, совершенно одинакового секции выдачи результата в нашем исходном скрипте.

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

 sub count_cds_by_artist {
   my %artists;

   foreach (@CDs) {
     $artists{$_->{artist}}++;
   }

   return \%artists;
 }

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

 sub count_cds_by_attr {
   my $attr = shift;

   my %counts;

   foreach (@CDs) {
     $counts{$_->}++;
   }

   return \%counts;
 }

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

 #!/usr/bin/perl -w

 use strict;

 my @CDs;

 sub input {
   my @attrs = qw(artist title label year);
   while (<STDIN>) {
     chomp;
     my %rec;
     @rec{@attrs} = split /\t/;
     push @CDs, \%rec;
   }
 }

 sub count_cds_by_attr {
   my $attr = shift;

   my %counts;

   foreach (@CDs) {
     $counts{$_->}++;
   }

   return \%counts;
 }

 sub output {
   my $counts = shift;
   foreach (sort keys %) {
     print "$_: $counts->\n";
   }
 }

 my $attr = shift;

 input();
 my $counts = count_cds_by_attr($attr);
 output($counts);

Допустим, что данный программный файл называется count_cds.pl, и то, что файл-список компакт-дисков называется cd.txt. Тогда использование данной программы, может быть примерно таким:

 count_cds.pl year < cds.txt > cds_by_year.txt
 count_cds.pl label < cds.txt > cds_by_label.txt
 count_cds.pl artist < cds.txt > cds_by_artist.txt
 count_cds.pl title < cds.txt > cds_by_title.txt

В большинстве случаев вы будете рассуждать сходным образом, когда будете разрабатывать структуры данных. Структура данных, которая разработана исключительно только лишь для одной задачи, в общем случае, будет проще, чем структура, которая разработана, исходя из предположений гибкости. Решать Вам, -- стоит ли тратить дополнительные усилия на более гибкую разработку (подсказка: обычно стОит все-таки напрячься!).

2.3 Инкапсуляция бизнес-правил

Основную, логическую, часть вашей программы обработки данных, будет составлять моделирование того, что описывается термином "бизнес-логика" (прим.перев.: business rules). Эти правила описывают то, что именно означают конкретные элементы данных, какие допустимые (прим.перев.: valid) множества значений они могут принимать, и каким образом они связаны (прим.перев.: relation) с другими значениями в тех же, или других, записях. (Здесь я описываю эти ограничения (прим.перев.: constraints), как "бизнес-логику", так как мне кажется, что это легче запомнить, чем нечто вроде "доменные ограничения" (прим.перев.: domain specific constraints). Конечно же то, что вы далее реализуете в коде не будет иметь ничего общего с бизнесом).

Вот примеры этих трех типов бизнес-логики:

  • Код покупателя -- это уникальный идентификатор покупателя.
  • Код покупателя -- всегда должен следовать формату CUS-XXXXX, где XXXXX -- уникальное целое.
  • Каждая запись о покупателе должна быть связана с существующей записью о продавце.

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

2.3.1 Причины для инкапсуляции бизнес-правил

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

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

2.3.2 Способы инкапсуляции бизнес-правил

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

В Perl, вы создаете модуль, который содержит бизнес-логику для конкретного типа записи (например, для покупателя), и включаете этот модуль в любую другую Perl-программу, нуждающуюся в бизнес-логике, которая будет контролировать состоятельность записей о покупателях. На самом деле Perl предоставляет вам ряд способов реализовать такую функциональность. Если правила более-менее простые, то вы можете написать модуль, который содержит функции, с названиями по типу get_next_custno или save_cust_record, которые могут быть вызваны в соответствующих точках ваших программ. Чтобы получить более надежное решение, возможно потребуется рассмотреть написание Perl-объекта, который бы реализовал вашу запись о покупателе. Рассмотрим примеры реализации обоих из таких подходов.

2.3.3 Простой модуль

Предположим, что мы решили реализовать три правила бизнес-логики, описанные в начале данного раздела. Мы напишем модуль Customer_Rules.pm, который будет содержать две функции, о которых говорилось ранее, get_next_cust_no и save_cust_record. В следующем ниже примере опущены некоторые низкоуровневые функции.

 package Customer_Rules;

 use strict;
 use Carp;
 use vars qw(@EXPORT @ISA);

 @EXPORT = qw(get_next_cust_no save_cust_record);
 @ISA = qw(Exporter);

 require Exporter;

 sub get_next_cust_no {
   my $prev_cust = get_max_cust_no()
   || croak "Невозможно получить код нового покупателя.\n";

   my ($prev_no) = $prev_cust =~ /(\d+)/;
   $prev_no++;

   return "CUS-$prev_no";
 }

 sub save_cust_record {
   my $cust = shift;

   $cust->{cust_no} ||= get_next_cust_no();

   is_valid_sales_ref($cust->{salesperson})
   || croak "Недопустимая ссылка на продавца: $cust->{salesperson}.";

   write_sales_record($cust);
 }

Как работает Customer_Rules.pm?

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

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

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

В функции save_cust_record, мы предполагаем, что запись о покупателе хранится в некоторой внутренней, сложной структуре данных, и что мы передаем ссылку на эту структуру. Первое, что мы делаем, -- убеждаемся что в структуре присутствует код покупателя. Далее проверяем, что значение $cust->{salesperson} представляет собой, допустимого, в нашей системе, продавца. И снова отметим, что, в нашей системе, список допустимых продавцов может храниться различными способами. Вполне возможно, что потребуется больше данных, чтобы проверить допустимость кода продавца. К примеру, может статься так, что некий продавец может осуществлять сделки с покупателями только в определенном городе/районе. В таком случае, город/район, в котором расположен покупатель, также будет передаваться в функцию is_valid_sales_ref.

И наконец, мы получаем значение true или false, из функции is_valid_sales_ref, и можем двигаться соответственно этому коду дальше. Если продавец допустим, мы можем выполнить запись о покупателе, в используемом нами хранилище, в противном случае, мы уведомляем пользователя о ошибке. В настоящих же системах могут потребоваться и другие, похожие на описанные выше, проверки.

Использование Customer_Rules.pm

Реализовав такой модуль мы можем сделать его доступным для всех программистов, которые пишут приложения для нашей системы, поместив его в специальный каталог проекта. Чтобы использовать эти функции, программисту потребуется всего лишь добавить в программу такую строку:

 use Customer_Rules;

Теперь программа получит доступ к функциям get_next_cust_no и save_cust_record. Иначе говоря, теперь мы уверены в том, что каждая программа имеет ту же самую реализацию правил бизнес-логики, и, наверное наиболее важно, что в случае, если правила бизнес-логики изменятся, то нам потребуется изменить только этот модуль, чтобы внести изменения в каждую из программ.

2.3.4 Класс объектов

Хоть модуль из предыдущего раздела и полезен, но все же имеет ряд проблем, некоторые из которых связаны с тем, что структура записи о покупателе определяется где-то в приложении. Если модуль будет повторно использоваться больше чем в одном приложении, то каждое из них будет определять свою собственную структуру записи о покупателе, и вполне возможно, что эти определения станут отличаться одно от другого. Решением этой проблемы будет создание класса объектов.

Объект определяет как структуру записи, так и все методы, которые будут взаимодействовать с такой записью. Это делает код более легким в повторном использовании и поддержке. Полное описание преимуществ объектно-ориентированного подхода (ООП) выходит за рамки данной книги, здесь приведем лишь два полезных способа, которыми можно дополнить недостающую картину -- страницы руководства perltoot и Damian Conway книга Object Oriented Perl издательство Manning.

Рассмотрим урезанный объект покупатель, реализованный в виде модуля Customer.pm

 package Customer;

 use strict;

 sub new {
   my $thing = shift;
   my $self = {};

   bless $self, ref($thing) || $thing;

   $self->init(@_);
   return $self;
 }

 sub init {
   my $self = shift;

   # Извлекаем элементы из @_ и используем их
   # для создания структуры, хранящую данные о покупателе.
 }

 sub validate {
   my $self = shift;
   # Вызываем несколько методов, каждый из которых проверяет
   # один элемент данных в записи о покупателе.

   return $self->is_valid_sales_ref
          && $self->is_valid_other_attr
          && $self->is_valid_another_attr;
 }

 sub save {
   my $self = shift;

   if ($self->validate) {
   $self->{cust_no} ||= $self->get_next_cust_no;
     return $self->write;
   } else {
     return;
   }
 }

 # В данном примере, ниже по тексту пропущены методы,
 # извлекающие информацию для объекта-покупателя из базы данных,
 # записывающие объект-покупатель в базу данных и проч.
 1; # Все модули должны возвращать значение true.

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

Пример: использования Customer.pm

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

 use Customer;

 my $cust = Customer->new;

 print 'Введите имя нового покупателя: ';
 my $name = <STDIN>;
 $cust->name($name);

 print 'Введите адрес покупателя: ';
 my $addr = <STDIN>;
 $cust->address($addr);

 print 'Введите код продавца: ';
 my $sp_code = <STDIN>;
 $cust->salesperson($sp_code);

 # Допишите код, такой же как выше, для ввода
 # оставшиейся необходимой информации

 if ($cust->save) {
   print "Новый покупатель сохранен успешно.\n";
   print "Новый покупатель получил код ", $cust->code, "\n";
 } else {
   print "При сохранении нового покупателя возникла ошибка.\n";
 }

В этом примере мы создаем пустой покупательский объект, вызывая метод Customer->new, не передавая ему никаких параметров. Затем заполняем различные элементы данных в нашем покупательском объекте объекте, данными полученными от пользователя. Заметим, что тут предполагается, что каждый покупательский атрибут имеет метод доступа, с помощью которого присваивается или извлекается значние атрибута.

''Примечание: Это наиболее общая практика. Например, метод name подсчитывает число параметров, которые ему переданы. Если он получает некоторое новое значение, то он присваивает это значение имени покупателя, в противном случает просто возвращает предыдущее значение.''

''Альтернативным вариантом будет заиметь два отдельных метода, например, с названиями get_name и set_name. Какой из подходов выбрать, будет лишь вашим предпочтением. В любом случае, наиболее приемлемым будет использование методов доступа, нежели доступ к атрибутам напрямую.''

Заполнив все поля необходимой информацией, вызываем $cust->save, чтобы сохранить нашу новую запись. Если сохранение прошло успешно, то атрибут "код покупателя" будет заполнен и мы можем отобразить его значение пользователю, используя метод доступа к атрибуту $cust->code.

Если мы желаем получить доступ к существующей записи о покупателе, то нам потребуется передать покупателя методу Customer->new (например, Customer->new(id =>'CUS-00123')),и тогда метод init заполнит наш объект данными покупателя. Затем мы можем использовать эти данные для каких-либо целей, либо, возможно, изменить их и использовать метод save, чтобы сохранить эти изменения в базе данных.

2.4 Использование модели UNIX-овский фильтр

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

2.4.1 Обзор модели "фильтр"

Многие операционные системы, в основном UNIX и его варианты, поддерживают такой функционал, как перенаправление ввода/вывода (прим.перев.: I/O redirection). Такая возможность имеется также и в Microsoft Windows, являясь составной частью, так называемой, "командной строки", но используется не так широко, как в UNIX. Перенаправление ввода/вывода дает пользователю весьма гибкий механизм, который будет работать везде, где программы ожидают данные на стандартном вводе и отправляют данные на стандартный вывод. Такой функционал достигается тем, что весь программный ввод и вывод трактуется как файловые операции. Операционная система открывает два особых файловых хэндла (прим.перев.: handle), с названиями STDIN и STDOUT, которые, по умполчанию, присоеденены к пользовательской клавиатуре и монитору. (В действительности имеется и третий файловый хэндл -- STDERR, особый файл вывода, в который записываются сообщения об ошибках, однако в нашем обсуждении его наличие можно проигнорировать). Это означает, что все, что будет набрано пользователем на клавиатуре, может быть считано программой из STDIN, а все, что программа запишет в STDOUT, появится на пользовательском мониторе.

Например, если пользователь выполнит UNIX-команду

 ls

то список файлов в текущем каталоге будет записан в STDOUT, и отобразится на пользовательском мониторе.

Имеется также некоторое количество особых символьных строк, которые могут использоваться для перенаправления (прим.перев.: redirect) этих особых файлов. Например, если наш пользователь выполнит команду

 ls > files.txt

то все, что должно было бы быть записано в STDOUT, вместо этого, будет записано в файл files.txt. Сходным образом и STDIN может быть перенапавлен, с использованием символа <. Например,

 sort < files.txt

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

Другой, более мощной, концепцией являются каналы ввода/вывода (прим.перев.: I/O pipes). Это когда вывод одного процесса напрямую присоединяется к вводу другого. Такой функционал достигается использованием символа |. Например, если наш пользователь выполнит команду

 ls | sort

то все, что команда ls запишет на @@STDOUT@ (т.е. список файлов в текущем каталоге) будет записано непосредственно в STDIN команды sort. Команда sort обрабатывает данные, которые появляются в его STDIN, сортирует их, и записывает отсортированные данные на свой STDOUT. STDOUT команды sort не был перенаправлен, и поэтому отсортированный список файлов появится на экране пользовательского монитора.

Краткий справочник символьных строк, используемых в простейшем перенаправлении ввода/вывода, приведен в таблице 2.1. В других операционных системах доступны более сложные возможности, однако эти символы, присутствуют во всех версиях UNIX и Windows.

Таблица 2.1 Символы перенаправления ввода/вывода

Строка Использование Описание
> cmd > file Выполняется cmd и вывод записывается в file, перезаписывая все, что было в file.
>> cmd >> file Выполняется cmd и вывод дописывается в конец file.
< cmd < file Выполняется cmd и ввод берется из file.
| cmd1 | cmd2 Выполняется cmd1 и весь его вывод передается в cmd2.

2.4.2 Преимущества модели "фильтр"

Модель "фильтр" это очень полезная концепция и его фундаментальность отражена в том, как работает UNIX (прим.перев.: UNIX-way). Это означает, что UNIX предоставляет большое количество небольших, простых утилит, каждая из которых выполняет одну задачу, но делает это хорошо. Многие сложные задачи могут быть созданы, всего лишь, соединением некоторого количества таких утилит. Например, если мне потребуется отобразить список файлов в каталоге, с названием, содержащим строку "proj01", и захочется отсортировать их в алфавитном порядке, то я буду использовать комбинацию ls, sort и grep (которая получает текстовую строку, в качестве аргумента, и записывает на STDOUT только те вводимые строки, которые содержат запрошенную строку), вроде такой:

 ls -1 | grep proj01 | sort

Большинство UNIX-утилит написано так, чтобы поддерживать использование такого режима. Эти утилиты известны как фильтры, так как они считывают входные данные с STDIN, некоторым образом фильтруют данные, и записывают то, что осталось в STDOUT.

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

Пример: Независимость ввода/вывода

Предположим, к примеру, что мы пишем программу, с названием data_munger, которая преобразовывает данные из одной системы в данные, подходящие для использования в другой. Изначально мы можем считать данные из файла и записать наш результат в другой. Можеть показаться заманчивым написать программу, вызываемую с двумя аргументами, которые являются именами входного и выходного файлов. Такая программа может быть выполнена примерно так:

 data_munger input.dat output.dat

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

 #!/usr/bin/perl -w

 use strict;

 my ($input, $output) = @ARGV;

 open(IN, $input) || die "Не удается открыть $input для чтения: $!";
 open(OUT, ">$output") || die "Не удается открыть $output для чтения: $!";

 while (<IN>) {
   print OUT munge_data($_);
 }
 close(IN) || die "Не удается закрыть $input: $!";
 close(OUT) || die "Не удается закрыть $output: $!";

И это конечно же будет прекрасно работать до тех пор, пока мы получаем наши входные данные в файле и ожидаем, что записываем результирующие данные в другой файл. Предположим, что позже, программисты, которые пишут наш источник данных заявляют, что написал новую программу, допустим с названием data_writer, которую и нужно теперь использовать, чтобы извлекать данные из их системы. Эта программа будет записывать извлекаемые данные на свой STDOUT. В то же самое время программисты, ответственные за написание приемника данных, заявляют, что их новая программа data_reader, которую мы будем использовать для загрузки данных в их систему, и которая считывает данные из STDIN.

Чтобы использовать нашу программу без изменений, нам потребуется написать некоторое количество дополнительного кода в скрипте, который будет обслуживать нашу программу. Наша программа должна будет вызываться примерно таким кодом:

 data_writer > input.dat
 data_munger input.dat output.dat
 data_reader < output.dat

Это выглядит уже неряшливо, но представьте, что мы сделали такие изменения в большом количестве систем. Наверное уже стоит задуматься о том, чтобы переписать исходную программу.

Если мы сделаем предположение, что наша программа будет считывать данные с STDIN и записывать их в STDOUT, то программа в действительности станет проще и значительно гибче. Переписанная программа выглядит примерно так:

 #!/usr/bin/perl -w

 while (<STDIN>) {
   print munge_data($_);
 }

Заметим, что нам больше не требуется явно открывать обрабатываемый и результирующий файлы, так как Perl уже открыл STDIN и STDOUT за нас. Кроме того, print по умолчанию использует файловый хэндл STDOUT; другими словами, нам больше не требуется передавать файловый хэндл функции print. Таким образом, этот скрипт значительно меньше, чем наш исходный. Когда нам потребуется иметь дело с входным и результирующим файлами, наша программа будет вызываться следующим образом:

 data_munger < input.dat > output.dat

а когда другие системы потребуют от нас использовать их программы data_writer и @@data_reader@, то мы будем вызывать нашу программу так:

 data_writer | data_munger | data_reader

и все будет работать одинаковым образом, без каких бы то ни было изменений в нашей программе. И даже лучше, если временно потребуется работать с data_writer, но без data_reader или наоборот, то мы с легкостью можем вызывать нашу программу таким образом:

 data_writer | data_munger > output.dat

или так:

 data_munger < input.dat | data_reader

и все по прежнему будет работать ожидаемым образом.

Вместо использования файлового хэндла STDIN, Perl позволяет вам написать программу еще более гибким образом с меньшими усилиями, считывая входные данные с пустого файлового хэндла, таким образом:

 #!/usr/bin/perl -w
 while (<>) {
   print munged_data($_);
 }

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

 data_munger input.dat > output.dat
 data_munger input.dat | data reader

в дополнение ко всем предыдущим методам.

Пример: Выстраивание цепочек ввода/вывода

Другое преимущество модели "фильтр" состоит в том, что она позволяет с легкостью добавлять новую функциональность в обрабатывающую цепочку, без какого бы то ни было изменения существующего кода. Предположим, что система отправляет вам данные о продукции. Вы загружаете эти данные в базу данных, на которой основывается web-сайт вашей компании. Вы получаете данные в файле, с названием products.dat, и занимаетесь написанием скрипта с названием load_products. Этот скрипт считывает данные с STDIN, выполняет различные процессы обработки, и наконец загружает данные в базу данных. Команда, которую вы вполняете, для загрузки файла, выглядит примерно так:

 load_products < products.dat

Что происходит когда отдел, который формирует products.dat, оповещает вас о том, что в следствии реорганизации их базы данных, они изменили формат вашего входного файла? Например, возможно, они больше не идентифицируют продукцию уникальным целочисленным значением, а алфавитно-цифровым кодом. Первым побуждением будет переписать load_products, чтобы обрабатывать новый формат данных. Однако действительно ли необходимо дестабилизировать скрипт, который устойчиво работает, уже долгое время? Использование модели "UNIX-фильтр", говорит вам о том, что не обязательно. Вы можете написать новый скрипт, с названием translate_products, который считает новый файловый формат, и преобразует новый код продукции в, ожидаемый вами, идентификатор, и выведет записи на STDOUT в прежнем формате. Теперь ваш, существующий, скрипт load_products, сможет считать записи, в приемлимом формате, с STDIN, и обработать их, в точности тем же самым способом, которым он всегда это делал. Теперь, командная строка будет выглядеть примерно так:

 translate_products < products.dat | load_products

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

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

2.5 Написание контрольного журнала

Когда занимаешься преобразованием данных, то зачастую, полезно вести детальный контрольный журнал (прим.перев.: audit trail) всего того, что вы сделали. В частности, это нужно, когда конечный пользователь, ваших преобразованных данных, спрашивает вас о результатах преобразований. Может быть весьма полезным, иметь возможность отследить (в точности восстановить), по контрольному журналу, каким образом, какой-либо элемент данных появился и преобразовался. В общем, проблемы в выходных данных, (прим.перев.: output data) могут быть двух видов, -- ошибки во входных данных, либо ошибки в программер преобразования. Если вы сможете быстро отследить причину возникновения проблемы, то это сделает вашу жизнь намного проще.

2.5.1 Что записывать в контрольный журнал?

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

 my $audit_level = $ENV{AUDIT_LEVEL} || 0;

В этом примере мы устанавливаем значение $audit_level из переменной окружения AUDIT_LEVEL. Если уровень не установлен, то мы устанавливаем его по-умолчанию в 0, т.е. в минимальный уровень. Позже в скрипте мы можем написать код, похожий на этот:

 print LOG 'Начало обработки в ', scalar localtime, "\n"
    if $audit_level > 0;

чтобы напечатать информацию аудита в, ранее открытый, файловый хэндл LOG.

Стандарты контрольного журнала, обычно варьируются от предприятия к предприятию, однако некоторые вы можете пожелать рассмотреть следующие моменты, подлежащие аудиту:

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

2.5.2 Простейший контрольный журнал

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

 Процесс: daily_upd запущен в 00:30:00 25 марта 2000 года
 Источник: /data/input/daily.dat
 Приемник: база данных покупалей на сервере DATA_SERVER (используется идентификатор 'maint')
 Входная запись: D:CUS-00123
 Действие: Удаление
 Преобразование: CUS-00123 = database id 2364
 Запись 2364 успешно удалена
 Входная запись: U:CUS-00124:Jones & Co| [и т.д.]
 Действие: Изменение
 Преобразование: CUS-00124 = database id 2365
 Запись 2365 успешно изменена
 Входная запись: I:CUS-01000:Magnum Solutions Ltd| [и т.д.]
 Действие: Добавление
 Проверка целостности: CUS-01000 не найдена в базе данных
 Запись 3159 успешно добавлена

 [ остальные записи пропущены ]

 Конец файла-источника
 Обработано 1037 записей (60 добавлено, 964 изменено, 13 удалено)
 Процесс: daily_upd завершен в 00:43:14 25 марта 2000 года

2.5.3 Использование системного журналирования UNIX

Иногда вам может потребоваться интегрировать ваш контрольный журнал в системный журнал UNIX. Это централизованный процесс, в котором администратор UNIX-системы может управлять тем, куда будет записана различная аудит-информация от прочих разнообразных процессов. Чтобы обратиться к системному журналу из Perl, воспользуйтесь модулем Sys::Syslog. Этот модуль содержит четыре функции -- openlog, closelog, setlogmask и syslog, которые в точности повторяют функциональность UNIX-процедур с такими же самыми именами. Для более детальной информации, обратитесь к документации на модуль Sys::Syslog и страницам руководства вашего UNIX. Вот пример того, как можно воспользоваться этим модулем:

 use Sys::Syslog;

 openlog('data_munger.pl', 'cons', 'user');

 # чуть ниже в программе
 syslog('info', 'Процесс запущен');

 # и снова чуть позже
 closelog();

Заметим, что системный журнал автоматически делает временные отметки, -- нам не потребуется вычислять и печатать время в нашей записи контрольного журнала.

2.6 Дополнительная информация

Чтобы получить больше информации по использованию объектов в Perl обратитсь к книге Damian Conway "Object Oriented Perl", издательство Manning.

Для дополнительной информции о модели "UNIX-фильтр" и других приемах UNIX-программирования читайте Brian Kernigan, Rob Pike "The UNIX Programming Environment", издательство Prentice Hall, либо Jerry Peek, Tim O'Reilly, Mike Loukides "UNIX Power Tools", издательство O'Reilly.

Чтобы получить наиболее общие руководства по программированию, смотрите Brian Kernigan, Rob Pike "The Practice of Programming", издательство Addison-Wesley, и Jon Bentley "Programming Pearls", издательство Addison-Wesley.

2.7 Резюме

  • Расщепление вашей программы на различные стадии может уменьшить размер вашего кода, сделав его более удобным

в повторном использовании

  • Более тщательная разработка структур данных сделает ваши программы более гибкими
  • Используйте модули или объекты для инкапсуляции бизнес-правил
  • Модель "UNIX-фильтра" поможет сделать ваши программ независимыми, с точки зрения ввода/вывода
  • Всегда реализуйте контрольные журналы

<< Глава 1. Данные, их обработка и Perl | Data Munging With Perl | Глава 3. Полезные Perl-идиомы >>


edit RightSideBar