Next Previous Contents

Unix Review Column 2 -- Подсчет слов

Randal Schwartz

Май 1995

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

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

Например, давайте реализуем на Perl подсчет числа строк в каждом файле, заданном в командной строке. Я сделаю это с использованием специального оператор чтения ``<>'', который автоматически открывает каждый файл из командной строки.

        #!/usr/bin/perl
        while (<>) {
                $count{$ARGV}++;
        }
        foreach $file (sort keys %count) {
                print "$file has $count{$file} lines\n";
        }

Цикл while проходит по каждой строчке каждого файла, заданного в командной строке или стандартного потока ввода, если не заданы имена файлов. Для каждой строки в ассоциативном массиве %count увеличиваем значение элемента на единицу. Элемент массива выбирается ключом $ARGV, который является именем файла, открытого в ``<>''. Если не задано имен файлов, имя файла равно ``-'', что, следуя принятому в UNIX соглашению, означает стандартный ввод.

После завершения цикла while, все счетчики посчитаны, цикл foreach перебирает все ключи из ассоциативного массива %count и выводит имена файлов, которые я обрабатываю, (они являются ключами) и значения соответствующих счетчиков. Для красоты я отсортировал имена файлов в алфавитном порядке с помощью sort. На каждом шаге цикла foreach имя файла попадает в переменную $file, которая используется для печати счетчика строк в операторе print.

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

        #!/usr/bin/perl
        while (<>) {
                $count{$ARGV}++;
        }
        foreach $file (sort by_count keys %count) {
                print "$file has $count{$file} lines\n";
        }
        sub by_count {
                $count{$b} <=> $count{$a};
        }

Здесь я добавил функцию сравнения элементов, которая задает новое правило сортировки для sort. В моем случае, у меня есть список ключей ассоциативного массива %count и мне хочется сортировать не ключи, а значения из ассоциативного массива. Когда вызывается функция сортировки by_count, она получает два элемента (два ключа из %count) в $a и $b. Задача by_count вернуть значение (-1,0,+1), в зависимости от того, как мы рассматриваем $a, меньшим, равным или большим, чем $b, соответственно. Бинарная операция <=> делает именно то, что нам надо. Она возвращает знак разницы (-1,0,+1) между своими операндами, что и требуется от функции сравнения. Изменив порядок операндов мы получим другой порядок сортировки.

Я поменял $a и $b местами в by_count и, таким образом, получил сортировку по убыванию. Таким образом, длиннейшие файлы будут идти первыми.

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

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

        #!/usr/bin/perl
        while (<>) {
                @words = split(/\W+/);
                $count{$ARGV} += @words;
        }
        foreach $file (sort by_count keys %count) {
                print "$file has $count{$file} words\n";
        }
        sub by_count {
                $count{$b} <=> $count{$a};
        }

Список @words создается для каждой строчки файла и содержит вырезанные split слова, которые разделялись регулярным выражением /\W+/. Это регулярное выражение совпадает с последовательностями, которые не могут быть буквенно-цифровыми. Оператор split ``протягивает'' это регулярное выражение по строке (в этом случае по строке $_, поскольку я не указал ничего другого). Оператор split не заносит в список выражения, подходящие под маску разделителя, все остальное становится элементами списка.

Получив список @words, я могу добавить длину этого списка к счетчику. В скалярном контексте переменная @words равна длине массива @words. В результате, в нашем массиве %counts мы посчитаем не строчки, а слова.

Теперь, посчитав слова, мне может быть даже более интересно посчитать, какие слова встречаются чаще, не только какие файлы имеют больше слов. Давайте перевернем наш счетчик.

       #!/usr/bin/perl
        while (<>) {
                @words = split(/\W+/);
                foreach $word (@words) {
                        $count{$word}++;
                }
        }
        foreach $word (sort by_count keys %count) {
                print "$word occurs $count{$word} times\n";
        }
        sub by_count {
                $count{$b} <=> $count{$a};
        }

Вместо того, чтобы просто считать количество слов на строчку, я прохожу по каждому слову в цикле foreach внутри первого цикла while. Тело цикла foreach выполняется на каждое слово и увеличивает счетчик %counts{$word} в ассоциативном массиве %counts. Ключ $word этого ассоциативного массива теперь не имя файла, как это было в предыдущем случае, а само слово. (Я потерял все ссылки на файл, но они скоро вернутся).

После завершения цикла while, я прохожу по ключам ассоциативного массива %counts, но в этот раз, ключами являются слова, чем и отличается наше сообщение в print. Хотя, функция сравнения элементов осталась та же.

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

Как я уже говорил, я потерял имя файла, где встретилось слово. Предположим, что я хочу учесть соответствие слова и файла, где оно встречается, вместо простого количества встреченных слов. Мне пришлось бы где-то сохранить имя файла. Ладно, это всего лишь несколько нажатий на клавиши и я получу это.

        #!/usr/bin/perl
        while (<>) {
                @words = split(/\W+/);
                foreach $word (@words) {
                        $count{$word}{$ARGV}++;
                }
        }
        foreach $word (sort keys %count) {
                foreach $file (sort keys %{$count{$word}}) {
                        print "$word occurs $count{$word}{$file}",
                                " times in $file\n";
                }
        }

Уф. Да, получилось несколько больше, чем просто несколько нажатий на клавиши. Что же произошло? Теперь я превратил %counts в двумерный ассоциативный массив. Такая конструкция не поддерживалась в Perl до версии 5.0, так что убедитесь, что у вас свежая версия Perl (не волнуйтесь, Perl распространяется бесплатно). Ключи %counts все еще слова, но значения уже анонимные ассоциативные массивы. Ключами у этого массива второго уровня являются имена файлов, в которых встречалось слово. Например, $count{``fred''}{``hello.c''} содержит число, сколько раз слово ``fred'' встретилось в ``hello.c''.

Печатающий цикл тоже немного изменился, ведь мне теперь надо пройтись по всем именам файлов, в которых встретилось слово (теперь в алфавитном порядке), и для каждого слова посмотреть, сколько раз оно встретилось в каждом файле. Этот жуткий синтаксис %{$counts{$word}} необходим, чтобы ссылаться на анонимный ассоциативный массив в $counts{$word}. (Это занимает какое-то время, но может выглядеть вполне естественно, после приобретения некоторого опыта.) Заметьте, что даже внутри двойных кавычек я могу использовать синтаксис вложенного ассоциативного массива для доступа к счетчику.

Хмм. Этот результат работы этой программы выглядит жутковато. Что мне действительно бы хотелось, так чтобы слово было слева, а имена файлов и счетчики справа. Нет проблем --- сейчас подчистим.

        #!/usr/bin/perl
        while (<>) {
                @words = split(/\W+/);
                foreach $word (@words) {
                        $count{$word}{$ARGV}++;
                }
        }
        foreach $word (sort keys %count) {
                print "$word:",
                        join(", ",
                                map "$_: $count{$word}{$_}",
                                sort keys %{$count{$word}}),
                        "\n";
        }

Теперь это выглядит так:

        bedrock: barney.c: 10, betty.c: 5, fred.c: 15
        flintstone: barney.c: 3
        rubble: barney.c: 5, betty.c: 2

Это работа по превращению ключей из внутреннего ассоциативного массива (имена файлов, где встретилось слово) в строчку, содержащую ключевое имя вместе со значением. Такой эффект достигается использованием мощного оператора map, который устанавливает $_ на каждый элемент данного списка, затем возвращает результат из этого в новый список. После этого, оператор join вставляет запятую с пробелом между элементами и склеивает в строчку, которая печатается print.

Ух. Сколько работы в таком маленьком кусочке. Хотя, это все еще не очень опрятно выглядит. Давайте подчистим, используя format.

        #!/usr/bin/perl
        while (<>) {
                @words = split(/\W+/);
                foreach $word (@words) {
                        $count{$word}{$ARGV}++;
                }
        }
        foreach $word (sort keys %count) {
                $left = "$word:";
                $right = join(", ",
                        map "$_: $count{$word}{$_}",
                        sort keys %{$count{$word}});
                write;
        }

        format STDOUT =
        @<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<
        $left,          $right
          ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ~~
          $right
        .

Я поместил печатаемое слово с следующим за ним двоеточием в $left, а список счетчиков для каждого файла в $right. format используется с оператором write, используя формат, определенный в программе позже. Этот формат ставит отмеченное слово в поле с левым выравниванием. Счетчики будут дополнены пробелами справа. Если будет ссылок, чем может поместиться в строку, то оставшиеся ссылки будут разбросаны по последующим строкам (без отступов, что слегка отличается от предыдущего варианта), спасибо встроенному переносчику слов оператора format. (Две тильды в конце строки означают, что формат требует повторения строчки до тех пор, пока строчка не будет напечатана пустой).

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


Next Previous Contents