LE Blog

Инженер от души

14.04.2009 firtree_right Фильтрация rss-потоков с помощью Sinatra и HTTParty

Задача

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

Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.

Для удобства предположим, что потоки имеют одинаковый формат — atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.

Ресурсы

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

А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.

sudo gem i sinatra
sudo gem i httparty

Сбор и фильтрация

Создадим библиотечный файл feed_fetcher.rb:

require 'rubygems'
require 'httparty'

class FeedFetcher
  include HTTParty
  format :xml # позволяет получть результат сразу расфасованный
              # в Hash

  def self.get_items
    urls = nil # будет массив адресов
    titles = nil # будет массив нужных частей заголовков
    items = [] # будет массив записей
    File.open("path_to_feed_urls_file") do |f|
      urls = f.readlines.each(&:strip!)
    end
    File.open("path_to_titles_file") do |f|
      titles = f.readlines.each(&:strip!)
    end

    # составим единое регулярное выражение для фильтрации
    retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })

    # соберём записи со всех адресов в единый масив
    urls.each do |u|
      items += get(u)["rss"]["channel"]["item"] unless u.empty?
    end

    # отфильтруем по регулярному выражению и упорядочим по дате
    items.select { |i| i["title"] =~ retitles }.sort do |x, y|
      DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
    end
  end
end

Выдача результата

Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.

Файл feed_filter.rb:

require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'

get '/' do
  content_type 'application/xml', :charset => 'utf-8'
  @items = FeedFetcher.get_items
  builder :index
end

По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:

xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
  xml.channel do
    xml.title "My Filtered Feed"
    xml.link "http://lonelyelk.com"
    xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
    xml.description "Some description"
    @items.each do |item|
      xml.item do
        item.each_pair do |key, value|
          xml.tag!(key, value)
        end
      end
    end
  end
end

Запуск приложения с помощью passenger

Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.

В установках виртуального сервера для apache:

<VirtualHost *:80>
  ServerAdmin webmaster@mydomain.ru
  ServerName feedfilter.mydomain.ru
  DocumentRoot /path/to/feed_filter/public
  ...
</VirtualHost>

А в папке приложения нужно создать файл config.ru:

require 'rubygems'
require 'sinatra'

Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])

require 'feed_filter'
run Sinatra::Application

Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.

Материалы для изучения

Первое знакомство с фреймворком Sinatra