Web spider (ou aspiration de site) avec Anemone …

J’ai besoin une version de quelques sites réalisés avec Rails en version statique pour une présentation offline.
J’aurai pu utiliser wget, mais franchement j’avais envie et besoin d’un outil plus personnalisable. Ou bien étendre les controller pour récupérer la sortie mais trop de problème et pas assez de souplesse. C’est là qu’intervient Anemone.

La mise en place est simple (via gem bien sûr), et la prise en main l’est tout autant, comme le montre l’exmple de leur site :

require 'anemone'

Anemone.crawl("http://www.example.com/") do |anemone|
  anemone.on_every_page do |page|
      puts page.url
  end
end

C’est assez rudimentaire, mais en couplant avec des outils puissant comme Hpricot, et une surcharge de la classe URI, on en sort un outil vraiment personnalisé et puissant.

J’ai pu réécrire les chemins statiques de mon code en chemin relatif pour une visualisation offline en surchargeant la classe URI


require 'uri'
require 'rubygems'
require 'anemone'
require 'hpricot'

OUTPUT_DIR = File.join(File.dirname(__FILE__), 'export', 'sites')

module UriExt

  def last_item_of_path_is_used_for_filename
    self.query || (File.basename(self.path) =~ /^(.*)\..*$/)
  end

  def to_static_filename
    filename = ''
    if self.query
      if (File.basename(self.path) =~ /^(.*)\..*$/)
        filename = $1
      else
        filename = File.basename(self.path)
      end
      filename += '-' + self.query_to_filename_ext + '.html'
    else
      if (File.basename(self.path) =~ /^(.*)\..*$/)
        filename = File.basename(self.path)
      else
        filename = 'index.html'
      end
    end
    filename
  end

  def query_to_filename_ext
    self.query ? self.query.gsub(/[=;&]/, '-') : nil
  end

  def build_relative_path(with_host=true)
    if self.path !~ /^\/$/
      items = with_host ? [self.host] : []
      original_path = (self.path =~ /^\/(.*)$/ ? $1 : self.path)
      items_original_path =  original_path.split('/')
      items_original_path.shift if items_original_path.first == '/'
      items_original_path.pop if self.last_item_of_path_is_used_for_filename
      items += items_original_path
      File.join(items)
    end
  end

  def complete_path_filename(with_host=true)
    items = []
    relative_path = self.build_relative_path(with_host)
    items << relative_path if relative_path
    items << self.to_static_filename
    File.join(items)
  end

  def create_dir
    relative_path = self.build_relative_path
    if relative_path
      FileUtils.mkdir_p(File.join(OUTPUT_DIR, relative_path))
    end
  end

  def create_file(content)
    self.create_dir
    File.open(File.join(OUTPUT_DIR, self.complete_path_filename), 'w') do |file|
      file.write(content)
    end
  end

  def build_relative_path_for_html
    if self.path !~ /^\/$/
      (self.path =~ /^\/(.*)$/ ? $1 : self.path)
    else
      'index.html'
    end
  end

  def root_back_relative_path(an_absloute_path)
    if self.path !~ /^\/$/
      return File.join((['..'] * ((self.path.split('/')).length - 1)).join('/'), (an_absloute_path =~ /^\/(.*)$/ ? $1 : an_absloute_path))
    else
      (an_absloute_path =~ /^\/(.*)$/ ? $1 : an_absloute_path)
    end
  end
end

URI::HTTP.send(:include, UriExt)
URI::Generic.send(:include, UriExt)

Anemone.crawl("http://www.example.com") do |anemone|
  # cache pages
  anemone.storage = Anemone::Storage.PStore('arnaudkozlinski.pstore')

  # avoid JPG crawl (just a particular case for my website)
  # it's here only for th example
  anemone.focus_crawl { |page| page.links.select{ |uri| uri.path !~ /\.(jpg|JPG)$/ } }

  anemone.on_every_page do |page|
      puts page.url
      # if craw a html page parse and modify it with Hpricot
      if page.headers['content-type'].join('') =~ /^text\/html/
        doc = Hpricot(page.body)

        # image source rewriting
        doc.search('img').each do |img|
          if img.attributes['src']
            uri_img = URI.parse(img.attributes['src'])
            img['src'] =  page.url.root_back_relative_path(uri_img.path)
          end
        end
        ....

        # stylesheet source rewriting
        doc.search('link').each do |script|
          if script.attributes['href']
            uri_script = URI.parse(script.attributes['href'])
            script['href'] =  page.url.root_back_relative_path(uri_script.path)
          end
        end
        ....

     end
  end

Petit bémol, reste le parsing des feuilles de style CSS pour la réécriture des url() vers les images de style. Mais comme les url des feuilles de style sont déjà en relatif (c’est ma règle en tout cas), je n’ai pas eu besoin de parser, juste copier les images au bon endroit. D’autant que je n’ai pas trouvé le Hpricot du CSS.

Ça peut paraître long à première vue, mais quand on maîtrise Ruby, c’est largement moins prise de tête que d’utiliser un web spider automatique, car il y a toujours des cas particuliers et par expérience aucun ne permet d’obtenir un résultat parfait.

Ce contenu a été publié dans Web Dev. Vous pouvez le mettre en favoris avec ce permalien.