Table of Contents
Wstęp
v5 (a jakże ;) ) wyposażone będzie (a w sumie to już jest) w dość miły Feed Reader. Z tego względu, postanowiłem pokazać Wam jak zrobić podobny (tylko skromniejszy) samemu. Co ma nasz czytnik oferować?
- Możliwość podpinania się pod różne kanały
- Możliwość filtrowania newsów - wg. słów kluczowych w polu tytułu
- Ma weryfikować czy link działa (status 200)
- Możliwość odświeżania wszystkich subskrypcji naraz
W tym mini tutorialu skupię się jedynie na samej idei, tak więc nie będzie ani testów ani niczego "super" zaawansowanego. Będzie to jednak dobra baza do napisania swojego większego. W tutorialu pomijam takie "banałki" jak kod do generowania modeli, itp. Tego jest pełno w innych poradnikach. Zaczynajmy więc :)
Struktura modeli
Najpierw sama struktura modeli. Będziemy mieli subskrypcje (Subscription) oraz same newsy zwane Feedami (Feed). Żeby było fajniej, całość zamkniemy w przestrzeni RSS, tak więc:
- Rss::Subscription - odpowiada subskrypcji pojedynczego kanału
- Rss::Feed - odpowiada pojedynczej wiadomości z kanału
Szkielet subskrypcji wygląda następująco:
require 'rss' require 'date' class Rss::Subscription < ActiveRecord::Base set_table_name "rss_subscriptions" # Reszta kodu którym zajmiemy się za chwilkę :) end
Pola jakie nasza subskrypcja ma mieć (a które sami dopiszecie do pliku migracji ;) ) to:
- source - link do źródła feedów
- refreshed_at - data ostatniej aktualizacji subskrypcji
- name - nazwa kanału
- description - opis kanału
- filter_keywords - opcjonalne słowa kluczowe do filtracji
Pierwszą rzeczą jaką musimy dodać, jest info o tym że nasza subskrypcja może mieć feedy:
has_many :feeds, :class_name => 'Rss::Feed', :dependent => :destroy
Dalej powinny byc walidacje jak np:
validates_uniqueness_of :name validates_uniqueness_of :source
I tego typu walidacje, które są tak proste, że pozostawię je wam :) - trzeba sprawdzić poprawność URLa źródła, długość nazwy i opisu i ew. słowa kluczowe (jeśli są wymagane).
Metody modelu Rss::Subscription
Zanim przejdziemy dalej i rozbudujemy sobie nasz model, musimy zrobić listę metod które ma obsługiwać, a także stworzyć model pojedynczego Feeda. Tak więc metody modelu Rss::Subscription:
- self.refresh_all! - odświeża wszystkie kanały sprawdzając czy są nowe wiadomości
- refresh! - odświeża pojedynczy kanał
- validate_all! - usuwa stare niedziałające feedy (prowadzące do 404 lub 500 - ogólnie takie które nie zwracają 200)
- validate! - jw. ale dla pojedynczego kanału
- private has_keyword?(string) - czy podany string (w naszym wypadku tytuł feeda) zawiera któreś ze słów kluczowych (pod warunkiem, że je sprawdzamy)
Rss::Feed
Przejdźmy do pojedyńczego Rss::Feed'a (szkielecik):
require 'net/http' require 'digest/sha2' # Odpowiednik jednego wpisu z RSSa class Rss::Feed < ActiveRecord::Base set_table_name "rss_feeds" # Reszta kodu do obsługi wpisu end
Poinformujmy Railsy, że ten model do kogoś należy (do Rss::Subscription) i dodajmy scope'y:
default_scope order("published_at DESC") scope :recent, joins(:subscription).limit(8) belongs_to :subscription, :class_name => 'Rss::Subscription'
A oto jakie pola zawiera model Feeda:
- subscription_id - klucz obcy
- hash - hash do sprawdzania unikalności wpisu
- title - tytuł
- description - opis
- address - adres zwrotny z feeda - do otwarcia danej wiadomości na portalu źródła
- published_at - data publikacji na źródle
Metod które ma Feed za dużo nie będzie:
- working? - czy adres zwrotny jest poprawny
- private create_hash - tworzy nowy unikatowy hash - potrzebne aby nasz czytnik nie tworzył wpisów które już mamy w systemie.
Najpierw zajmijmy się metodą working?:
# Czy dany wpis jest aktywny na serwerze (czy status 200) def working? begin case Net::HTTP.get_response(URI.parse(self.address)) when Net::HTTPSuccess then true else false end rescue false end end
Sprawdza ona czy odpowiedź z adresu jest poprawna (Success czyli 200). Jeśli tak, to zwraca true, jeśli nie to false.
Do zrealizowania mamy jeszcze prywatną metodę create_hash która tworzy nam hash weryfikujący unikalność danego feeda - w obrębie danej subskrypcji. Możnaby zapytać, dlaczego nie weryfikujemy tylko daty wpisu - przecież nie zdarza się za często, że dwa wpisy są o tej samej porze. Fakt - nie zdarza się za często w obrębie jednej subskrypcji, ale zdarzyć się może w obrębie wielu.
private # Tworzy hash do sprawdzania unikalnosci danego feeda def create_hash self.hash = Digest::SHA2.hexdigest("#{self.published_at}#{self.subscription_id}") end
Hash składa się z daty publikacji oraz klucza obcego, przez co weryfikacja unikalności odbywa się w obrębie danej subskrypcji. Można "dorzucić" jeszcze tytuł, jednak tytuł może ulec zmianie a wtedy hash ulegnie zmianie, przez co otrzymamy dwa razy ten sam wpis. Można by było weryfikować także adres, jednak w wypadku reorganizacji serwisu - adresy mogłyby się po pewnym czasie "nakładać" a feedy nie byłyby dodawane.
Informujemy Railsy, że przed walidacją warto stworzyć hash oraz że ma być on unikatowy:
before_validation :create_hash validates_uniqueness_of :hash
i to by było na tyle jeśli chodzi o Rss::Feed.
Dokończenie modelu Rss::Subscription
Teraz już będzie z górki. Najpierw metoda klasowa validate_all!:
# Usuwa niedziałające feedy ze wszystkich kanałów def self.validate_all! self.all.each do |el| el.validate! end end
Oraz jej wersja dla danej instacji (co kolejno wywołujemy wyżej):
# Usuwa niedziałające feedy (takie których link nie działa) def validate! self.feeds.each do |feed| feed.destroy unless feed.working? end end
Warto pamiętać, że jeśli nie będzie dostępu do internetu, to linki także zostaną uznane za niedziałające i zostaną usunięte. Warto np. wprowadzić dwustopniowe usuwanie - jeśli link nie działa - to jest on oznaczany jako nieaktywny - i sprawdzany w późniejszym okresie po raz 2. Ale to pozostawiam wam :)
Odświeżenie wszystkich subskrypcji:
# Aktualizacja wszystkich subskrypcji def self.refresh_all! self.all.each do |el| el.refresh! end end
Sprawdzanie słów kluczowych:
private def has_keyword?(string) return true if self.filter_keywords.nil? || self.filter_keywords.length == 0 keywords = self.filter_keywords.downcase.split(',') downcase_title = string.downcase keywords.each do |keyword| return true if downcase_title.include?(keyword.strip) end return false end
I najprzyjemniejsze ^^, kod odświeżający pojedynczą subskrypcję:
# Odswiezamy kanał RSS def refresh! self.refreshed_at = Time.now.to_s(:db) # Jesli odswiezamy instancję - to musi być ona poprawna # W zasadzie korzystałem z tego w czasie testów, ale zostawiłem tak na zapas ;) return unless self.valid? # Próbujemy otworzyć kanał RSS i pobrać feedy begin new_feeds = RSS::Parser.parse(open(self.source).read, false).items rescue new_feeds = [] end # Spróbuj przeparsować kolejne wpisy - jeśli się nam nie udało - to nie ma wpisów # i nic nie zostanie przeparsowane new_feeds.each do |f| begin title = f.title.to_s published_at = DateTime.parse(f.pubDate.to_s).to_s(:db) address = f.link.to_s description = f.description.to_s # Przejdz do nastepnego feeda ten nam nie pasuje do słów kluczowych next unless has_keyword?(title) # Jesli takiego feeda nie było - to go zapisz data = {:title => title, :published_at => published_at, :address => address, :description => description} new_feed_record = self.feeds.new(data) new_feed_record.save! if new_feed_record.valid? && new_feed_record.working? rescue # Jeżeli coś poszło nie tak - przeskocz do następnego feeda next end end end
No i to by było na tyle! Warto przetestować na koniec :) :
sub = Rss::Subscription.new rss.title = "Dev blog" rss.source = 'http://dev.mensfeld.pl/feed/' rss.refresh! rss.recent.each {|feed| p feed.title }
Leave a Reply