Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper)

Дело было вечером, делать было нечего и решил я написать небольшое приложение на Sinatra с Datamapper’ом. За идеей далеко ходить также не пришлось: решил написать небольшой «фильтратор» интересного для меня контента из новостей news.ycombinator.ru. Не стал изобретать велосипед на этот раз и интересными буду считать новости, названия которых содержат определенные слова. Будем отображать список прочитанных и непрочитанных новостей. Список новостей каждый час будет обновляться по cron’у — вот и вся задача.

Начнем с реализации: для этого нам понадобится:

data_mapper с двумя адаптерами (sqlite3 для локального использование и postgresql для production’а)
sinatra
coffeeScript, хоть можно было и легко обойтись без него
slim в качестве шаблонизатора
Итак, поехали:

Gemfile:
source 'https://rubygems.org'

gem 'sinatra'
gem 'data_mapper'

group :development do
  gem 'dm-sqlite-adapter'
  gem 'capistrano'
end

group :production do
  gem 'dm-postgres-adapter'
end

gem 'slim'
gem 'coffee-script'
gem 'whenever', :require => false

gem 'nokogiri'

gem 'unicorn'

В нем нет ничего необычного, добавляем необходимые гемы для разных сред.

Теперь самое интересное: основной файл приложения, который занимает больше всего места.

./app.rb
require 'sinatra'
require 'slim'

require 'coffee-script'
require 'data_mapper'

DataMapper::Property::String.length(400)

configure :development do
  DataMapper.setup(:default, 'sqlite3:./db/articles.db')
end

configure :production do
  DataMapper.setup(:default, 'postgres://deployer:funnydb@localhost/ycombinator')
end

class Article
  include DataMapper::Resource

  INTERESTING_KEYWORDS = %w(ruby rails coffee js javascript ember angular
    backbone tdd rspec shoulda gem unicorn nginx sinatra vim mac)

  property :id, Serial
  property :url, String, :unique_index => :u, :required => true, :format => :url
  property :title, String, :required => true, :index => true
  property :interesting, Boolean, :default => false
  property :read_at, DateTime
  timestamps :created_at, :updated_on

  def interesting?
    !!(title =~ Regexp.new(INTERESTING_KEYWORDS.join('|'), Regexp::IGNORECASE))
  end

  def self.interesting_to_me
    all(:interesting => true)
  end

  def self.unread
    all(:read_at => nil)
  end

  def self.read
    all(:read_at.not => nil)
  end

  def self.search(term='')
    if DataMapper.repository.adapter.options[:scheme] == 'sqlite3'
      all(:title.like => "%#{term.to_s}%")
    else
      all(:conditions => [ 'title ILIKE ?', "%#{term.to_s}%" ])
    end
  end
end

DataMapper.finalize
#DataMapper.auto_migrate!
DataMapper.auto_upgrade!

helpers do
  def do_process(scope=nil)
    @search_term = params[:term].nil? ? nil : params[:term]
    @articles = case scope
                when :all
                  Article
                when :all_read
                  Article.read
                when :all_unread
                  Article.unread
                else
                  Article.interesting_to_me.unread
                end.search(@search_term)
    slim :index
  end
end

get '/application.js' do
  coffee :application
end

post '/:id/read' do
  @article = Article.get(params[:id])
  @article.read_at = Time.now
  @article.save
end

get '/all' do
  do_process :all
end

get '/all/read' do
  do_process :all_read
end

get '/all/unread' do
  do_process :all_unread
end

get '/*' do
  do_process
end

А теперь немного комментариев:

1-5 строки — подключаем необходимые для работы файлы
7 строка — сообщаем DataMapper’y, что длина строки (String) не 80 символов, а 400, 255 не хватает.
9-15 — конфигурируем два адаптера: один для разработки, другой для продакшна.
17-53
20,21 — объявляем интересные мне ключевые слова
23-28 — описываем все поля, которые будут в нашей модели
30-32 — метод interesting? определяет по заголовку новости интересна она мне или нет
34-40 — несколько используемых в приложении scope’ов
46-52 — метод search (из-за того, что в Postgresql like учитывает регистр букв, пришлось переписать оператор поиска на ilike, который этого не делает)
60-73 — объявляем метод, который является «сердцем» и в зависимости от параметра заполняет коллекцию определенными статьями и рендерит вьюху ./views/index.slim
76-78 — рендерим coffeeScript, которые делает следующее, если мы кликам по новости, то отправляем ajax post запрос и помечаем новость как прочитанную (read_at = Time.now)
80-84 — сам метод, который помечает новость прочитанной при post запросе
86-100 — разные коллекции (все, прочитанные, непрочитанные и т.д.)
теперь Rakefile, который будет парсить news.ycombinator.com каждый час
require './app'

require 'nokogiri'
require 'open-uri'

desc 'Parse all articles'
task :parse do
  doc = Nokogiri::HTML(open('http://news.ycombinator.com/'))
  links = doc.css('td.title a')
  next_page_link = links.pop

  links.each do |link|
    href = link[:href]
    text = link.children.text

    unless Article.first(:url => href)
      Article.create(:url => href, :title => text) 
    end
  end

  puts Time.now.to_s
end

desc 'Update Interesting tasks'
task :update_interesting do
  Article.all.each do |a|
    a.update(:interesting => a.interesting?)
    puts "#{a.id} updated"
  end
end

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

Файл, который отвечает за частоту выполнения определенных тасков ./config/shedule.rb
set :output, '/home/deployer/projects/ycombinator/shared/log/shedule.log'

job_type :rake, "cd :path && RACK_ENV=:environment bundle exec rake :task --silent :output"

every :hour do
  rake 'parse'
end

В первой строчке я указываю путь до файла с логами, чтобы каждый раз при запуске rake task’а в конец добавлялось время последнего обновления. В блоке с every можно очень гибко указать как часто выполняться, смотрите документацию к гему whenever.

Также я добавил несколько строк к файлу, выполняющего деплой из Разворачиваем Rails приложение вместе с Capistrano. ./config/deploy.rb
...
set :application, 'ycombinator'
set :whenever_command, "bundle exec whenever"
require "whenever/capistrano"
...

Теперь мы можем запустить обновления cron’а deployer’а командой
cap whenever:update_crontab

После ее запуска вы можете проверить, что вышло, обновился ли cron, запустив на сервере, список cron задач:
crontab -l

Без комментариев оставлю вьюхи, но текст их приведу.

./views/application.coffee
$ ->
  $(".article").click ->
    $.post "/" + $(this).attr("id") + "/read", ->
    $(this).parent('li').remove()


./views/layout.slim
doctype html
 
head
  title Ycombinator
  script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
  script src="/application.js"
 
body
  a{ href="/all" } All
  br
  a{ href="/all/read" } All read
  br
  a{ href="/all/unread" } All unread
  br
  a{ href="/" } Home
  hr
  == yield

И наконец, ./views/index.slim
h1
  ' Unread articles
  - if @search_term
    = "searched by: '#{ @search_term }'"

p Search form
form{ method="get" action="" }
  input{ type="text" name="term" value="#{ @search_term }" }
  input{ type="submit" value="Find" }

- if @search_term
  a{ href="/" } Home

ul
  - @articles.each do |article|
    li
      a{ href="#{ article.url }" id="#{ article.id }" class="article" target="_blank" }
        = article.title

Получислось такое незамысловатое и некрасивое приложение icon smile Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper) .



Вместо заключения: чтобы мозги не были напичканы только рельсами (читать как одним фреймворком), мне кажется, необходимо покидать зону комфорта и писать небольшые приложения для души на смежных технологиях. Скажу честно, для того, чтобы реализовать это несложное приложение у меня ушло масса времени на чтение мануалов к Sinatra, DataMapper’у, нежели на написание кода. Но мне понравилось, практической ценности, конечно, приложение почти не имеет, но мозги размялись однозначно. Разминай мозги, коллега icon smile Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper)
Теги:
Парсинг, Sinatra, DataMapper
Добавлено: 21 Сентября 2013 09:14:39 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...