Newsletter Developpez.com

Inscrivez-vous gratuitement au Club pour recevoir
la newsletter hebdomadaire des développeurs et IT pro

Developpez.com - Ruby & Rails
X

Choisissez d'abord la catégorieensuite la rubrique :


Traiter du HTML et du XML en Ruby avec Hpricot

Hpricot est une bibliothèque Ruby simple et performante pour parser du HTML et du XML et en extraire des informations. C'est un parser DOM dont l'objectif est de fonctionner même avec du HTML mal formé. Dans ce tutoriel, nous allons voir comment installer cette bibliothèque puis l'utiliser pour récupérer des informations sur une page HTML.

            



I. Installation

Grâce à Gem, le gestionnaire de paquets de Ruby, l'installation de Hpricot est très simple. Il suffit de taper dans une console :
gem install --include-dependencies hpricot
si vous êtes sous Linux ou Unix, il vous faut probablement executer cette commande en tant que root :
sudo gem install --include-dependencies hpricot
L'installation vous propose alors plusieurs version du Gem. Il faut choisir "mswin32” pour Windows ou "ruby” pour le reste (Linux et Mac OS X).

Une fois le gem installé, il suffit d'inclure les fichiers ‘rubygems' et ‘hpricot' dans vos scripts :
require 'rubygems'
require 'hpricot'

II. Un premier aperçu de la bibliothèque :

Pour créer un nouveau document, il suffit de passer un objet String ou IO à la fonction Hpricot().
doc = Hpricot(open("fichier.html"))
=> #<Hpricot::Doc ...>
Grâce au gem open-uri, il est possible de spécifier une adresse HTTP dans open() :
require 'open-uri'
doc = Hpricot(open("http://www.google.com/"))
=> #<Hpricot::Doc ...>
L'objet Doc représente un document HTML complet. En d'autres termes, c'est la racine de l'arbre DOM.

Pour rechercher des éléments dans l'arbre, on peut utiliser les méthodes suivantes :

search : Retourne un objet Elements qui contient tout les éléments correspondant à la recherche effectuée. La recherche peut se faire sous deux formes : CSS ou XPath.

Par exemple pour les CSS :
doc.search('a') -> tous les liens du documents
doc.search('a.post') -> tous les liens ayant la class "post"
doc.search('#menu a') -> tous les liens contenus dans l'élement dont l'id est "menu"
Le support des sélecteurs CSS est très bon et de nombreux sélecteurs supportés par Hpricot ne le sont pas encore dans la plupart des navigateurs !

La liste des expressions CSS supportées est disponible (en anglais) sur : http://code.whytheluckystiff.net/hpricot/wiki/SupportedCSSExpressions

L'objet Elements retourné est une collection d'objets Elem qui représentent chacun un élément du document. Il implémente le module Enumerable, c'est à dire qu'on peut utiliser les méthodes each, collect, inject, first, find, select, all? etc. comme avec un simple tableau.

at : Retourne le premier élément correspondant à la recherche, ou nil. Cette méthode est équivalente à doc.search().first, mais c'est un raccourci très utile.

A partir d'un objet Elem, on peut naviguer dans l'arbre avec les méthodes :

next_node / previous_node : retourne le noeud frère suivant/précédent. Cette méthode parcours tout les noeuds (textes et les commentaires inclus).

next_sibling / previous_sibling : retourne le noeud conteneur frère suivant/précédent. Cette méthode parcours uniquement les noeuds "balises" et pas les noeuds textes ou commentaires.

each_child : itère sur tous les noeuds enfants.

On peut également utiliser à nouveau les méthodes search et at sur un élément afin d'affiner la recherche.

Hpricot propose des méthodes pour extraire les données sous forme de chaine, une fois les éléments intéressants isolés. Si vous avez fait du Javascript, ces fonctions ne vous seront probablement pas étrangères, il s'agit de :

inner_html : renvoie une chaine représentant le code HTML contenu dans l'élément.

inner_text : renvoie une chaine représentant le texte contenu dans l'élément. Toutes les balises sont supprimées. Par exemple :
<a href="http://www.developpez.com">
  Lien vers <b>Développez.com</b>
</a>
Sera transformé par inner_text en :
a.inner_text # => "Lien vers Développez.com"
Pour accéder au contenu des attributs d'un élément, on utilise tout simplement la notation [], comme pour un Hash :
a['href'] # => "http://www.developpez.com"

III. Exemples complets


III-A. Extraire le contenu d'un tableau HTML

On va extraire les titres des 10 premières news de la page d'accueil de Developpez.com En regardant le code HTML de la page d'accueil, on s'aperçoit que chaque titre est contenu dans une cellule (balise td) qui possède la classe "tdt". On va donc extraire ces balises et afficher le contenu texte des 10 premiers titres sur le terminal :
require 'rubygems'
require 'hpricot'
require 'open-uri'

doc = Hpricot(open("http://www.developpez.com/"))

doc.search('td.tdt').first(10).each do |news|
  puts news.inner_text.strip
end
Résultat :
$ ruby dvp.rb 
Tutoriel : Anti-patrons de tests unitaires, par James Carr, traduit par Bruno Orsier
Comparatif : Les librairies graphiques pour PHP, par Hugo Étiévant
Evénement : Conférence Microsoft SOA (Web 2.0, Mashups, SaaS,...), le 25 octobre à Paris
Le Calendrier des événements développeurs, chefs de projets et DBA. Tout voir
Tutoriel : Programmation Orientée Objet avec JavaScript (3ème partie), par Thierry Templier
Formation Developpez : Initiation aux Rich Internet Applications avec AJAX, du 17 au 19 octobre à Paris
Actualité : Publication du code source du Framework .NET avec la sortie de Visual Studio 2008
Systèmes : Les meilleurs cours, tutoriels, témoignages sur Mac et MacOS X
Tutoriel : Présentation du logiciel DriveImage XML : sauvegardez vos disques durs, par H.Azaiez
Livre : Suivez la traduction de la 3ème édition de Thinking In Java. Participez à la traduction

III-B. Extraire les favoris enregistrés sur Delicious

Dans le principe, ce script ressemble au précédent, mais les données sont plus complexes à extraire. On va extraire d'une page du site del.icio.us (social bookmarking) la liste des favoris enregistrés par un utilisateur. En effet Delicious ne propose pas d'API pour récupérer l'intégralité des favoris d'un compte. On va donc télécharger une page HTML, en extraire une liste de lien puis itérer sur cette liste pour extraire les informations qui nous intéressent.

Si on regarde le site dans Firebug, cette structure est assez facile a visualiser : --- screenshot firebug ---
require 'rubygems'
require 'hpricot'
require 'open-uri'

doc = Hpricot(open("http://del.icio.us/taum/ruby"))

bookmarks = doc.search("li.post").collect do |post|
  {:url => post.at("h4 a")['href'],
   :desc => post.at("h4 a").inner_text,
   :tags => post.search("a.tag").collect{ |tag| tag.inner_text },

   # On utilise Date.parse pour obtenir un objet ruby Date au lieu d'une simple chaine
   :date => Date.parse((post % "span.date")['title']),

   # Si l'élément n'est pas présent, on défini count à 0
   :count => post.at("a.pop") ? post.at("a.pop").inner_text.scan(/\d+/)[0].to_i : 0
   }
end
La variable bookmarks contient un tableau de Hashs du type: {:count=>1141, :tags=>["doc", "ruby"], :date=>#<Date: 4908363/2,0,2299161>, :url=>"http://www.ruby-doc.org/core/", :desc=>"RDoc Documentation"}

Ce tableau peut ensuite être facilement utilisé dans votre application, traduit dans un autre format (XML par exemple), sauvegardé dans une base de données, etc...

Mais le site del.icio.us utilise un système de pagination. Il n'est pas possible de récupérer tout les liens en une seule requête, car ils sont séparés sur plusieurs pages. Heureusement, chaque page possède un lien vers la page suivante. Ce lien est de la forme :
<a href="/taum?page=2" accesskey="e" rel="prev">« earlier</a>
L'attribut rel="prev" ici est particulièrement intéressant, car il signifie littéralement en HTML "ce lien représente la page précédente", nous allons donc utiliser cette information pour parcourir toutes les pages :
require 'rubygems'
require 'hpricot'
require 'open-uri'


doc = Hpricot(open("http://del.icio.us/taum/"))

bookmarks = []

while doc

  bookmarks += doc.search("li.post").collect do |post|
    {:url => post.at("h4 a")['href'],
     :desc => post.at("h4 a").inner_text,
     :tags => post.search("a.tag").collect{ |tag| tag.inner_text },
   
     # On utilise Date.parse pour obtenir un objet ruby Date au lieu d'une simple chaine
     :date => Date.parse(post.at("span.date")['title']),
   
     # Si l'élément n'est pas présent (at renvoie nil), retourne 0
     :count => post.at("a.pop") ? post.at("a.pop").inner_text.scan(/\d+/)[0].to_i : 0
     }
  end

  doc = 
  if a = doc.at("a[@rel='prev']")
    puts "Page suivante : #{a['href']}"
    Hpricot(open("http://del.icio.us" + a['href']))
  end 
end

puts "Total : #{bookmarks.length} bookmarks"
On obtient la sortie suivante :
$ ruby delicious.rb 
Page suivante : /taum?page=2
Page suivante : /taum?page=3
Page suivante : /taum?page=4
Page suivante : /taum?page=5
Page suivante : /taum?page=6
Page suivante : /taum?page=7
Page suivante : /taum?page=8
Page suivante : /taum?page=9
Total : 86 bookmarks

IV. Liens utiles

Site officiel : http://code.whytheluckystiff.net/hpricot/



            

Valid XHTML 1.1!Valid CSS!

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.
Contacter le responsable de la rubrique Ruby & Rails