Next Previous Contents

Unix Review Column 34

Randal Schwartz

Октябрь 2000

[предполагаемый заголовок: короткий сеанс магии]

Давайте начнем с некоторых действий с текстом. У меня в файле peg_poem есть поэма, написанная моим хорошим другом, Peg Edera, которая выглядит так:


    The Little Acts

    Maybe there is no magic.
    Maybe it is only faith.
    The lonely girl stuffs a letter
    In a bottle, casts it in the sea.
    It floats, it sinks.
    What counts is that it's cast.

    Those little intentions,
    The petals placed on the altar,
    The prayer whispered into air,
    The deep breath.
    The little acts,
    The candles lit,
    The incense burning
    And we remember what counts
    And we know what we need
    In the little acts.

    Then is when to listen.
    What is it that brought you
    To this?
    There's the magic.
    Right in hearing what
    Sends you to 
    Hope.

      Peg Edera
      February 8, 2000

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

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


  open POEM, "peg_poem" or die "Cannot open: $!";
  while (<POEM>) {
    ... do something here with each line
  }

Внутри тела данного цикла while, $_ содержит каждую строку поэмы. Так что на первом проходе мы получим строку The Little Acts и символ новой строки, и так далее.

Если мы просто хотим скопировать данные в STDOUT, это сделает оператор print:


  # open...
  while (<POEM>) {
    print;
  }

По умолчанию print производит вывод в STDOUT, так что мы просто копируем ввод в вывод. А что если мы хотим пронумеровать строки? Переменная $. содержит номер строки последнего файла из которого производилось чтение:


  # open...
  while (<POEM>) {
    print "$.: $_";
  }

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


  # open...
  print "$.: $_" while <POEM>;

Здесь используется форма модификатора while. Каждая строка все равно считывается в $_, и таким образом print получает правильную информацию.

Даже оператор open может быть оптимизован используя великолепный оператор ``diamond (<>)''. Этот оператор просматривает текущее значение массива @ARGV в поисках списка имен файлов, так что давайте зададим их:


  @ARGV = qw(peg_poem);
  while (<>) {
    print;
  }

Заметьте, что нам нет необходимости в явном открытии файла, поскольку это выполняется оператором <>. Конечно, копирование файла лучше выполняется модулем специально созданным для копирования:


  use File::Copy;
  copy "peg_poem", \*STDOUT;

Hо это просто другой способ выполнить работу.

Давайте пойдем в другом направлении: обработке информации до того, как она будет послана в вывод. Как артист, я уверен что Peg ценит возможность включения пустых строк между параграфами поэмы. Hо как мы могли бы удалить пустые строки из вывода? Достаточно просто: используя регулярное выражение:


  while (<>) {
    print if /\S/;
  }

В этом коде регулярное выражение ищет любой одиночный непробельный символ. Если такого нет, то строка выглядит как пустая и не достойна печати.

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


  while (<>) {
    push @data, $_;
  }

Каждая новая строка добавляется в конец массива @data, который в начале является пустым. Теперь мы можем выдать строки в обратном порядке:


  for ($i = $#data; $i >= 0; $i--) {
    print $data[$i];
  }

И поскольку это работает (это нормальный цикл for), то в действительности будет меньше работы для программиста (и немного больше для Perl) если записать это просто как:


  print reverse @data;

что получает массив @data и создает копию массива, переворачивая исходный массив, а затем эта копия передается оператору print.

А что если мы захотим перевернуть каждую строку? Хорошо, в скалярном контексте оператор reverse переворачивает строку. Hо символ новой строки появится не там где нужно. Так что для получения правильного результата нам необходимо выполнить несколько действий:


  foreach $line (@data) {
    chomp($copy = $line);
    print reverse($copy)."\n";
  }

Здесь я беру строку, копирую ее в отдельную переменную, так что chomp не затронет оригинальный элемент массива @data), и затем в скалярном контексте переворачивает ее (поскольку он используется в операторе соединения строк) и результат выдается на вывод.

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


  foreach (@data) {
    print reverse($1)."\n" if /(.*)/;
  }

В этом случае я использую неявную переменную $_ вместе с регулярным выражением, которое находит все символы за исключением символа новой строки (поскольку знак точка . не соответствует символу новой строки) и затем использую результат соответствия в функции reverse. Волшебство!

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


  @reversed = map {
    /(.*)/ && reverse($1)."\n";
  } @data;
  print @reversed;

Операция map берет каждый из элементов @data и временно помещает его в переменную $_. Соответствие регулярного выражения всегда происходит, и после этого переменная $1 содержит строку не включающую символ новой строки, которая затем инвертируется и символ новой строки помещается в конец строки. Конечно, нам не нужна промежуточная переменная:


  print map {
    /(.*)/ && reverse($1)."\n";
  } @data;

Я думаю, что Peg смеялся бы над результатом применения программы к его работе, так что давайте взглянем на другие маленькие волшебные вещи.

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


  while (<>) {
    push @words, /(\S+)/g;
  }

Здесь используется регулярное выражение \S+, соответствующее каждой из протяженной последовательности непробельных символов. Так что после обработки первой строки, мы будем иметь следующий результат:


  @words = ("The", "Little", "Acts");

а вторая строка не вносит в массив ничего, поскольку в ней нет совпадений с регулярным выражением. Мы можем сократить этот код снова используя оператор map:


  @words = map /(\S+)/g, <>;

Это достаточно мощно, так что позвольте мне неспеша пройтись по этому коду. Сначала оператор чтения стоящий справа используется в списочном контексте, означая что все строки всех файлов из массива @ARGV считываются в один прием. Затем оператор map берет из полученного массива по одному элементу (строке) и помещает его в переменную $_. Затем в списочном контексте оценивается регулярное выражение и поскольку совпадение для одной может произойти несколько раз, то каждое совпадение дает нам 0 или более элементов в массиве результатов. Затем полученный массив присваивается массиву @words. Классно, все выполняется одной строкой кода.

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

Так что мы можем немного изменить данный код:


  @words = map /(\w+)/g, <>;

и теперь мы отбираем все смежные буквы, цифры и знаки подчеркивания, которые подпадают под шаблон \w+.

Hо этот шаблон разбивает слово <There's> на две части. Я участвовал во многих обсуждениях разработчиков, на которых я требовал, чтобы в определение \w были включены апострофы и и удалены знаки подчеркивания. Так что, вот более точное и явное регулярное выражение используемое мной:


  @words = map /([a-zA-Z']+)/g, <>;

Так. Это работает с данной поэмой. И пропускает отвратительные номера дней.

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


  @words = map lc, map /([a-zA-Z']+)/g, <>;

Эта строка приводит все слова к нижнему регистру. Hамного лучше! Теперь давайте подсчитаем слова:


  $count{$_}++ for @words;

Hет..... это не может быть так просто? но это так. Каждое из слов оказывается в переменной $_. Мы используем ее как ключ к хешу. В начале значение пары имеет значение undef, и оно увеличивается когда мы находим соответствующее слово.

Теперь давайте взглянем следующий код:


  @by_order = sort { $count{$b} <=> $count{$a} } keys %count;
  for (@by_order) {
    print "$_ => $count{$_}\n";
  }

Этот код выдает слова в отсортированном порядке. Я использовал блок сортировки для контроля порядка ключей, и затем выдаю ключи в этом порядке.

Хорошо, я надеюсь, что вы увидели, что Perl может быть волшебен всегда. До следующей встречи, наслаждайтесь!


Next Previous Contents