Running with Ruby

Tag: links

Adding rel=”nofollow” to all the links in your comments, news, posts, messages in Ruby on Rails

SEO, SEO, SEO

After migrating somewhere like 600 news messages from my old PHP CMS to a Susanoo (at Naruto Shippuuden Senpuu website), I’ve realized that a lot of them have an external links to other websites. Most of them should have rel=”nofollow”,but hey, who cared about SEO 8 years ago :)

Fortunately there is a quite simple and convenient way to fix this. Also after implementing this, we will be able to prevent such things in future.

Nokogiri, HTML and CSS selectors

What we need to do? Well, we must:

  • parse our news content (html part generated by CKEditor),
  • fetch all the link tags,
  • add to all the external links a rel=”nofollow” attribute.

To start doing this, let’s add a gem called Nokogiri to our gemfile:

gem 'nokogiri'

Nokogiri basic usage is quite simple:

noko = Nokogiri::HTML.parse(html_stuff)

After we create an Nokogiri::HTML instance we can use a CSS selector to get all the links:

doc.css('any selector').each do |link|
  # do smthng with those links
end

Each link instance is a Nokogiri::XML::Element so we can easily add an rel attribute:

# Nokogiri::XML::Element link instance
link[:rel] = 'nofollow'

So we could iterate through all the links, add a rel attribute, run to_html method and save the output as our content. Unfortunately we cannot do so because Nokogiri adds some extra stuff, like doctype and header to our html, so when displaying on our website would break the layout.

We could try to gsub links like this:

# Nokogiri::XML::Element link instance
# convert it into a html element
old = link.to_s
# add nofollow and convert to html
link[:rel] = 'nofollow'
new = link.to_s

# try to replace old whole tag with a new one
content.gsub!(old, new)

It might work, but it requires a well formatted and valid xhtml. Won’t work with something like that:

<A href="LINK">msg</A>

Some posts on Naruto Shippuuden Senpuu.net are 8 years old and I had to handle also the tags like the one above. So, what can I do?

There is a third, lil bit less elegant approach (yet it works pretty well). We can replace all the href=”link” parts with href=”link” rel=”nofollow”. This approach seams to work for any valid/invalid type of links.

Before filter to the rescue

We can use a before_filter to handle “nofollowing” all the links in our content. To do so, just place the:

before_save :add_nofollow

in your model declaration file and implement the add_nofollow method.

The add_nofollow method is pretty straightforward. We just cover two existing types of bracket: ‘ and “, then we skip links that are local (within our website) and we are done.

def add_nofollow
  doc = Nokogiri::HTML.parse(self.content)
  links = []
  doc.css(selector).each do |link|
    next unless link['rel'].blank?
    next if (link['href'][0,4] != 'http' && link['href'][0,3] != 'www')
    next if (link['href'].downcase.include?('senpuu.net'))
    links << link
  end

  links.uniq.each do |link|
    link['rel'] = 'nofollow'

    href1 = "href='#{link['href']}'"
    href2 = 'href="'+link['href']+'"'

    self.content = self.content.gsub(href1, href1+' rel="nofollow"')
    self.content = self.content.gsub(href2, href2+' rel="nofollow"')
  end
end

After implementing this in your model logic, you don’t need to worry again about any external links that are inserted into the models.

Problems installing Nokogiri?

If you’ve encountered a problems during the Nokogiri installation (running bundle install):

Building native extensions.  This could take a while…
ERROR:  Error installing nokogiri:
ERROR: Failed to build gem native extension.
/opt/ruby-enterprise-1.8.7-2010.02/bin/ruby extconf.rb
checking for iconv.h… yes
checking for libxml/parser.h… yes
checking for libxslt/xslt.h… no
—–
libxslt is missing.
—–
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers.  Check the mkmf.log file for more
details.  You may need configuration options.
Provided configuration options:
–with-opt-dir
–without-opt-dir
–with-opt-include
–without-opt-include=${opt-dir}/include
–with-opt-lib
–without-opt-lib=${opt-dir}/lib
–with-make-prog
–without-make-prog
–srcdir=.
–curdir
–ruby=/opt/ruby-enterprise-1.8.7-2010.02/bin/ruby
–with-zlib-dir
–without-zlib-dir
–with-zlib-include

Type into your console:

sudo apt-get install libxslt-dev libxml2-dev

and run again:

bundle install

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 ↑