Archives juillet 2009

Il y a quelques temps maintenant, mais pas si longtemps que cela quand même, je me suis intéressé aux Context Objects in Spans, ces données bibliographiques que l'on peut insérer dans des pages HTML, et qui permet aux utilisateurs, soit de récupérer les informations bibliographies, c'est ce que fait Zotero, ou encore utiliser cette information pour rebondir sur un serveur OpenURL de son choix, c'est ce que fait OpenURL Referrer.

Les solutions ci-dessus exploitant les COinS sont orientés navigateurs, et même si j'utilise quasi uniquement Mozilla Firefox, j'aime bien garder un certain recul par rapport à certains outils pour ne pas être pieds et poings liés à un seul outil omniprésent et omnipotent. Que pouvais-je donc faire pour exploiter ces COinS, mais sans me lier à un navigateur. Mais un proxy pardi ! Aussitôt dit, aussitôt fait.

Le proxy, ou serveur mandataire, traitera les requêtes entre le navigateur et Internet. Il pourra donc pour chaque page HTML qu'il verra passer, l'analyser, et s'il trouve un COinS, faire une action spécifique. Pour le moment, je ne me suis pas trop cassé la tête pour les actions spécifiques comme vous pourrez le voir. L'essentiel pour moi étant de reconnaître les COinS. Pour les traitements intelligents, on verra une prochaine fois !

Et c'est ainsi que j'ai commencé à programmer un petit proxy en Perl. Ou plus exactement, j'en ai programmé deux. Vous verrez que c'est vraiment trivial, surtout avec l'excellent module HTTP::Proxy de BooK.

Le premier script utilise XPath pour retrouver les COinS. C'est probablement la solution la plus élégante. L'expression XPath permettant de capturer tout les COinS d'une page est la suivante :

//span[@class="Z3988"]

Ce qui nous donne le script suivant :

#!/usr/bin/env perl

use strict;
use warnings;

use HTTP::Proxy;
use HTTP::Proxy::BodyFilter::complete;
use HTTP::Proxy::BodyFilter::simple;

use HTML::TreeBuilder::XPath;
use Data::Dump qw( dump );

use URI;
use URI::QueryParam;

my $proxy = HTTP::Proxy->new( @ARGV );

$proxy->push_filter(
    mime => 'text/html',
    response => HTTP::Proxy::BodyFilter::complete->new(),
    response => HTTP::Proxy::BodyFilter::simple->new(
        sub {
            my ( $self, $dataref, $message, $protocol, $buffer ) = @_;

            my $tree = HTML::TreeBuilder::XPath->new_from_content( $$dataref     );
            my $coins_xpath = '//span[@class="Z3988"]';

            my $coins = $tree->findnodes( $coins_xpath );

            my $coins_title_xpath = './@title';
            foreach (@$coins) {
                my $coins_title = $_->findvalue( $coins_title_xpath );

                my $uri = URI->new( 'http://www.bib.ulb.ac.be' . '?' .     $coins_title );
                my $params = $uri->query_form_hash();
                my $issn = $params->{'rft.issn'};
                my $link_uri = URI->new( 'http://bib7.ulb.ac.be/uhtbin/ISSN/'     . $issn);
                my $link = HTML::Element->new( 'a', href =>     $link_uri->as_string );
                $link->push_content('ZOZO');
                $_->push_content( $link );
            }

            $$dataref = $tree->as_HTML;
        }        
    ),
);

$proxy->start;

Donc, on charge les modules nécessaires, on crée le proxy, puis on ajoute les filtres nécessaires à ce proxy. Le premier filtre à ajouter permet de travailler sur un corps complet, j'ai en effet besoin d'avoir toute la page HTML avant de faire le traitement. Le second filtre se charge de retrouver les COinS via l'expression XPath présenter ci-dessus, puis pour chaque noeud trouvé, nous allons récupérer l'attribut title qui contient l'information que nous souhaitons récupérer. Après vient le traitement proprement dit, dans notre cas, ajouter un lien ZOZO pointant vers une URI construit par nos soins. Cette dernière partie doit évidemment évoluer vers quelques choses de plus utile, mais bon, mes objectifs étaient plus du domaine du cas d'étude que d'une réelle application.

Mais bon, le problème de ce proxy est l'usage des expressions XPath, d'après ce que j'ai pu comprendre avec quelques recherches, HTML::Parser serait plus rapide pour ce genre de traitement. Et donc, je me suis lancer dans l'aventure d'écrire un second proxy, similaire, mais ce dernier utilisant HTML::Parser pour retrouver les COinS.

#!/usr/bin/env perl

use strict;
use warnings;

use HTTP::Proxy;
use HTTP::Proxy::BodyFilter::complete;
use HTTP::Proxy::BodyFilter::htmlparser;
use HTML::Parser;
use URI;
use URI::QueryParam;

my $parser = HTML::Parser->new( api_version => 3 );

$parser->handler(
    start => sub {
        my ($self, $tagname, $attr, $attrseq, $text) = @_;

        if ($tagname eq 'span' && $attr->{class} eq 'Z3988') {
            # Must be updated when OpenURL resolver was found
            my $uri = URI->new( 'http://www.bib.ulb.ac.be' . '?' .     $attr->{title} );
            my $params = $uri->query_form_hash();
            my $issn = $params->{'rft.issn'};
            my $link_uri = URI->new( 'http://bib7.ulb.ac.be/uhtbin/ISSN/' .     $issn);
            my $coins_text = '<a href="' . $link_uri->as_string .     '">ZOZO</a>';
            # End of the 'must be updated' code

            $text .= $coins_text;
        }
        $self->{output} .= $text;
    }, "self, tagname, attr, attrseq, text",
);

$parser->handler(
    default => sub {
        my ($self, $text) = @_;
        $self->{output} .= $text;
    }, "self,text",
);

my $proxy = HTTP::Proxy->new( @ARGV );

$proxy->push_filter(
    mime => 'text/html',
    response => HTTP::Proxy::BodyFilter::complete->new(),
    response => HTTP::Proxy::BodyFilter::htmlparser->new( $parser, rw => 1 ),
);

$proxy->start;

Vous voyez que le traitement des COinS par HTML::Parser n'est pas beaucoup plus compliqué.

Ces scripts sont disponibles dans un dépôt Git (via GitHub). Et peut-être aurai-je le temps d'ici peu d'exploiter ces deux scripts pour faire quelque chose d'utile : mettre les COinS ainsi collecté dans une base de données, ou encore les publier dans d'autres formats, etc.

Ah oui, une dernière chose. Si vous voulez tester ces scripts, il faudra désactiver OpenURL Referrer s'il est installé. En effet, pour une raison inconnue, quand il est activé, je ne vois pas mes liens ZOZO, même s'ils sont visibles quand je regarde dans les sources de la page. Il faudrait que je regarde comment fonctionne OpenURL Referrer pour résoudre ce mystère.

Dans un des derniers numéro de GNU/Linux Magazine France, je suis tombé sur un article qui a suscité mon intérêt : SQLite : index et recherche rapide de texte.

Les SGBD(R) offrent quasiment tous leur fonctionnalité de recherche en texte intégral. Ainsi, les habituels clients du marché open source :

Et donc, SQLite offre également cette fonctionnalité, ce qui est très intéressant car SQLite étant conçu pour être embarqué dans d'autres applications, cela ouvre des perspectives non négligeables.

L'article illustre cette fonctionnalité en créant un moteur de recherche pour la documentation disponible dans les répertoires /usr/share/doc de votre distribution Linux. L'article utilise Python comme langage, et je me propose de voir ce que Perl nous offre en la matière. Dans un premier temps, je me contenterai de produire un équivalent de ce qui a été présenté dans l'article, mais j'avoue que j'ai déjà envie de modifier un peu l'outil de manière à le transformer en quelque chose d'utilisable dans ma vie quotidienne.

Compilation de DBD::SQLite

Une des premières choses à faire pour pouvoir utiliser SQLite dans vos scripts Perl est bien entendu d'installer les modules adéquats s'ils ne sont pas déjà présent sur votre système. Le module Perl concerné ici est DBD::SQLite. Evidemment, il s'agit de rajouter un pilote pour DBI afin de permettre à ce dernier de se connecter à des bases de données SQLite. La bonne nouvelle est qu'il n'est pas nécessaire d'avoir SQLite installé sur votre système puisque DBD::SQLite embarque SQLite dans le module. Il vous faut néanmoins un compilateur pour installer le module via CPAN. Néanmoins, dans mon cas, je veux modifier la compilation du module de manière à permettre à SQLite de supporter les opérateurs AND et NOT. Voici comment faire :

edgeoya:./~ $ sudo cpan

cpan shell -- CPAN exploration and modules installation (v1.9402)
Enter 'h' for help.

cpan[1]> get DBD::SQLite
[...]
cpan[2]> look DBD::SQLite

Ce qui vous amènera dans le répertoire d'installation du module. Il vous faudra alors modifier le fichier Makefile.PL, et ajouter une option de compilation. Voici la sortie d'un diff illustrant cette modification :

diff --git a/Makefile.PL b/Makefile.PL
index 8a5be1c..a14b958 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -172,6 +172,7 @@ if ( $sqlite_inc ) {
 my @CC_DEFINE = (
        '-DSQLITE_CORE',
        '-DSQLITE_ENABLE_FTS3',
+        '-DSQLITE_ENABLE_FTS3_PARENTHESIS',
        '-DSQLITE_ENABLE_COLUMN_METADATA',
        '-DNDEBUG=1',
        "-DSQLITE_PTR_SZ=$Config{ptrsize}"

L'option SQLITE_ENABLE_FTS3_PARENTHESIS permet d'utiliser les opérateurs AND et NOT dans les requêtes FTS3, ce qui est plutôt utile, non ? Il permet également d'utiliser les parenthèses pour regrouper les sous-requêtes.

Une fois que la modification est faite, vous pouvez lancer la séquence traditionnelle de l'installation manuelle d'un module :

$ perl Makefile.PL
$ make
$ make test
$ sudo make install

Le script d'indexation

Voici le script d'indexation de la documentation :

#!/usr/bin/env perl

use strict;
use warnings;
use DBI;
use File::Find::Rule;
use Compress::Zlib;
use File::Slurp;

my $db = './doc.db';
my $docDir = '/usr/share/doc';

unlink $db if -e $db;
my $dbh = DBI->connect("dbi:SQLite:dbname=$db", '', '');
$dbh->do('create table docs(package text, file text)');
$dbh->do('create virtual table docs_text using fts3(contents)');

my $sth_docs = $dbh->prepare('insert into docs values (?, ?)');
my $sth_contents = $dbh->prepare('insert into docs_text (rowid, contents) values (?, ?)');

foreach my $file (File::Find::Rule->file()->in($docDir)) {
    my $contents = '';
    if ($file =~ /\.gz$/) {
        my $gz = gzopen( $file, 'rb' );
        my $buffer = '';
        while ($gz->gzread( $buffer ) > 0) {
            $contents .= $buffer;
        }
        $gz->gzclose();
    } else {
        $contents = read_file( $file );
    }

    $file =~ m|/usr/share/doc/([^/]*)|g;
    my $package = $1;

    $sth_docs->execute( $package, $file );
    my $id = $dbh->last_insert_id(undef, undef, 'docs', 'rowid');
    $sth_contents->execute( $id, $contents );
    $dbh->commit;
}

Notez que j'ai suivi comme fil conducteur le script en Python de l'article, je ne me suis pas amusé à perlifier le code.

L'instruction qui nous intéresse est la suivante :

$dbh->do('create virtual table docs_text using fts3(contents)');

Cela permet de créer une table virtuelle qui indexera le contenu d'un attribut de la table.

Je renvois à l'article pour plus d'explication.

Conclusion

Bref, voilà, cela m'aura permis de découvrir une fonctionnalité de SQLite qui me paraît assez intéressante. Nul doute que je parviendrai à l'utiliser dans un futur proche.

Mais avant de me lancer dans mes propres développements, il faut encore que je programme le second outil de l'article, à savoir un script permettant d'interroger la base de données ainsi créée. Ben oui, sinon, ce n'est pas fort intéressant !

Mise à jour du 1er août 2009

Je me suis trompé dans ma retranscription du script de l'article. En effet, dans la table docs, l'auteur de l'article gère le nom du fichier et le nom du paquet contenant ce fichier. Ce que je faisais, de manière assez idiote, était de stocker le nom du fichier, et le contenu de ce fichier. Donc, je me retrouvais avec une base de données énorme ! 1.7Go ce n'est pas rien !

J'ai donc mis à jour le script pour récupérer le nom du package et pour le stocker dans la base de données. Je le fais au moyen d'une regex. Le code ci-dessus a été corrigé, et voici les lignes ajouées ou modifiées :

    $file =~ m|/usr/share/doc/([^/]*)|g;
    my $package = $1;

    $sth_docs->execute( $package, $file );
Je ferai plus attention la prochaine fois ;-)

Finally...

| Aucun Commentaire | Aucun Trackback
It was a project since ... some weeks (or years). Finally I've installed Movable Type and my second first blog is ready.

I can now participate in the Perl Iron Man initiative.

So stay tuned for some Perl and some other things

À propos de cette archive

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

août 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