Comme je le disais, je suis en train de porter des scripts PHP en Ruby pour un petit projet perso (dont j’ai d’ailleurs mis les sources en ligne sur mon compte github, j’ai du faire appel intensivement à curb, qui est l’interface ruby à la libcurl.
Et quand j’ai lu les exemples donnés par curb sur la page github, une chose m’a frappé: L’auteur a surement pensé que tout se passait pour le mieux et qu’aucune de ses requêtes n’allaient échouer. Je ne me suis pas laissé faire et j’ai voulu traiter les cas à erreurs, malgré la pauvreté de la documentation.

⇀ Limiter le temps d’une requête

Pour ne pas attendre indéfiniment la résolution d’une requête, on peut utiliser la propriété « timeout ». Il faudra dans ce cas être capable d’attraper l’exception qui sera levée en cas de problème:

def http_request(url)
    curl_handler = Curl::Easy.new()
    curl_handler.url = url

    curl_handler.timeout = 3
    curl_handler.follow_location = true

    begin
        curl_handler.http_get
    rescue Exception => e
        return nil
    end

    return curl_handler.body_str
end

⇀ Valeurs de retour

Curb permet non seulement de récupérer le body d’une requête (via body_str), mais également les headers (avec header_str). Encore moins documenté, on pourra utiliser response_code afin de ne pas parser les headers pour connaitre le code de retour:

    ...
    curl_handler.http_get

    resp = Hash[]
    resp['code'] = curl_handler.response_code

    if curl_handler.response_code == 200
        resp['head'] = curl_handler.header_str
        resp['body'] = curl_handler.body_str
    end

Juste une quick hack rapide vieux de 15000 ans mais qui peut toujours être utile, il s’agit de la récupération via curl de flux RSS et de leur lecture avec SimpleXml.

Et comme pour moi du code bien commenté peut se passer de long discours, c’est parti:

⇀ Téléchargement du flux avec curl

<?php
// Download du flux avec curl:
$curl_hd = curl_init('http://rss.slashdot.org/Slashdot/slashdot');
// On garde une version Atom dans le coin pour la demo:
//$curl_hd = curl_init('http://rss.slashdot.org/Slashdot/slashdotatom');

// On a besoin de la reponse ...
curl_setopt($curl_hd, CURLOPT_RETURNTRANSFER, true);
// ... mais pas des headers:
curl_setopt($curl_hd, CURLOPT_HEADER, 0);

// Recuperation dans $rss du flux, et fermeture de curl.
$rss = curl_exec($curl_hd);
curl_close($curl_hd);

⇀ Deux fonctions bien utiles pour parser les flux RSS et Atom

function parse_rss($doc)
{
    // Pour chaque element...
    foreach($doc->channel->item as $item)
    {
        echo $item->title . "\n";
        echo $item->link . "\n";
        echo $item->description . "\n\n";
    }  
}  

function parse_atom($doc)
{
    // Pour chaque element...
    foreach($doc->entry as $item)
    {
        echo $item->title . "\n";
        echo $item->link->attributes() . "\n";
        echo $item->content . "\n\n";
    }  
}

⇀ Et pour finir, le parsing du flux téléchargé ($rss) avec SimpleXml:

// Creation et parsing de l'element telecharge avec SimpleXml:
$rss_doc = new SimpleXmlElement($rss, LIBXML_NOCDATA);

// Pour faire la distinction entre un fichier atom et un rss,
// on regarde juste quels tags sont presents. Dans le cas
// d'un "channel", ca sera du rss, dans le cas de l'atom,
// ce sera un "entry".
if(isset($rss_doc->channel))
{
    parse_rss($rss_doc);
}  
elseif(isset($rss_doc->entry))
{
    parse_atom($rss_doc);
}

That’s all.

Il y a un peu plus d’un mois, Starcraft 2 est sorti. Ayant été un fan modéré du premier, j’ai pris beaucoup de plaisir à jouer au second, notamment en ligne dans le système de leagues. Et pour suivre mon classement dans les différentes leagues, je me suis amusé à développer quelques scripts en PHP pour télécharger les données à partir du site de Blizzard, les stocker dans mongodb et les visualiser. Etant donné que Blizzard ne propose pas d’API, il a fallu récupérer les pages HTML brutes et les parser avec simplehtmldom. Ces scripts sont mal codés, mais disponibles sur mon compte github.

J’ai décidé de recommencer from scratch cette application, mais en partant vers un langage et des technos que je ne maitrise pas particulièrement, à savoir Ruby puis peut-être plus tard Ruby on Rails.

Cet article décrit comment j’ai très basiquement téléchargé une page simple web et ayant récupéré dans la structure de celle ci quelques informations. Pour cela, j’ai du utiliser les modules Curb (bindings ruby de curl) et Nokogiri (parseur html, xml, sax…):

$ gem install curb
$ gem install nikogiri

On commence par écrire une petite fonction pour télécharger une page:

def http_request(url)
  curl_handler = Curl::Easy.new()
  curl_handler.url = url
  curl_handler.http_get

  return curl_handler.body_str
end

Puis une autre, pour parser la page téléchargée. Nous faisons une utilisation intensive de xpath pour trouver les éléments désirés:

def html_profile_parse(url)
    profile = Hash[]

    profile['link'] = url

    html_contents = http_request(url)

    html_doc = Nokogiri::HTML(html_contents)

    profile_header = html_doc.xpath('//div[@id="profile-header"]')

    profile_header_nick = profile_header.xpath('//h2/a/text()')
    if profile_header_nick.count
        profile_nick = profile_header_nick[0].content
        profile['nick'] = profile_nick
    end

    profile_header_nick_span = profile_header.xpath('//h2/a/span')
    if profile_header_nick_span.count
        profile_id = profile_header_nick_span[0].content
        profile['id'] = profile_id
    end

    profile_header_hf_points_h3 = profile_header.xpath('//h3')
    if profile_header_hf_points_h3.count
        profile_hf = profile_header_hf_points_h3[0].content
        profile['hf'] = profile_hf
    end

    return profile
end

Une fois ce code structuré, cela donne:

#!/usr/bin/env ruby

require 'curb'
require 'nokogiri'

def http_request(url)
    ...
end

def html_profile_parse(url)
    ...
end

url = 'http://eu.battle.net/sc2/fr/profile/491479/1/myc/'
profile = html_profile_parse(url)

print profile

Et dans le shell:

$ chmod a+x fetch.rb
$ ./fetch.rb
{"link"=>"http://eu.battle.net/sc2/fr/profile/491479/1/myc/", "nick"=>"myc", "id"=>"#456", "hf"=>"2950"}