Выпуск 4. Июнь 2013

Сортировка в Perl | Содержание | Введение в разработку web-приложений на PSGI/Plack. Часть 3. Starman.

Создание RSS из списка файлов

В позапрошлом номере было упомянуто о преобразовании XML в Perl-структуры. В этой статье будет рассказано об обратном пути на примере генерирования RSS из списка файлов в каталоге.

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

Создание RSS можно было бы описать в несколько предложений, но, как мы увидим позже, есть несколько подводных камней.

Реверс-инжиниринг

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

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:blogChannel="http://backend.userland.com/blogChannelModule">
<channel>
    <title>Very Cool Podcast</title>
    <link>http://www.example.com</link>
    <description>Very Cool Podcast Description</description>
    <image>
        <title>Very Cool Podcast</title>
        <url>http://www.example.com/images/logo.png</url>
        <link>http://www.example.com</link>
    </image>
    <item>
        <title>Very Cool Podcast - 2013-02-18T19-00-01.mp3</title>
        <link>http://www.example.com/very_cool_podacst/2013-02-18T19-00-01.mp3</link>
        <description>2013-02-18T19-00-01.mp3</description>
        <enclosure url="http://www.example.com/very_cool_podacst/2013-02-18T19-00-01.mp3" type="audio/mpeg" />
    </item>
</channel>
</rss>

Как мы видим, для минимального RSS нужно довольно много информации. Проще всего получить имя файла и дату его создания/изменения. Остальную информацию, общую для всех файлов, будем хранить в отдельном ini-файле в том же каталоге. Для чтения будем использовать Config::Simple.

Вот как будет выглядеть ini-файл для подкаста:

title="Very Cool Podcast"
link="http://www.example.com"
image="http://www.example.com/images/logo.png"
description="Very Cool Podcast Description"
extensions="ogg", "mp3"
httpbase="http://www.example.com/very_cool_podacst"

Осталось ещё одна мелочь. Так как мы хотим, чтобы подкаст смогли скачивать RSS-агрегаторы и мобильные устройства, нужен тег enclosure с правильным mime-типом. Чтобы определить правильный mime-тип воспользуемся модулем MIME::Types.

Сценарий

Скрипт можно разбить на несколько частей.

  • Разбор командной строки с помощью Getopt::Long.
  • Загрузка ini-файла с помощью Config::Simple.
  • Создание XML::RSS объекта.
  • Проход файлов в каталоге.
  • Добавление элементов в XML::RSS-объект.
  • Запись RSS-файла.

Разбор командной строки с помощью Getopt::Long

Это самая тривиальная часть. Минимальный джентльменский набор:

#!/usr/bin/perl

use strict;
use warnings;

readdir ведёт себя по-другому в 5.11.2:

use 5.012; # so readdir assigns to $_ in a lone while test

Загружаем все нужные модули:

use Getopt::Long;
use Config::Simple;
use XML::RSS;
use MIME::Types;

Декларируем глобальные переменные:

my $projPath;
my $debug = 0;
my $test  = 0;
my $config;
my $sampleConfig = <<EOF;
title="Podcast Title"
link="http://blah.com"
description="Podcast Description"
extensions = "flv", "mp4", "mp3"
httpbase="http://server/test-podcast"
EOF

$projPath будет указывать на каталог с медиафайлами, а $config на объект конфигурации. Также декларируем пример конфигурационного файла для удобства. Кстати, изображение не обязательно. К этому вернёмся ещё раз при создании XML::RSS-объекта.

Начнем с функции вывода помощи. $0 будет знаменен на полный путь и имя вызванного скрипта.

sub usage {
    die <<EOF;
Usage:
    $0 -path /path/to/my/videos_or_audio_podcasts [-verbose] [-debug] [-test]

-debug    enable debugging output

EOF
}

И сам разбор командной строки:

GetOptions(
    "path=s" => \$projPath,
    "debug"  => \$debug,
    "test"   => \$test
);
usage() if !defined $projPath;

Загрузка ini-файла с помощью Config::Simple

Файл конфигурации должен находиться в каталоге проекта.

my $configFile = $projPath . '/config.ini';

Выдаём пример конфигурации, если файл конфигурации не найден, или не читаем.

print "Checking if $configFile is readable.$/" if $debug;
die "Can't read config file $configFile!"
  . "$/ Here is a sample config:$/$sampleConfig"
  if !-r $configFile;

Загружаем файл конфигурации и проверяем, все ли переменные декларированы. Если нет, выдаём также пример конфигурации. В конце сохраняем ссылку на хеш объекта конфигурации в глобальную переменную для последующего использования.

my %tempConfig;
print "Loading config file $configFile.$/" if $debug;
Config::Simple->import_from($configFile, \%tempConfig);

print "Checking if needed variables exist in the config file.$/" if $debug;
if (   defined $tempConfig{'default.title'}
    && defined $tempConfig{'default.link'}
    && defined $tempConfig{'default.description'}
    && defined $tempConfig{'default.extensions'}
    && defined $tempConfig{'default.httpbase'})
{
    print "All config variables exist.$/" if $debug;
}
else {
    die <<EOF;
Can't find all of the needed variables in the config file.
The config file should look like this:
--cut--
$sampleConfig
--cut--

EOF
}
$config = \%tempConfig;

На этом этапе файл конфигурации загружен, и ссылка на его объект находится в глобальной переменной $config.

Создание XML::RSS-объекта

Предупреждаем, если скрипт работает в тестовом режиме.

print "Running in test mode, no xml file will be written.$/"
  if $debug && $test;

Генерируем XML::RSS-объект. Здесь важны версия 2.0 и ключ encode_output => 0. Версия нужна для тега enclosure, а ключ encode_output важен для русского. Без него все русские буквы (и не только) будут кодироваться в HTML-коды.

print "Generating RSS.$/" if $debug;
my $rss = XML::RSS->new(version => '2.0', encode_output => 0);

Заполняем поля канала из объекта конфигурации.

$rss->channel(
    title       => $config->{'default.title'},
    link        => $config->{'default.link'},
    description => $config->{'default.description'},
);

Если найдено изображение, то добавляем и его.

if (defined $config->{'default.image'}) {
    print "Found image variable in the config files, "
      . "adding image tag to rss.$/"
      if $debug;
    $rss->image(
        title => $config->{'default.title'},
        url   => $config->{'default.image'},
        link  => $config->{'default.link'},
    );
}

На этом этапе мы создали базовый XML::RSS-объект.

Проход файлов в каталоге

Необходимо получить список всех файлов в каталоге проекта $projPath и отсортировать их по дате создания. Для получения списка файлов используем readdir из стандартного комплекта Perl. Но перед этим стоит узнать, какие типы файлов нас интересуют. Для этого и был объявлен массив extensions в файле конфигурации.

Сообщаем, что будем искать.

print "Searching for "
  . join(', ', @{$config->{'default.extensions'}})
  . " in $projPath.$/"
  if $debug;

Генерируем регулярное выражение для скорости. Регулярное выражение может выглядеть, например, так: /(ogg|mp3)$/i

my $extensionsMatch = join('|', @{$config->{'default.extensions'}});
$extensionsMatch = qr/($extensionsMatch)$/i;

Теперь приступим к самим файлам.

Создаём хеш для последующего добавления файлов и сортировки по дате.

my %items;

opendir(my $dh, $projPath) || die;
while (readdir $dh) {
    my $fullPath = $projPath . '/' . $_;

Игнорируем каталоги и пустые файлы.

    next if -d $fullPath;
    next if -z $fullPath;

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

    if (/$extensionsMatch/i) {
        my $timeStamp = (stat($fullPath))[9];
        my $inode     = (stat($fullPath))[1];
        $items{$timeStamp . $inode} = $_;
    }
}
closedir $dh;

Добавление элементов в XML::RSS-объект

На этом этапе есть хеш с ключами, которые нужно отсортировать по убыванию. Потом добавить элементы хеша (имена файлов) в XML::RSS-объект предварительно определив mime-тип.

Создаём объект для определения mime-типа.

my $mimetypes = MIME::Types->new;

Проходим по всем ключам, отсортированным по убыванию.

foreach my $key ( sort {$b <=> $a} keys(%items) ){
    my $filename = $items{$key};

Берём только расширение файла и по нему определяем mime-тип.

    my ($fileExtension) = ($filename =~ /\.([\w\d]+)$/);
    my $fileMimeType = $mimetypes->mimeTypeOf($filename)->type();

Добавляем файл в XML::RSS-объект как элемент.

    print "Adding $filename.$/" if $debug;
    $rss->add_item(
        title     => $config->{'default.title'} . ' - ' . $filename,
        link      => $config->{'default.httpbase'} . '/' . $filename,
        enclosure => {
            url  => $config->{'default.httpbase'} . '/' . $filename,
            type => $fileMimeType
        },
        description => $filename,
    );
}

Запись RSS-файла

Осталось теперь только записать RSS-файл.

Если указан флаг -test, то только выдаём результат на STDOUT и завершаем выполнение.

if ($test) {
    print $rss->as_string;
    exit 0;
}

Если работаем не в тестовом режиме, то записываем файл в каталог проекта $projPath стандартными средствами Perl. Мы не используем функцию XML::RSS save ($rss->save('filename.xml')), потому что она записывает результат не в UTF-8, что делает его нечитаемым.

my $outFile = $projPath . '/rss.xml';
print "Saving xml to $outFile$/" if $debug;
open(my $fh, ">", $outFile);
print $fh $rss->as_string;
close($fh);

Заключение

Красота Perl — в простоте использования различных модулей: соединяя их своим кодом, получаем комплексное решение. Надеюсь, мне удалось это продемонстрировать в этой короткой статье. Полный текст программы доступен в репозитории.

Олег Фиксель


Сортировка в Perl | Содержание | Введение в разработку web-приложений на PSGI/Plack. Часть 3. Starman.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье