Выпуск 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. →