Running with Ruby

Tag: facebook

Facebook i problem w JW Player – część II – cachowanie

Wstęp

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Ze względu na dużą popularność wpisu dot. “naprawy” linków z Facebooka (tutaj), postanowiłem dołączyć wam drugą część poradnika. Ta część dotyczyć będzie cachowania wyników – cobyśmy za często nie odpytywali Facebooka o linki. Dlaczego lepiej jest cachować wyniki, niż każdorazowo odpytywać FB? Z dwóch powodów:

  • czas – przede wszystkim – czas potrzebny na odpytanie serwera Facebooka o potrzebne dane, a następnie zwrócenie tego klientowi, zajmuje zbyt wiele czasu. Dużo szybciej jest wyciągnąć i zwrócić pojedynczy rekord z bazy,
  • logi – Facebook na pewno nie będzie zadowolony (i nie przepuści po pewnym czasie) z tego, że codziennie nasze konto odpytuje np. 30 – 40 tys razy o strony z filmami. Uznają to za dziwne zachowanie i po prostu zbanują konto lub co gorsza IP.

Czas “życia” adresu z Facebooka to od 24 do 36 godzin. Aby ciągłość usług była stabilna, załóżmy że cache będzie działał przez 30 minut. Da nam to odpytywanie Facebooka – maksymalnie co 30 minut na filmik (a nie przy każdym żądaniu). Dodatkowo, kiedy nasz cache się przedawni, nie odpytamy od razu o nowy URL – najpierw sprawdzimy czy stary już umarł, wysyłając zapytanie o nagłówek pod URL filmu. Jeśli dostaniemy odpowiedź 200 – tzn, że link wciąż jest aktywny i nie musimy go zmieniać.

Takie podejście zapewni nam uptime na poziomie (w najgorszym przypadku): 100%-(30/1440)*100%= 97.9% czasu. A to i tak zakładając wariant, że url z FB zmieni się dokładnie po naszym odpytaniu, przez co cache będzie serwował przez całe 30 minut zły URL. Myślę, że spokojnie można przyjąć uptime na poziomie 98.5%-99%.

Ustawienia

Aby było nam łatwiej, wszystkie ustawienia będziemy przechowywać w jednym miejscu. Stwórzmy sobie plik lib/settings.rb, w którym w stałej SETTINGS będziemy przechowywać co nam trzeba:

SETTINGS = {
  :db_user    => 'user_bazy',
  :db_adapter  => "mysql",
  :db_host     => "mysql5",
  :db_database => "baza",
  :db_pass     => 'haslo',
  :db_socket   => '/tmp/mysql.sock',

  :fb_login    => 'mail@do.facebooka.pl',
  :fb_pass     => 'haslo-do-fb',
}

Baza danych

Do przechowywania danych wykorzystamy bazę MySQLa. Stworzymy sobie plik db_connector.rb, który tak jak poprzedni (i wszystkie następne oprócz main.rb) będzie przechowywany w katalogu lib/. W pliku tym będziemy trzymać kod odpowiedzialny za łączenie się z bazą danych oraz naszą “pseudo” migrację – generującą  tabelę z odpowiednimi kolumnami. Warto zwrócić uwagę na to, że mimo wykorzystania ActiveRecord – ORMa z Railsów – nie wykorzystujemy całego frameworka. Sam ORM nam w zupełności wystarczy.

ActiveRecord::Base.establish_connection(
  :adapter  => SETTINGS[:db_adapter],
  :host     => SETTINGS[:db_host],
  :database => SETTINGS[:db_database],
  :username => SETTINGS[:db_user],
  :password => SETTINGS[:db_pass],
  :socket   => SETTINGS[:db_socket]
)

unless Video.table_exists?
  ActiveRecord::Base.connection.create_table(:videos) do |t|
    t.column :video_id, :string
    t.column :name, :string
    t.column :url, :string, :default => nil
    t.column :views, :integer, :default => 1
    t.column :cached_at, :datetime
  end
end

Warto zwrócić uwagę na ten warunek:

unless Video.table_exists?

dzięki któremu migracja zostanie wykonana tylko jeśli tabela z danymi nie istnieje.

Model filmu wideo

Mamy już gdzie trzymać nasze dane. Pora stworzyć model pojedyńczego filmiku z FB. Model nasz zawierać będzie cztery metody:

  1. url=(new_url) – przypisanie nowego adresu filmiku z Facebooka,
  2. working? – metoda zwracająca true/false zależnie od tego czy filmik działa czy też nie,
  3. url_working? – metoda sprawdzająca nagłówek odpowiedzi z żądania pod adresem URL filmu,
  4. (private) set_cached_at_time – ustawiająca podczas tworzenia instancji czas cachowania na teraz.

Zacznijmy od szkieletu modelu:

require 'net/http'
require 'uri'

class Video < ActiveRecord::Base
  # 30 minut
  CACHE_TIME = 60*30

  before_create :set_cached_at_time
  # Gwarancja, ze jeden filmik bedzie mial jeden wpis w bazie
  validates_uniqueness_of :video_id
  validate :working?
end

Metody

1. url=

def url=(new_url)
  self.errors.clear
  self.cached_at = Time.now
  super new_url
end

Część z was może zapytać, dlaczego czyszczę błędy (self.errors.clear)? Czyszczę je, ponieważ przypisanie nowego URLa następuje wtedy i tylko wtedy, gdy stary był niepoprawny i nie przechodził walidacji. Nowy z założenia przechodzi, tak więc błąd starego URLa musi zostać “zapomniany”.

2. working?

def working?
  v = false
  if self.cached_at >= Time.now - CACHE_TIME && self.url.length > 10
    v = true
  else
    v = url_working?
    self.cached_at = Time.now
  end
  self.errors.add(:cached_at, 'Cache ulegl przedawnieniu') unless v
  v
end

W tej metodzie sprawdzamy czy URL jest poprawny. Jeśli jest “młody” (czyli nowszy niż 30 minut) i jego długość jest większa niż 10 (to tak dla pewności ;) ), tzn. że jest prawdziwy (w 99% procentach przypadków – patrz wstęp). W przeciwnym razie – sprawdzamy nagłówek odpowiedzi z żądania wysłanego pod link, i jeśli jest poprawny – to ok, jeśli nie – to wrzucamy błąd do tablicy błędów naszej instancji modelu. Uważniejsze osoby mogą zapytać, dlaczego ustawiamy cache na nowy – mimo, że URL się nie zmienił. Ustawiamy tak, ponieważ zmiana URLa nastąpi na pewno jeśli ten link był niepoprawny. Nastąpi ona jednak w pliku main.rb.

3. url_working?

def url_working?
  begin
    host = URI.parse(self.url).host
    http = Net::HTTP.new(host)
    headers = http.head(self.url)
  rescue
    return true
  end
  if headers.code == "200"
    true
  else
    false
  end
end

Zasadniczo sposób działania tej metody opisałem już wyżej, ale tak dla pewności:

  1. Wysyłamy żądanie nagłówka pod adres naszego filmiku
  2. Jeśli coś poszło nie tak – zakładamy, że URL nie jest poprawny
  3. Jeśli dostaliśmy nagłówek to go sprawdzamy
  4. Jeśli 200 – to znaczy że URL jest poprawny (true)
  5. Jeśli cokolwiek innego (404, 401, 403, 500, itd) – tzn. że już “umarł” (false)

4. set_cached_at_time

  def set_cached_at_time
    self.cached_at = Time.now
  end

Tutaj nie ma co tłumaczyć.

To by było na tyle jeśli chodzi o nasz model Video. Pozostaje nam jeszcze tylko plik main.rb.

Połączenie wszystkiego w całość

Na początek zaincludujmy pliki z katalogu lib/ oraz inne niezbędne nam biblioteki:

# coding: utf-8

def require_local(file)
  require File.join(File.dirname(__FILE__), "/lib/#{file}")
end

require 'rubygems'
require_local 'settings'
require_local 'facebook_bot'
require 'active_record'
require_local 'video'
require_local 'db_connector'

Zwróćcie uwagę, że dołączamy naszego FacebookBota z pierwszej części tego poradnika. Jednak jest to wersja lekko zmodyfikowana w stos. do wersji z części pierwszej. Różnica jest zasadniczo taka, że ta “nowa” wersja bota, oprócz URLa filmu pobiera także jego nazwę, dlatego radzę pobrać sobie paczkę ze źródełkami lub plik z botem z mojego githuba.

Dalej pobieramy z parametrów ID filmu oraz opcję “force” której użycie wymusi regenerację linku wprost z Facebooka, niezależnie od wartości cache’a:

video_id = ARGV[0]
force = false || ARGV[1] == 'true'

Dalej cała główna logika tej aplikacji:

video = Video.find_by_video_id(video_id)

# Jesli taki plik istnieje i jest poprawny i żądanie nie jest wymuszone
# to zwróć link
if video && video.valid? && !force
  puts 'Cache'
  puts video.url
else
  # Jesli link był niepoprawny lub jest to nowy filmik to zainicjuj FBbota
  # i pobierz z FB URL oraz nazwę filmiku
  fb = FacebookBot.new(SETTINGS[:fb_login], SETTINGS[:fb_pass])
  url = fb.video_url(video_id)
  name = fb.video_name(video_id)
  # Jesli video istniało tylko URL wygasł to przypisz nowy URL i nazwę
  # (nazwe na wypadek gdyby ulegla zmianie)
  if video
    video.url = url
    video.name = name
  else
    # Jesli filmiku nie bylo to go utworz
    video = Video.new(
      :video_id => video_id,
      :url => url,
      :name => name
    )
  end
  puts 'Nowe wywolanie'
  puts video.url
end
# Podbij ilosc wyswietlen
video.views+=1
# I Zapisz zmiany w modelu
video.save

Tyle. Od teraz mamy w pełni działający program umożliwiający “regenerację” linków do plików video z Facebooka. Dzięki cachowaniu w bazie danych ilość odpytań dla popularnych filmików znacznie spada.

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Poprawne linki z Facebooka dla JWPlayera – wygasające linki – część I

Wstęp

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Część z Was na pewno osadza swoje filmiki z konta Facebook także na swoich stronach www. Niektórzy robią to z pomocą odtwarzacza dostarczonego przez Facebooka, inni korzystają np. z JWPlayera. Niestety ostatnimi czasy, Facebook zauważył, że staje się bardzo fajną platformą hostingową dla filmów wszelakich (i to w jakości HD!). Zjadało im to (i zjada ;) ) gigantyczne ilości zasobów – przede wszystkim łącza. Jeśli osadzasz pliki wideo przez ich odtwarzacz – to nie ma problemu – FB ma z tego korzyści (mają swoje logo na filmiku, itd). Korzystając jednak z JWPlayera – nie uda ci się ta sztuczka. Linki bezpośrednie do plików MP4 są zmieniane co 24-36 godzin, w skutek czego umieszczanie ich “w” odtwarzaczu nie ma sensu. Na szczęście da się to bardzo łatwo rozwiązać, jak zawsze w… Rubym :)

Mechanize

Napiszemy prostego bota – który wchodzi na Facebooka, loguje się i sprawdza URL filmu. Warto dodać cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo. Rozbudowę tego narzędzia pozostawię jednak Wam. Wracając do sedna sprawy. Aby napisać tego bota, skorzystamy z Mechanize. Mechanize jest biblioteką stworzoną do łatwej integracji ze stronami wszelakimi. Umożliwia przesyłanie formularzy, odwiedzanie stron, zapewnia obsługę cookies, ssl-a, itp, itd. To głównie dzięki niemu, będziemy mogli odświeżać linki do plików MP4 z FB.

Zanim jednak to zrobimy, musimy dodać do Mechanize pewną małą poprawkę. Domyślnie nie pozwala wyszukiwać formularzy po ID a nam taka metoda się przyda. Tak więc:

class Mechanize::Page
  def form_id(formId)
    formContents = (self/:form).find { |elem| elem['id'] == formId }
    if formContents then return Mechanize::Form.new(formContents) end
  end
end

Tyle :) A teraz pora na FacebookBota.

FacebookBot

Nasz bot będzie miał łącznie 4 metody:

  1. Initialize – inicjalizacja bota
  2. Login – logowanie do Facebooka
  3. Video_Url – pobranie URLa pliku wideo
  4. (priv) get_url – wyodrębnienie samego urla pliku ze strony z linkiem

Szkielet klasy będzie więc wyglądał tak:

require 'rubygems'
require 'mechanize'
require 'uri'
require 'cgi'
require 'time'

class FacebookBot
  # Strona główna Facebooka
  FB_URL = "http://www.facebook.com/"
  # Nasza przeglądarka i system :)
  USER_AGENT = 'Linux Firefox'

  def initialize(email, pass)
  end

  def login
  end

  def video_url(video_id)
  end

  private

  def get_url(url)
  end
end

Inicjalizacja naszego bota składa się z zapamiętania e-maila i hasła, utworzenia obiektu Mechanize do eksploracji FB oraz próby zalogowania na nasze konto:

  def initialize(email, pass)
    @email, @pass = email, pass

    @agent = Mechanize.new
    @agent.user_agent_alias = USER_AGENT

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

    self.login
  end

Wartym omówienia jest ten fragment:

    @cookies = File.dirname(__FILE__) + "/../cookies-" + @email + ".yml"
    if (File.file?(@cookies))
      @agent.cookie_jar.load(@cookies)
    end

Zmienna @cookies trzyma ciastka sesji od Facebooka na dysku. Dlaczego jest o katalog wyżej (../) niż sam plik? Ponieważ plik z botem wrzucimy do ‘lib/’ a plik z ciastkiem będziemy chcieli mieć w katalogu głównym naszej małej aplikacji. Tak więc, ustalamy sobie ścieżkę, sprawdzamy czy plik już istnieje i jeśli tak jest, to ładujemy jego zawartość do kontenera na ciacho – tak żeby można się było przedstawiać nim w Facebooku. Następnie podejmujemy próbę logowania.

Pora na logowanie:

  def login
    page = @agent.get(FB_URL)

    if (loginf = page.form_id("login_form"))
      loginf.set_fields(:email => @email, :pass => @pass)
      page = @agent.submit(loginf, loginf.buttons.first)
    end

    @agent.cookie_jar.save_as(@cookies)

    body = page.root.to_html
    @uid = %r{\\"user\\":(\d+),\\"hide\\"}.match(body)[1]
    @post_form_id = %r{<input type="hidden" id="post_form_id" name="post_form_id" value="([^"]+)}.match(body)[1]
  end

Logowanie przebiega w następujący sposób:

  1. Próbujemy przejść na stronę główną FB.
  2. Jeśli się to udało – tzn że nasze ciastka były i były prawidłowe
  3. Jeśli nie, to wypełniamy formularz logowania i klikamy na “Zaloguj”
  4. Zapamiętujemy uaktualnione ciastka
  5. Przechodzimy na stronę główną i zapamiętujemy id usera (UID)
  6. Zapamiętujemy klucz do przesyłania żądań typu POST

Prawda że proste? :)

Video_url:

def video_url(id)
    begin
      pa = @agent.get("#{FB_URL}video/video.php?v=#{id}")
      pr = pa.body
      get_url(pr)
    rescue
      "Niepoprawne ID pliku wideo lub wideo nie jest publiczne."
    end
end

Pobieramy stronę pliku wideo, wyciągamy link do MP4 i zwracamy. Jeśli coś pójdzie nie tak, to zwracamy info że wideo jest nieprawidłowe lub nie jest publiczne.

Samo parsowanie przebiega w sposób trochę “brutalny” – nie chciało mi się bawić i myśleć to wyciągnąłem tak niezbyt elegancko:

private

def get_url(url)
	url = url.scan(/addVariable\(\"highqual_src\",\s\"http.+\"\)/ix).first
	url = url.split(')').first.gsub('\u00253A', ':')
	url = url.gsub('\u00252F', '/')
	url = url.gsub('\u00253F', '?')
	url = url.gsub('\u00253D', '=')
	url = url.gsub('\u002526', '&')
	url = "http://#{url.split('http://')[1]}".split('"').first
	CGI.unescapeHTML(url)
end

Grunt że działa.
Oto jak działa całość:

  fb = FacebookBot.new('moj@mail.pl', 'moje_tajne_haslo')
  p  fb.video_url(ID_PLIKU_VIDEO)

Warto dodać do tego system cacheowanie, tak aby nie odpytywać FB za każdym razem o to samo!

Źródełko :)

W związku z szerokim zainteresowaniem, udostępniam prosty interfejs: www.fbc.mensfeld.pl. Linki do odcinków buduje się tak:

www.fbc.mensfeld.pl/ID-ODCINKA.mp4

Tutorial składa się z dwóch cześci:

  1. FacebookBot z wykorzystaniem Mechanize
  2. Cachowanie odpytań

Kod (nowszy i lepszy ;) ) dostępny na githubie.

Copyright © 2018 Running with Ruby

Theme by Anders NorenUp ↑