Archives août 2009

La ronde des questions

| Aucun Commentaire | Aucun Trackback

Fin du mois d'août. Fin des vacances1, et avant de commencer une nouvelle année académique, il est nécessaire de terminer la précédente. Et donc, voici venu le temps des secondes sessions.

En tant que prof, j'aime bien les examens oraux, et j'aime bien les questions tirées au hasard, c'est pour cela que j'avais développé un petit script CGI en Perl. Rien de bien excitant.

Mais durant mes lectures d'été, je suis tombé sur un billet d'un mongueur français traitant d'une autre autre manière de faire des applications web. Ce mongueur, c'est sukria, et cette autre manière de faire, c'est Dancer2.

Bref, cela m'a donné envie de passer d'un script CGI à une application web autonome ! La voici :

#!/usr/bin/env perl

use strict;
use warnings;

use YAML;
use Dancer;
use Template;

my $file = shift(@ARGV) or die "Usage: $0 questions.yml";
my $questions = YAML::LoadFile($file);

get '/random' => sub {
    my $random_index = int rand scalar(@{$questions});
    my $question = $questions->[ $random_index ];
    
    template 'random' => { question => $question, number => $random_index + 1 };
};

Dancer->dance();

Et donc, je lance cette application à partir d'une ligne de commande :

$ questions_roll.pl /path/to/questions.yaml

Et je peux voir les lignes suivantes :

~ >> Listening on 127.0.0.1:3000 == Entering the development dance floor … ~

Et donc, en pointant mon navigateur à l'adresse http://localhost:3000/random, je peux voir s'afficher une question au hasard.

Vraiment simple, non ? Tellement simple qu'il m'est déjà venu quelques idées d'améliorations. Ce ne sera probablement plus pour cette session d'examen, mais je garde ces idées au chaud.

Notes :

1 enfin, ce qui me tient lieu de vacances

2 en fait, Dancer est lui même une réécriture de Sinatra en Perl. The Pragmatic Bookshelf a édité deux screencasts sur ce sujet. Je n'ai pas encore eu le temps de les regarder, mais cela ne tardera pas !

Cybook Gen3 et le web

| Aucun Commentaire | Aucun Trackback

Depuis presqu'un an maintenant, je suis l'heureux propriétaire d'un Cybook Gen3, à savoir un e-book employant la technologie de l'encre électronique. La lecture sur ce genre de support étant quasi égale à celle d'un support papier, je dois avouer que je n'hésite pas beaucoup quand l'occasion m'est donnée d'utiliser ma liseuse1. Or, il m'arrive souvent d'avoir plusieurs pages sur le web dont j'aimerais achever (ou commencer) la lecture dans le métro. Evidemment, je pourrais utiliser des outils comme Scrapbook ou Zotero, mais cela impliquerait que la lecture se fasse sur un ordinateur. Or, mon Gen3 est nettement plus agréable à lire et bien plus facile à manipuler qu'un ordinateur, même un EeePC. Bref, c'est l'occasion idéale pour programmer un peu !!!

Alors, qui dit récupération d'information sur le web dit LWP::UserAgent, et comme j'ai quelques besoins spécifiques, je travaillerai donc sur le HTML avec quelques outils comme HTML::Parser ou encore HTML::TreeBuilder::XPath.

1.1 Les besoins spécifiques

1.1.1 Sélection d'une partie de la page

Le premier problème vient qu'une page web contient des informations qui ne m'intéressent pas, les menus entre autres. Donc, j'ai besoin de sélectionner uniquement une partie de la page. De nos jours, cela peut être fait assez facilement avec XPath. J'ai donc écrit un petit module qui permet de récupérer la portion d'HTML intéressante sur base d'une expression XPath : il s'agit d'HTML::Excerpt::FromXPath (le dépôt git est disponible sur GitHub).

1.1.2 Récupérer les images

Le second problème consiste à récupérer les images de la portion d'HTML extraite précédemment. Ce n'est pas bien compliqué, mais il y a plusieurs étapes à suivre pour obtenir le résultat souhaité :

  1. transformer chaque lien vers une image en un lien absolu ; nous effectuerons cette tâche avec HTML::ResolveLink ;
  2. télécharger chaque image et la stocker en local ; lors de ce téléchargement, nous pouvons souhaiter effectuer quelques traitements spécifiques :
    1. stocker l'image dans un répertoire spécifique ;
    2. changer le nom de l'image ;
    3. changer le format de l'image ; dans le cas de ma liseuse, elle ne gère pas le format PNG, je dois donc convertir les images en JPEG.
  3. sur base des transformations effectuées précédemment, il est nécessaire de modifier l'extrait d'HTML afin qu'une fois sauvegardé, nous soyons capable de voir les images, etc.

1.1.3 Convertir le fichier HTML en fichier Mobibook

Bien que le Cybook Gen3 soit capable de lire du HTML, je préfère le convertir en Mobibook, en effet, c'est la seule manière de pouvoir accès aux images. Pour la création de ce fichier, j'ai le choix entre plusieurs solutions, celle que j'ai retenue finalement est d'employer mobigenlinux, un programme créé par Mobipocket. Les fichiers produits étaient ceux dont la finition était la plus intéressante. J'ai donc écris un petit wrapper, Mobigen::Command, permettant d'utiliser le programme à partir d'un script Perl.

1.1.4 Finalement

Au terme de tout ces efforts, je suis maintenant capable de lancer la commande suivante me permettant de capturer un extrait de page web sur base d'une expression XPath :

uri2mobi.pl -u http://myurl/to/my/article -x //div[@class="content"] -o myebook.pdb

Ce programme est également disponible sur GitHub. Je suis finalement assez content de cet outil. Le seul point noir étant que je dois trouver l'expression XPath me permettant de sélectionner la partie intéressante. Et même si avec une extension comme XPather pour Firefox, c'est relativement facile, cela devient assez fastidieux quand j'ai une dizaine de liens à faire !

Je parlerai bientôt de la solution que j'ai trouvé !

Notes :

1 le terme d'e-book n'est pas le plus adapté qui soit, en effet, il suggère que l'object est un remplaçant du livre, ce qui n'est pas le cas à mon avis, je lui préfère donc le terme qui est employé un peu partout sur le web : à savoir celui de liseuse.

YAPC::EU 2009 et Twitter

| Aucun Commentaire | Aucun Trackback

Le YAPC::EU 2009 s'est tenu début de cette semaine. Du 3 au 5 août pour être précis. Cela se passait à Lisbonne, et malheureusement, je n'y étais pas. Pas vraiment de bonnes excuses, d'autant plus que le programme était vraiment intéressant. Néanmoins, à l'heure actuelle, les conférences se déroulent localement, et c'est le plus intéressant bien entendu, mais il y également moyen de les suivre en ligne. D'une part sur IRC, où cela se passait sur #yapc du serveur irc.perl.org, et d'autre part sur le web, avec le microblogging. Un des sites les plus connus pour cette activité est Twitter.

Mais comme cela a été mis en avant par d'autres (ou ), il est intéressant de conserver une version des tweets publiés lors des conférences (et plus généralement aussi d'ailleurs), je me suis donc assigné comme tâche de reprendre le code PHP, et d'en faire une version Perl. Cela me donne ainsi l'occasion de jouer avec de nouveaux modules.

Mes compagnons seront :

  • DBI, et DBD::SQLite : en effet, les tweets seront sauvés dans une base de données. A l'origine, c'était une base de données MySQL, mais dans mon cas, je vais utiliser SQLite ;
  • Net::Twitter pour interroger l'API de Twitter ;
  • et finalement, DBIx::Class pour accéder à la base de données d'une manière plus abstraite.

Voici le code de récupération des tweets pour le YAPC::EU 2009 (le hashag était #yapceu2009 :

#!/usr/bin/env perl

use Modern::Perl;

package KeepTweet::Schema::Result::Tweet;

use base 'DBIx::Class';

__PACKAGE__->load_components( qw/ Core / );
__PACKAGE__->table('tw_hashtag_search');

__PACKAGE__->add_columns(
    'id' => {
      'data_type' => 'bigint',
      'is_auto_increment' => 1,
      'default_value' => undef,
      'is_foreign_key' => 0,
      'name' => 'id',
      'is_nullable' => 0,
      'size' => '20'
    },
    'tw_hashtag' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_hashtag',
      'is_nullable' => 1,
      'size' => '32'
    },
    'tw_id' => {
      'data_type' => 'bigint',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_id',
      'is_nullable' => 1,
      'size' => '20'
    },
    'tw_lang' => {
      'data_type' => 'char',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_lang',
      'is_nullable' => 1,
      'size' => '2'
    },
    'tw_source' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_source',
      'is_nullable' => 1,
      'size' => '255'
    },
    'tw_text' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_text',
      'is_nullable' => 1,
      'size' => '160'
    },
    'tw_created_at' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_created_at',
      'is_nullable' => 1,
      'size' => '64'
    },
    'tw_to_user_id' => {
      'data_type' => 'bigint',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_to_user_id',
      'is_nullable' => 1,
      'size' => '20'
    },
    'tw_to_user' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_to_user',
      'is_nullable' => 1,
      'size' => '32'
    },
    'tw_from_user_id' => {
      'data_type' => 'bigint',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_from_user_id',
      'is_nullable' => 1,
      'size' => '20'
    },
    'tw_from_user' => {
      'data_type' => 'varchar',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'tw_from_user',
      'is_nullable' => 1,
      'size' => '32'
    },
    'record_add_date' => {
      'data_type' => 'datetime',
      'is_auto_increment' => 0,
      'default_value' => 'null',
      'is_foreign_key' => 0,
      'name' => 'record_add_date',
      'is_nullable' => 1,
      'size' => 0
    },
);

__PACKAGE__->set_primary_key('id');
__PACKAGE__->add_unique_constraint('tw_id', [ 'tw_id' ] );

package KeepTweet::Schema;

use base 'DBIx::Class::Schema';

# __PACKAGE__->load_namespaces();
__PACKAGE__->load_classes( qw( Result::Tweet ) );

package main;

use Net::Twitter;
use Getopt::Long;
use Data::Dump;

my $db_file = shift or die "Usage: $0 twitter_db.sqlite\n";
my $schema = KeepTweet::Schema->connect("dbi:SQLite:$db_file");

$schema->deploy() unless -e $db_file;

my $bird = Net::Twitter->new( traits => [ qw( API::Search ) ]);
$bird->username($ENV{TWITTER_LOGIN});
$bird->password($ENV{TWITTER_PASS});

my $count = 0;
foreach my $page (1..15) {
    my $tweets = $bird->search({ q => '#yapceu2009', rpp => 100, page => $page });
    next unless scalar(@{$tweets->{results}}) > 0;
    
    foreach my $tweet (@{$tweets->{results}}) {
        my $ktweet = $schema->resultset('Result::Tweet')->new( {
            tw_text => $tweet->{text},
            tw_hashtag => '#yapceu2009',
            tw_id => $tweet->{id},
            tw_lang => $tweet->{iso_language_code},
            tw_source => $tweet->{source},
            tw_created_at => $tweet->{created_at},
            tw_to_user_id => $tweet->{to_user_id},
            tw_to_user => $tweet->{to_user},
            tw_from_user_id => $tweet->{from_user_id},
            tw_from_user => $tweet->{from_user},
            record_add_date => 'now()',
        });
        
        $schema->txn_do( sub { $ktweet->insert } );
    }

    $count += scalar(@{$tweets->{results}});
}

say 'Finished: ', $count, ' tweets processed';

Le code n'est pas vraiment compliqué à comprendre. Mais il n'est pas non plus assez abstrait, et j'ai bien envie de créer un App::KeepTweet qui permettrait de gérer la récupération de tweets. Je retravaillerais la base de données dans un premier temps, puis l'interface utilisateur, probablement une CLI, de manière à pouvoir mettre l'exécution du programme dans une crontab. Donc, probablement plus de nouvelles dans quelques jours.

Et cela m'aura ainsi permis de récupérer les 413 tweets concernant cette conférence. Je n'ai pas encore pris le temps de tout lire, mais cela m'aura permis de glâner quelques liens vers les diapositives de quelques présentations intéressantes.

J'avais parlé précédemment des fonctionnalités de texte intégral de SQLIte en programmant un petit script qui se charge d'indexer le contenu de la documentation de mon système (c'est-à-dire le contenu du répertoire /usr/share/doc/). Indexer, c'est bien, mais encore faut-il pouvoir exploiter la base de données ainsi créée, il me semble donc intéressant de voir comment nous pouvoir interroger cette base de données. C'est ce que je me propose de faire aujourd'hui.

Alors, comment est-ce que cela fonctionne ? Tout simplement en utilisant l'opérateur MATCH. Ce dernier indique que ce qui suit est une requête en texte intégral, et donc qu'un traitement particulier doit lui être appliqué. Pour le reste, c'est une requête SQL classique. Comme nous avons deux tables dans notre schéma, nous devrons faire une jointure.

Voici donc le script en question :

#!/usr/bin/env perl

use Modern::Perl;

use DBI;
use SQL::Library;
use Getopt::Long;
use File::Slurp;

my $config = {
    database => './doc.db',
    query_name => 'basic',
};

GetOptions( $config, 'library=s', 'query_name=s', 'search=s', 'database=s' );

my ( $sql_lib, $dbh ) = _process_options( $config );
_run( $config, $dbh, $sql_lib );

sub _process_options {
    _usage() if not exists $config->{search};
    my $sql_content;

    if (not exists $config->{library}) {
        $sql_content = read_file( \*DATA );
    } else {
        read_file($config->{library});
    }

    my $sql_lib = SQL::Library->new({ lib => [ $sql_content ] });
    my $dbh = DBI->connect('dbi:SQLite:dbname=' . $config->{database});    

    return $sql_lib, $dbh;
}

sub _run {
    my ( $config, $dbh, $sql_lib ) = @_;

    my $sth = $dbh->prepare( $sql_lib->retr( $config->{query_name} ) );
    $sth->execute( $config->{search} );

    my $cpt = 1;
    while (my $doc = $sth->fetchrow_hashref()) {
        say $cpt++, '. '; 
        foreach my $key (keys %$doc) {
            say "\t", $key, ': ', $doc->{$key}
        }
    }

    $dbh->disconnect;
}

sub _usage {
    die "Usage: $0 --search query_search [--library SQL_library --query_name snipper|basic --database SQLite_database]\n";
}

__DATA__
[basic]
select package, file from docs join docs_text on docs.rowid == docs_text.rowid where docs_text.contents match ?
[snippet]
select package, file, snippet(docs_text) from docs join docs_text on docs.rowid == docs_text.rowid where docs_text.contents match ?

Le code étant entièrement le mien (ce qui m'évite de faire des erreurs de retranscription ;-) ), je me suis permis d'utiliser la force du Perl, à savoir le CPAN. Alors, j'ai utilisé Getopt::Long pour avoir accès à une gestion de la ligne de commande. Mon script pourra donc recevoir les options suivantes :

  • library : qui permet de spécifier un fichier contenant des requêtes SQL à exécuter sur la base de données ;
  • query_name : qui permet de choisir la requête à exécuter parmi les requêtes présentes dans la bibliothèque des requêtes disponibles (donc l'option library ci-dessus) ;
  • search : qui est la recherche que l'on veut effectuer ;
  • database : qui permet de choisir la base de données SQLite que l'on souhaite interroger. Evidemment, cette base de données doit avoir été créée le script précédent.

Il y a des valeurs par défaut, et donc, dans le meilleur des cas, on pourrait lancer le script de la manière suivante

querier.pl -s '(color AND absorbed) OR (printer NOT device)'

Bon, évidemment, dans le pire des cas, ce sera plutôt :

querier.pl -s '(color AND absorbed) OR (printer NOT device)' -d ~/doc.db -l ~/queries.ini -q snippet

Alors, que dire d'autres ? Et bien, que j'ai utilisé SQL::Library pour me permettre de rajouter des requêtes SQL sans pour autant modifier mon script. La seule chose que je devrai faire, c'est créer un fichier dont la struture est celle d'un fichier INI, ces fameux fichiers de configuration. Et dans ce fichier, je pourrai ajouter les requêtes souhaitées. Pr défaut, le script propose deux requêtes, la basic et la snippet. Cela permet de voir comment structurer les autres requêtes :

[basic]
select package, file from docs join docs_text on docs.rowid == docs_text.rowid where docs_text.contents match ?
[snippet]
select package, file, snippet(docs_text) from docs join docs_text on docs.rowid == docs_text.rowid where docs_text.contents match ?

Pour le reste, cela devrait être relativement clair, non ?

À propos de cette archive

Cette page est une archive des notes de août 2009 listées de la plus récente à la plus ancienne.

juillet 2009 est l'archive précédente.

septembre 2009 est l'archive suivante.

Retrouvez le contenu récent sur l'index principal ou allez dans les archives pour retrouver tout le contenu.

Pages

Powered by Movable Type 4.261