Cet article fait partie d'une série sur le Browser2.

Beaucoup de méthodes de modules weboob retournent des listes d'éléments itérés sur des pages paginées. C'est le cas des résultats de recherches notamment.

Voici l'exemple typique de code avec l'ancien browser permettant de gérer cette pagination :

class ResultsPage(BasePage):
    def iter_results(self):
        # ...

    def get_next_url(self):
        link = self.document.xpath('//a[text()="Next »"]')

        if not link:
            return None

        return link[0].attrib["href"]

class Browser(BaseBrowser):
    # ...
    def search_pattern(self, pattern):
        self.location('/search/%s' % urllib.quote(pattern))

        while True:
            assert self.is_on_page(ResultsPage)

            for video in self.page.iter_results():
                yield video

            next_url = self.page.get_next_url()
            if next_url is None:
                return

            self.location(next_url)

Plus verbeux, on ne fait pas.

Le Browser2 introduit un mécanisme qui permet de gérer beaucoup plus simplement la pagination, dont le fonctionnement interne est simple :

  • La méthode de la page, une fois qu'elle a fini d'envoyer les éléments de la page, doit envoyer une exception NextPage avec le lien ou la requête à exécuter ;
  • Un mécanisme au dessus de la page se charge de capturer cette exception, joue la requête, et appelle de nouveau la méthode sur la nouvelle page.

Bien camouflé dans ListElement et le décorateur pagination, cela donne :

class ResultsPage(HTMLPage):
    @pagination
    @method
    class iter_results(ListElement):
        item_xpath = '//span[@id="miniatura"]'

        next_page = Link(u'//a[text()="Next »"]')

        class item(ItemElement):
            # ...

class Browser(PagesBrowser):
    # ...
    def search_pattern(self, pattern):
        self.search.go(pattern=pattern)
        assert self.search.is_here(pattern=pattern)

        return self.page.iter_results()

Définir l'attribut next_page fait que si le lien est présent, ListElement lance l'exception NextPage. Ensuite, le décorateur pagination se charge de la traiter lorsqu'elle est capturée.

Note : une autre façon de gérer la pagination existe avec la méthode PagesBrowser.pagination().

Cet article fait partie d'une série sur le Browser2.

On a vu la classe ListElement qui itère sur les éléments d'une liste du HTML pour en sortir des objets weboob, je vais maintenant introduire la classe TableElement dont l'objet est de simplifier le traitement des tableaux

Prenons le code suivant :

class HistoryPage(HTMLPage):
    @method
    class get_history(ListElement):
        item_xpath = '//table[@class="liste"]/tbody/tr'

        class item(ItemElement):
            klass = Transaction
            condition = lambda self: len(self.el.xpath('./td')) >= 3

            obj_date = Transaction.Date('./td[1]')
            obj_raw = Transaction.Raw('./td[2]')
            obj_amount = Transaction.Amount('./td[last()]')

Grâce aux filtres, c'est parfaitement lisible, néanmoins ça souffre d'un problème. Si, comme le site du Crédit Mutuel, l'ordre des colonnes change ou des colonnes supplémentaires sont présentes (en fonction des clients), il est difficile de savoir où se trouve l'information.

Pour ce cas, la classe TableElement a été rajoutée afin de faire la sélection de la cellule non pas à partir de son index, mais à partir de son titre.

class HistoryPage(HTMLPage):
    @method
    class get_history(TableElement):
        head_xpath = '//table[@class="liste"]//thead//tr/th/text()'
        item_xpath = '//table[@class="liste"]//tbody/tr'

        col_date = u"Date de l'annonce"
        col_raw = u"Opération"
        col_amount = u"Montant"

        class item(ItemElement):
            klass = Transaction
            condition = lambda self: len(self.el.xpath('./td')) >= 3

            obj_date = Transaction.Date(TableCell('date'))
            obj_raw = Transaction.Raw(TableCell('raw'))
            obj_amount = Transaction.Amount(TableCell('amount'))

Comme on peut le voir, il suffit pour cela de fournir le xpath vers les différents titres de colonnes, ainsi que les titres associés à des identifiants. Ceux-ci peuvent alors être réutilisés dans ItemElement pour sélectionner cette cellule et la traiter dans les filtres suivants.

Le navigateur (ou Browser) est dans Weboob une classe pour faciliter l'écriture de modules. Comme son nom l'indique, il est là pour contenir les fonctions habituelles d'un navigateur Web, évitant au module de devoir gérer les requêtes de bas niveau.

Browser1 est le navigateur historique, basé sur mechanize, et ajoutant des fonctionnalités propres à Weboob. Malheureusement, il est apparu au cours du temps que mechanize avait de nombreuses limitations, et que son architecture rendait certaines choses (comme la gestion des formulaires) très laborieuses.

Browser2 est le nom de code pour le remplacement de ce navigateur. C'est un projet qui dure depuis quelques versions de Weboob, mais est arrivé cette fois dans la branche de développement. Il est cette fois basé sur requests. Cette bibliothèque est plus bas niveau que mechanize, et de nombreuses fonctions sont donc cette fois directement intégrées dans Weboob. Et ça permet de simplifier beaucoup de choses.

Romain a déjà beaucoup écrit sur Browser2, mais je complète par mon opinion. J'ai récrit quelques modules avec, et on peut dire que c'est vraiment de grand changements. En effet, non seulement Browser2 est plus simple, mais l'ajout de fonctions pour simplifier l'extraction de données dans les pages est un vrai plaisir. Cela aurait certes pu être découplé de Browser2, mais le changement de navigateur était une bonne occasion pour remettre les choses à plat et profiter de l'expérience accumulée en écriture de modules.

J'aime donc beaucoup les filtres, un ensemble d'outils pour extraire les données d'une page Web. Ils sont construits d'une manière qui me rappelle un peu la programmation fonctionnelle, on applique des compositions de fonctions pour arriver au résultat désiré. Prenons l'exemple du module pour Poivy. Le code ressemble maintenant à ça :


class HistoryPage(LoggedPage, HTMLPage):
    @method
    class get_calls(ListElement):
        item_xpath = '//table/tbody/tr'
        class item(ItemElement):
            klass = Detail
            obj_datetime = Date(CleanText('td[1] | td[2]'))
            obj_price = CleanDecimal('td[7]', replace_dots=False, default=0)
            obj_currency = u'EUR'
            obj_label = Format(u"%s from %s to %s - %s",
                               CleanText('td[3]'), CleanText('td[4]'),
                               CleanText('td[5]'), CleanText('td[6]'))

Avec ce code, on itère automagiquement sur toutes les lignes de l'historique de la page. On signale par l'héritage de LoggedPage que l'on est certain que le login a réussi si on atteint cette page. Il est inutile de créer et de renvoyer l'objet Detail à chaque itération, la ligne klass suffit Et on parse vraiment très facilement les éléments. On transforme ainsi la septième colonne des lignes du tableau en décimal, c'est le prix de la ligne. On spécifie de ne pas remplacer les points (souvent utilisés comme séparateur pour les milliers en France), et le default=0 est une option magique pour dire que si ce n'est pas un décimal, c'est gratuit (cas des appels inclus dans le forfait). Il fallait autrement vérifier à la main le contenu et gérer si nécessaire les exceptions.

Les fonctions DateTime, Time et Date sont également assez magiques, transformant du texte pas toujours bien ordonné en un objet correspondant python. Par rapport au code précédent, c'est une division par deux du nombre de lignes. Et par beaucoup plus de la lisibilité. Les filtres définis sont très nombreux, permettant l'utilisation d'expression rationnelle, de formater des chaînes, de récupérer très facilement les attributs d'une balise html, etc. Comme ils se combinent, on applique ce dont on a besoin en une seule fois.
Il y a beaucoup d'autres fonctionnalités magie dans Browser2, dont notamment la pagination. Je n'avais jamais ajouté la pagination de l'historique au module poivy, car la pagination n'était pas toujours très pratique sur l'ancien navigateur. À présent, c'est fait en quelques lignes. En ajoutant à la classe get_calls ces quelques lignes :


    next_page = Link("//div[@class='date-navigator center']/span/a[contains(text(), 'Previous')]",
                     default=None)

On va donc chercher le lien vers la page suivante, et on rend un objet Link s'il faut itérer, None si on ne trouve rien. Le navigateur se charge ensuite d'aller sur les pages au fur et à mesure si c'est nécessaire.

En bilan chiffré, la partie navigateur et extraction des données du module Poivy est passé de 125 à 85 lignes (sans copyright, commentaires et lignes vides). En gagnant au passage à la fois en fonctionnalité et en lisibilité du code. Browser2 est vraiment une étape importante pour la simplicité des modules Weboob.

Cet article fait partie d'une série sur le Browser2.

Le parsing des pages est un élément important de weboob car il s'agit du cœur de la problématique à laquelle répond le logiciel, mais aussi la partie la plus sensible et la plus complexe car c'est ici que l'on a affaire aux webmasters incompétents.

Ainsi qu'on l'a vu dans un un post précédent, à chaque url est associée une classe dérivée de BasePage qui est instanciée et qui traite la page pour en sortir des données structurées.

L'ancien régime

Voici à quoi ressemble l'implémentation d'une BasePage avec le browser actuel :

class IndexPage(BasePage):
    def iter_videos(self):
        span_list = self.parser.select(self.document.getroot(), 'span#miniatura')
        for span in span_list:
            a = self.parser.select(span, 'a', 1)
            url = a.attrib['href']
            _id = re.sub(r'/videos/(.+)\.html', r'\1', url)

            video = YoujizzVideo(_id)

            video.thumbnail = BaseImage(span.find('.//img').attrib['data-original'])
            video.thumbnail.url = video.thumbnail.id

            title_el = self.parser.select(span, 'span#title1', 1)
            video.title = to_unicode(title_el.text.strip())

            time_span = self.parser.select(span, 'span.thumbtime span', 1)
            time_txt = time_span.text.strip().replace(';', ':')
            hours, minutes, seconds = 0, 0, 0
            if ':' in time_txt:
                t = time_txt.split(':')
                t.reverse()
                seconds = int(t[0])
                minutes = int(t[1])
                if len(t) == 3:
                    hours = int(t[2])
            elif time_txt != 'N/A':
                raise BrokenPageError('Unable to parse the video duration: %s' % time_txt)

            video.duration = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)

            yield video

La révolution

class IndexPage(HTMLPage):
    @method
    class iter_videos(ListElement):
        item_xpath = '//span[@id="miniatura"]'

        next_page = Link(u'//a[text()="Next »"]')

        class item(ItemElement):
            klass = BaseVideo

            obj_id = Regexp(Link('.//a'), r'/videos/(.+)\.html')
            obj_title = CleanText('.//span[@id="title1"]')
            obj_duration = Duration(CleanText('.//span[@class="thumbtime"]//span'), default=NotAvailable)
            obj_nsfw = True

            def obj_thumbnail(self):
                thumbnail = BaseImage(Attr('.//img', 'data-original')(self))
                thumbnail.url = thumbnail.id
                return thumbnail

On le voit facilement, outre la diminution du nombre de lignes, c'est vraiment beaucoup plus lisible. Ceci s'explique par le fait que si la première implémentation est très procédurale, le nouveau style introduit et permis par le browser2 se veut descriptif.

Certes, il y a beaucoup de magie. Je me propose d'expliquer et de détailler chaque partie afin d'en comprendre les concepts.

HTMLPage

class IndexPage(HTMLPage):

La classe dont hérite IndexPage est HTMLPage. En effet, maintenant ce n'est plus au niveau du browser que s'effectue la désérialisation du document, mais au niveau de la page elle-même, ce qui est plus logique. On aura donc également JSonPage, CSVPage, etc.

ListElement

@method
class iter_videos(ListElement):

Une classe majeure qui a été introduite est ListElement. Elle est censée permettre, grâce à ses attributs, d'automatiser la découverte et l'itération de membres d'une liste dans la page. Le principe est que coder doit rester l'exception, parser les pages doit au maximum être du paramétrage.

Puisqu'on appelle iter_videos comme une méthode à partir du browser, le décorateur @method a été rajouté afin que ce soit fait de façon transparente et analogue à la première implémentation.

L'attribut obligatoire pour ListElement est item_xpath :

item_xpath = '//span[@id="miniatura"]'

On y précise la chaîne xpath qui sera utilisée pour itérer sur les éléments.

next_page = Link(u'//a[text()="Next »"]')

Cet attribut est utile pour traiter la pagination. On verra dans un prochain article comment cela fonctionne.

ItemElement

class item(ItemElement):

Il s'agit de la classe assurant le parsing d'un item de la liste, en instanciant un objet hérité de CapBaseObject et remplissant ses champs.

klass = BaseVideo

Le concept est le suivant : on définit quelle classe on utilise, et pour chaque champ, on crée un attribut obj_<NAME> qui peut être soit un filtre, soit une constante, soit une méthode retournant la valeur.

Un filtre est un objet dont on appelle la méthode __call__ sur l'item et qui a la particularité de pouvoir être chainé avec un ou plusieurs autres filtres.

Le browser2 propose un certain nombre de filtres par défaut, qui sont utilisés dans notre exemple. Le développeur du module peut également définir ses propres filtres.

obj_id = Regexp(Link('.//a'), r'/videos/(.+)\.html')

On utilise le filtre Link pour récupérer le lien de la balise dont on a fournit le xpath, puis on chaîne avec le filtre Regexp pour en extraire l'identifiant de la vidéo qui est contenu dans l'url.

obj_title = CleanText('.//span[@id="title1"]')

Le filtre CleanText est utilisé pour récupérer tout le texte contenu dans l'élément sélectionné et ses fils, et le nettoyer (suppression des multiples espaces, des tabulations, etc.)

obj_duration = Duration(CleanText('.//span[@class="thumbtime"]//span'), default=NotAvailable)

On utilise le filtre CleanText pour retourner le texte purgé de l'élément sélectionné, puis le filtre Duration le parse et renvoie un objet datetime.timedelta.

L'argument default précise que si la durée n'a pas pu être récupérée, la valeur NotAvailabe sera prise. Sans cet argument, une exception est lancée.

obj_nsfw = True

Le champ nsfw se verra tout le temps attribuer la constante True.

def obj_thumbnail(self):
    thumbnail = BaseImage(Attr('.//img', 'data-original')(self))
    thumbnail.url = thumbnail.id
    return thumbnail

Ici, obj_thumbnail est une méthode qui est appelée et qui retourne la valeur que l'on souhaite mettre dans le champ thumbnail.

À noter l'utilisation du filtre Attr qui sélectionne une image pour en extraire la valeur de l'attrib data-original.

Liens vers la documentation

Cet article fait partie d'une série sur le Browser2.

Mechanize offre la possibilité de remplir et de soumettre le formulaire d'une page assez facilement, mais souffre de nombreux problèmes. Prenons par exemple ce code issu du module ING :

def login(self, password):
    # ...
    self.browser.select_form('mrc')
    self.browser.set_all_readonly(False)
    self.browser.controls.append(ClientForm.TextControl('text', 'mrc:mrg', {'value': ''}))
    self.browser.controls.append(ClientForm.TextControl('text', 'AJAXREQUEST', {'value': ''}))
    self.browser['AJAXREQUEST'] = '_viewRoot'
    self.browser['mrc:mrldisplayLogin'] = vk.get_string_code(realpasswd)
    self.browser['mrc:mrg'] = 'mrc:mrg'
    self.browser['sens'] = ['1']
    self.browser.submit()

On peut noter les choses suivantes :

  • La sélection se fait par le browser (à partir du nom du formulaire)
  • Ce système est stateful, c'est à dire qu'on sélectionne un formulaire, puis on change des attributs du browser pour définir les valeurs
  • Par défaut, mechanize respecte les contraintes de la page, empêchant par exemple de modifier les champs désactivés ou les <input type="hidden">. Il est nécessaire d'appeler set_all_readyonly(False) pour contourner ça
  • Le parsing des pages est mal foutu, ce qui fait qu'il loupe parfois des champs. On est alors obligé de les rajouter nous-mêmes
  • Lorsque le champ est à valeurs multiples (un <select> par exemple), on doit passer une liste plutôt qu'une chaîne, et il gueule si il n'y a pas de champ <option> ayant cette valeur

Enfin, si le formulaire n'a pas de nom, il n'est possible que de passer un prédicat de ce genre :

self.browser.select_form(predicate=lambda x: x.attrs.get('id','')=='setInfosCGS')

Browser2 à la rescousse

L'approche du nouveau système est un peu différente. On décorrèle les formulaires du browser en rajoutant une méthode get_form() à la page, qui retourne un objet Form sur lequel on peut modifier et rajouter des champs sans restriction. On réécrirait le code ci-dessus comme suit :

def login(self, password):
    # ...
    form = self.get_form(name='mrc')
    form['AJAXREQUEST'] = '_viewRoot'
    form['mrc:mrldisplayLogin'] = vk.get_string_code(realpasswd)
    form['mrc:mrg'] = 'mrc:mrg'
    form['sens'] = '1'
    form.submit()

La méthode get_form accepte également un paramètre xpath qui, comme son nom l'indique, est une chaîne xpath :

form = self.get_form(xpath='//form[@id="setInfosCGS"]')

Liens vers la documentation

Cet article fait partie d'une série sur le Browser2.

Le browser est séparé en deux niveaux : les pages, qui sont représentées chacune par des classes dérivées de BasePage contenant le code nécessaire au parsing du contenu des pages, et le Browser lui-même qui sert de contrôleur pour la navigation.

L'ancien système

Voici ce à quoi ressemble le système de pages de la première version du browser :

class CreditMutuelBrowser(BaseBrowser):
    PROTOCOL = 'https'
    DOMAIN = 'www.creditmutuel.fr'
    PAGES = {'https://www.creditmutuel.fr/.*/fr/banque/situation_financiere.cgi': AccountsPage,
             'https://www.creditmutuel.fr/.*/fr/banque/mouvements.cgi.*': OperationsPage,
             'https://www.creditmutuel.fr/.*/fr/banque/nr/nr_devbooster.aspx.*': OperationsPage,
            }

    def get_accounts_list(self):
        if not self.is_on_page(AccountsPage):
            self.location('https://www.creditmutuel.fr/%s/fr/banque/situation_financiere.cgi' % self.currentSubBank)
        return self.page.get_list()

Un dictionnaire fait la correspondance entre une expression régulière d'url et la classe d'une page (dérivée de BasePage), et lorsque l'on fait un appel à la méthode location() il tente de retrouver la page à instancier en fonction de l'url. On peut donc ainsi savoir sur quelle page on se trouve avec la méthode is_on_page(klass), qui est un alias à isinstance(self.page, klass).

La classe URL

Voici maintenant comment on décrit les pages dans le browser2 :

class CreditMutuelBrowser(PagesBrowser):
    BASEURL = 'https://www.creditmutuel.fr'

    accounts =   URL('/(?P<subbank>.*)/fr/banque/situation_financiere.cgi',
                     AccountsPage)
    operations = URL('/(?P<subbank>.*)/fr/banque/mouvements.cgi.*',
                     '/(?P<subbank>.*)/fr/banque/nr/nr_devbooster.aspx.*',
                     OperationsPage)

Plutôt que d'avoir un dictionnaire, on décrit des attributs à la classe avec associé d'une part une liste de chemins, et d'autre part la classe à instancier. L'attribut BASEURL permet de préciser le chemin plutôt que des urls complètes.

En outre, la résolution se fait maintenant dans l'ordre de déclaration, ce qui est très utile dans le cas de conflits.

Reverse

Vous avez sans doutes constaté le fait que dans nos expressions régulières ont été précisés des noms pour le pattern subbank. Ceci permet d'une part de récupérer les valeurs dans l'instance de BasePage, mais surtout d'utiliser URL comme un moyen d'accéder directement aux pages avec des paramètres variables.

En effet, URL fournit deux méthodes très sympathiques, URL.go() et URL.stay_or_go(), ce qui nous permet de réécrire la méthode get_accounts_list comme ceci :

    def get_accounts_list(self):
        return self.accounts.stay_or_go(subbank=self.currentSubBank).get_accounts()

L'avantage majeur de ce système est donc d'éviter, comme dans l'exemple du browser1, d'avoir une redondance entre les URLs à associer aux BasePage et les appels à location() où l'on doit à nouveau spécifier l'url.

Liens vers la documentation

Weboob existe depuis plus de quatre ans, et est issu de deux projets, aum (2008) et bnporc (2009). Une composante assez importante est le Browser, une classe accompagnée d'un ensemble d'outils simulant le comportement d'un navigateur et aidant au scrapping des sites web.

Toujours présente aujourd'hui dans weboob, elle tire ses racines d'aum et n'a fait qu'évoluer petit à petit, en gardant la retro-compatibilité de l'API. Autant dire que depuis ses six années d'existence, c'est une usine à gaz sans cohérence qui s'est construite.

Un autre soucis majeur du Browser est qu'il dépend de mechanize, une bibliothèque Python rajoutant une surcouche à urllib2, et qui multiplie les inconvénients (API catastrophique, support limité du SSL, quasiment plus maintenu, etc.).

Mais fort de l'expérience en scrapping qui a été acquise par l'équipe depuis toutes ces années, et grâce à l'apparition de python-requests, une bibliothèque capable de remplacer avantageusement mechanize, nous avons décidé de procéder à l'écriture d'un Browser2.

Le projet a été démarré en novembre 2012 par Laurent Bachelier, mais à cette époque requests était encore assez peu mature et l'API évoluait sans cesses. Cela a conduit à mettre de côté les développements.

Mais la semaine dernière, pris d'un sursaut de motivation, j'ai repris en main le projet afin de supporter la version 2.0 de requests et de dessiner les nouveaux concepts du Browser2 pour aboutir à une première implémentation qui est maintenant mergée dans le dépôt de développement. Plusieurs modules (Crédit Mutuel, Youjizz et Hybride) ont déjà été portés.

Même si l'API est amenée à évoluer, je me propose de rédiger quelques articles durant les prochaines semaines se focalisant sur les nouveaux concepts introduits par le Browser2, notamment :

Un des paradoxe du projet Weboob, c'est la création de ticket qui passe surtout par l'interface Web du gestionnaire de tickets Redmine. Bien entendu, il existe un module Redmine, bien pratique pour les développeurs. Mais pour les utilisateurs, configurer le module juste pour ouvrir un rapport de bug était un peu compliqué, alors que cela devrait être une action simple.
Cependant, tout était déjà presque là pour faire un système agréable. Nous avons le module capable de créer des tickets, et une application pour utiliser le module. Afin de pouvoir facilement importer/exporter des bugs, cette application a été améliorée dans la branche de développement pour être capable de lire un ticket à partir d'un pipe reçu. Concrètement, un simple cat ticket.txt | boobtracker post weboob suffit à envoyer un ticket. De la même façon, un bug peut-être transformé en format texte.
Améliorons encore l'idée. On veut fournir un service simple pour les utilisateurs afin d'ouvrir un bug. Un moyen standard de transférer du texte, c'est l'email. On peut donc un peu modifier la configuration de postfix pour y ajouter un truc comme ça dans le fichier master.cf
weboobreport     unix -        n       n       -       -       
pipeflags=FR user=toto
argv=/usr/local/bin/boobtracker post weboob

Et hop, à la réception d'un mail, on envoie un ticket sur le gestionnaire de tickets. C'est ainsi que les utilisateurs de Weboob peuvent désormais ouvrir des bugs très simplement en envoyant un mail à nomdumodule@issues.weboob.org. Cela évite également de publier les adresses mails des mainteneurs des paquets lors d'un crash, et évitera de perdre des rapports de bugs envoyés directement aux mainteneurs (qui peuvent avoir délaissés le projet depuis). Cette fonctionnalité sera mise en avant dans la prochaine version publiée.

There are often good-willed people around open-source software that while they do not know programming or the specific technologies used, can greatly help the project.

So, what are we looking for? We do not have any translation support for now, so that is out. However, there are a few things you can already do:

  • Provide new website support ideas. To be helpful, you can provide us with everything you know like available APIs and workarounds, existing tools (Python preferred), etc.
  • Provide better logos. Many logos have been hastily done and could be better; the only requirement is that it has to have a parodic or humorous aspect, for legal reasons. I would consider replacing logos that are vulgar a priority.
  • Write packages for your distribution of choice, or simply lobby them to package Weboob and keep it up to date. Since websites break all the time, old versions can become useless quickly.

The simplest way to do that is to create new issues on our tracker. Accepted contributions will get mentioned, unless of course you do not want to.

We often get complaints around Weboob‘s name, and the various application names.

There’s no denying they’re childish. What they are not, however, is sexist.

There is “boob” in the main name, and “boob” is a friendly name referring to (mostly female) breasts. We would, for example, avoid using “tits” or “cunt”, because they are often demeaning1. Though it is a happy accident (our earlier ideas like “woob” and “webob” were taken), we certainly like playing with that.

The idea is the same with application names; it’s all about friendly jokes (like wetboobs the weather tool, which manages to be related to weather and boobs).

If you’re offended, just ask yourself “how is it sexist?”.

As it appears, Weboob is a formidable tool to detect people that are part of the “be offended first, think later” crowd. Interestingly, the crusaders2 are to date all male, and often assert that women can’t like jokes about breasts or sex in general3. How fucked up is that?

They will always make a scene4 on how they’re never going to use Weboob because of names. Guys, here’s the thing: we don’t need you and we certainly don’t want you. I for one am glad we created an Asshole Detector, albeit by accident.

  1. We however are mostly not native English speakers. Mistakes can happen.
  2. This is not an euphemism. They act like they are fighting for a good cause, but it’s only pretend.
  3. And who the hell are they to talk in place of others? That is actual sexism.
  4. So that it is abundantly clear, this is purely about making a scene; I do not care about their opinions or how many penises they may have. All our contributors do not and do not have to like the branding.

Weboob fait la une du magazine Linux Pratique (même si ce n'est pas précisé, c'est bien de ça dont parle l'encart Web). À l'intérieur, 6 pages parlant du logiciel, ainsi qu'une mention dans l'édito.

Hormis une étape bizarre dans la description de l'installation, le contenu est bon. On regrettera cependant que l'auteur n'ait pas pris contact avec les développeurs, on aurait peut-être pu aider (ou au moins ne pas le découvrir par hasard).

Pour cette 14e édition des Rencontres Mondiales du Logiciel Libre, une partie de l'équipe de weboob était présente pour partager autour du projet.

Bilan : deux conférences, programmées exactement au même moment par le plus grand des hasards, ainsi qu'une interview radio.

Radio RMLL

Noé et theo ont été interviewé par Radio RMLL en introduction à leur conférence.

Écouter l'interview

Weboob - Le Web en dehors du navigateur

From the start, creating a new weboob module was a pain, as you had a lot of repetitive copy-paste-alter tasks to do.
Since I can’t stand anything repetitive, I created a tool to speed up module creation, which was inspired from the now retired tools/gen_comic_reader.sh.

Now, when I see code generation, I usually think something must be wrong. In our case, what we are really doing is filling automatically some fields like author, class name, etc. A lot of web frameworks also do that kind of base code generation with success.

It’s a very simple tool, which provides “recipes”, those recipes being a set of templates. Though you can override them, it tries to guess as much details as possible, like your name and e-mail.

Here is an example session:

$ ./tools/boilerplate.py
usage: boilerplate.py [-h] [-a AUTHOR] [-e EMAIL] {base,comic,comic.test} ...
boilerplate.py: error: too few arguments
 
$ ./tools/boilerplate.py base "foo bar"
Created modules/foobar/__init__.py
Created modules/foobar/backend.py
Created modules/foobar/browser.py
Created modules/foobar/pages.py

The backend.py should look like:

# -*- coding: utf-8 -*-
 
# Copyright(C) 2013      Laurent Bachelier
#
# [...]
 
from weboob.tools.backend import BaseBackend
 
from .browser import FooBarBrowser
 
 
__all__ = ['FooBarBackend']
 
 
class FooBarBackend(BaseBackend):
    NAME = 'foobar'
    DESCRIPTION = u'foobar website'
    MAINTAINER = u'Laurent Bachelier'
    EMAIL = 'laurent@bachelier.name'
    VERSION = '0.f'
 
    BROWSER = FooBarBrowser

You can jump into the code right now!

As of now, there is only a minimal “base” recipe and the two “comic” recipes taken from the previous tool; however as boilerplate.py is intended to be highly extensible and maintainable, I hope it will support more specialized recipes soon (for example, “newspaper” or “pastebin”). You know what to do.

I am now working for Budget Insight.

My work should be mostly Python, and for some part contributions to the weboob project.

Even though I’m not looking at all in that branch, I still get at least an e-mail a week for symfony-related work! I guess this is good for Symfony developers, though.

Weboob était présent au 29C3 cette année à Hambourg, en Allemagne, le temps d'un bref lightning talk présenté par Olf, Phlogistique et theo :

Depuis deux semaines, le site de la banque BNP Paribas se met parfois à s'arrêter brusquement de répondre à une requête HTTPS. Or ceci est bien fâcheux, car bien que weboob impose un timeout de 15 secondes à urllib2, il se trouve qu'en l'occurrence l'appel du module à la méthode urlopen() ne se termine jamais.

Armé de strace, j'ai pu constater que systématiquement il s'agit d'un appel à la fonction système read() qui n'obtient pas de réponse. Un peu plus d'investigation m'a révélé que cela se produit lors du handshake SSL.

Après avoir cherché en vain l'explication dans les sources du wrapper SSL de Python, j'ai trouvé un rapport de bug daté de 2007 et concernant la version 2.6. Il explique que le constructeur de la classe SSLSocket effectue le handshake directement, et ce avant qu'il ne soit possible d'interagir avec la socket pour la rendre non bloquante ou, ce qui m'intéresse davantage, de définir un timeout.

Le patch qui a été proposé et intégré introduit un paramètre do_handshake_on_connect, par défaut à True, ainsi que la méthode do_handshake(). Il s'agit d'une solution bas niveau, qui certes s'harmonise très bien à l'API de la lib SSL de Python qui ne brillait déjà pas par sa qualité.

Or le problème que j'ai avec weboob, est qu'on ne tape évidement pas directement dans ssl, mais qu'on passe par mechanize, qui utilise urllib2, qui utilise httplib, qui utilise ssl (ouf). Et aucune de ces bibliothèques ne supporte ce paramètre.

Malgré le fait que mechanize implémente tous les design patterns baveux existants, rendant son code complètement illisible, il ne semble pas possible de changer aisément le handler HTTPS, chose qui aurait pu permettre de surcharger la classe de httplib pour effectuer moi-même le handshake après avoir désactivé do_handshake_on_connect.

La solution crade que j'ai choisie m'a été inspirée d'un patch tout aussi crade envoyé dans un ticket pour contourner un bug de Debian Wheezy (toujours présent d'ailleurs) qui rend OpenSSL inopérant avec le site de la Banque Postale dès lors que l'on utilise le protocole par défaut (SSLv23).

Le code est le suivant, bien dissimulé au fond de weboob/tools/browser/browser.py :

import ssl

def mywrap_socket(sock, *args, **kwargs):
    kwargs['do_handshake_on_connect'] = False
    kwargs['ssl_version'] = kwargs.get('ssl_version', ssl.PROTOCOL_TLSv1)
    sock = ssl.wrap_socketold(sock, *args, **kwargs)
    sock.settimeout(StandardBrowser.DEFAULT_TIMEOUT)
    # check if we are already connected
    try:
        sock.getpeername()
    except:
        sock.do_handshake_on_connect = True
    else:
        sock.do_handshake()
    return sock

ssl.wrap_socketold=ssl.wrap_socket
ssl.wrap_socket=mywrap_socket

httplib utilise la fonction wrap_socket pour créer la socket SSL. En remplaçant cette méthode, en changeant les arguments et en effectuant le handshake nous-même dans le cas où nous sommes déjà connectés, ça peut enfin fonctionner.

Et dire que nous sommes en 2012.

Le site Nolife TV permet de visualiser les émissions de la chaîne de télévision du même nom via un player flash. Il existe un module Weboob pour se passer de logiciel propriétaire, mais comme on va le voir, ils ont mis en place un mécanisme afin d'empêcher le contournement. En vain.

Première version

Fin 2011 je suis tombé sur ce post de leur forum, qui inclus cette mention :

Merci de ne pas intégrer la lecture des vidéos de Nolife Online dans votre client NoAir sans passer par l'interface web de Nolife Online. De façon générale, ne développez pas de lecteur accédant directement aux vidéos de Nolife Online sans notre accord.

Y voyant une provocation au libriste que je suis, et bien que n'étant pas intéressé par les émissions de cette chaîne, je me suis mis en tête de pondre un module weboob. À des fins de recherche, évidemment.

L'analyse du site m'a permis de constater que le player flash faisait deux requêtes à /_newplayer/api/api_player.php :

skey=9fJhXtl%5D%7CFR%3FN%7D%5B%3A%5Fd%22%5F&connect=1&a=US

Celle-ci effectue un connect=1 pour s'annoncer, je ne sais pas trop pourquoi.

skey=9fJhXtl%5D%7CFR%3FN%7D%5B%3A%5Fd%22%5F&a=UEM%7CSEM&quality=0&id%5Fnlshow=1234

Cette requête renvoie entre autres l'URL du fichier vidéo. On constate dans les paramètres le champ id_nlshow qui contient l'ID de la vidéo. Rien de bien compliqué, donc, mis à part cette skey dont je n'arrivais pas à déterminer l'origine.

Après diverses recherches, j'ai fini par comprendre qu'il s'agissait d'une constante. Un peu déçu de ne pas y avoir pensé alors que c'est une « protection » commune, et sans comprendre comme d'habitude l'intérêt de faire ça, j'avais atteint une version fonctionnelle de mon module et savourais ma première victoire.

Seconde version

Il y a quelques mois, une nouvelle version du site de Nolife TV a été mise en ligne, cassant sans ménagement le module de weboob. N'ayant alors pas le courage de me replonger sur le code d'un module que je n'utilise pas, j'ai laissé les choses trainer… jusqu'à aujourd'hui.

Je n'ai pas été déçu du voyage. Après avoir corrigé l'authentification, la recherche et autres trivialités, je me suis alors penché sur la récupération de l'URL des vidéos.

Le player effectue maintenant non pas deux mais trois appels à /_nlfplayer/api/api_player.php :

skey=1b6cf46e6e484e03b370f528441357fd&a=MD5&timestamp=1351359574

Renvoie des paramètres qui ne semblent pas être utilisés par la suite, probablement l'équivalent du connect=1 de la précédente version.

a=EML&skey=893873357e374594eb6fac475846574b&id%5Fnlshow=30833&timestamp=1351359576

Récupère quelques méta-informations sur la vidéo.

quality=0&a=UEM%7CSEM%7CMEM%7CCH%7CSWQ&skey=005f9ae80d77db93b557cc14c404aa76&id%5Fnlshow=30833&timestamp=1351359579

Enfin, le graal, l'URL de la vidéo.

Facile me diriez-vous ? Ce n'est sans compter un petit détail : le paramètre skey change d'un appel à l'autre. On constate également la présence du paramètre timestamp, ce qui laisse immédiatement penser que la clef est calculée à partir de lui. Mais de quelle manière ?

C'est probablement un checksum MD5, et il y a fort à parier qu'une clef privée est utilisée comme salt. Et pour la retrouver, ça risque d'être coton.

Je me suis alors penché sur divers outils pour tenter d'analyser le player flash. swftools, flasm ou flare n'en sont pas venu à bout. J'ai juste été capable de décompresser le .swf, mais aucune information dans ce qui restait un binaire ne m'ont donné la solution.

C'est alors que je suis tombé sur ce site qui m'a sorti ce fichier parfaitement lisible.

On y découvre les lignes suivantes :

public function nl_dataloader(){
    this.loaded_data = new Object();
    super();
    salt = chaine([97, 53, 51, 98, 101, 49, 56, 53, 51, 55, 55, 48, 102, 48, 101, 98, 101, 48, 51, 49, 49, 100, 54, 57, 57, 51, 99, 55, 98, 99, 98, 101]);
}
public static function addKey(_arg1:URLVariables):URLVariables{
    _arg1.timestamp = new Date().time;
    _arg1.skey = getKey(_arg1.timestamp);
    return (_arg1);
}
public static function getKey(_arg1):String{
    return (MD5.encrypt((MD5.encrypt(String(_arg1)) + salt)));
}

À partir de là, la solution est évidente. Transposé en python dans le module weboob, ça donne le code suivant :

SALT = 'a53be1853770f0ebe0311d6993c7bcbe'
def genkey(self):
    timestamp = str(int(time.time()))
    skey = md5(md5(timestamp).hexdigest() + self.SALT).hexdigest()
    return skey, timestamp

Et ça marche. Le module NolifeTV fonctionne de nouveau. Le commit complet pour régler le problème est visible ici.

Conclusion

Comme toujours, les mesures de protection que tentent de mettre en place les sites de streaming video se contournent très facilement. Il n'est plus à démontrer que les seules personnes que ça emmerde, ce sont les utilisateurs honnêtes qui ne peuvent pas regarder leur contenu payant sur la plateforme de leur choix, ni de les voir offline.

Jusqu'alors, un problème dans la sécurité de Weboob est qu'il ne vérifie pas la validité du certificat envoyé par le serveur lorsqu'on établie une connexion SSL. Ceci est dû à l'utilisation de mechanize, la bibliothèque qui simule le comportement d'un navigateur, et qui n'offre pas de tel mécanisme.

L'écriture par Laurent du Browser 2 se passant de mechanize pour le remplacer astucieusement par requests devrait résoudre ce problème proprement dans une prochaine version de Weboob. Malheureusement, ce nouveau browser n'étant pas encore terminé, une solution alternative et provisoire a été proposée par Florent.

Comme mechanize a une gestion opaque du SSL (du fait des multiples couches le séparant de la bibliothèque openssl, et de la médiocrité du code), l'idée est d'établir une première connexion en utilisant directement openssl et de valider le certificat à partir du fingerprint préalablement renseigné dans le module, avant de poursuivre le déroulement normal via mechanize.

Cette mesure de protection peut facilement être contournée, puisqu'il suffit à l'attaquant, dans le cas d'un Man-in-the-middle, de relayer la première connexion, et de s'interposer pour les suivantes qui elles ne seront pas vérifiées par mechanize.

Néanmoins, ce mécanisme temporaire est efficace dans la plupart des cas.

Pour les développeurs de modules, il suffit de renseigner au BaseBrowser l'attribut de classe CERTHASH contenant le SHA-256 de la chaîne de certificats du serveur :

class BNPorc(BaseBrowser):
    DOMAIN = 'www.secure.bnpparibas.net'
    PROTOCOL = 'https'
    CERTHASH = '5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb'

Une autre façon de faire est d'appeler directement la méthode StandardBrowser.lowsslcheck :

browser.lowsslcheck('www.secure.bnpparibas.net', '5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb')

Afin de calculer ce fingerprint, vous pouvez aisément procéder de la manière suivante :

$ python
Python 2.7.3rc2 (default, Apr 22 2012, 22:30:17)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import ssl
>>> from hashlib import sha256
>>> domain = 'www.secure.bnpparibas.net'
>>> sha256(ssl.get_server_certificate((domain,  443))).hexdigest()
'5511f0ff19c982b6351c17b901bfa7419f075edb13f2df41e446248beb7866bb'

Bien évidement, il vaut mieux être certain de ne pas soi-même être victime d'une attaque à ce moment là, car aucune vérification n'est effectuée.

Il ne s'agit que d'une mesure temporaire, en attendant mieux dans le Browser 2.

Dans l'utilisation quotidienne de Weboob, il peut parfois devenir lassant de taper des commandes, qui sont relativement longues. Quelques exemples, taper "subscriptions" dans boobill, "transfer" dans boobank, "forecasts" dans wetboobs. Bien sur on peut activer l'auto-complétion, mais ça reste relativement peu optimal.

Suite à des discussions sur IRC, une nouvelle méthode pour réduire la longueur des commandes a été ajoutée. Le principe est de ne taper que les premières lettres de la commande, comme peut le permettre la commande "ip" (une ou deux lettres sont habituellement suffisantes pour supprimer les ambiguïtés). Si plusieurs solutions existent, une erreur est renvoyée à l'utilisateur avec une liste de suggestions de commandes possibles commençant par le préfixe indiqué.

Du coup, on peut maintenant faire des choses comme :

$ boobill su
* (0177-XXXXXX@nettokom) 0177-XXXXXX - 27,33 € - 14.06.2013 - Einheitstarif
* (06XXXXXXXX@freemobile) 06XXXXXXXX - Forfait 60mn/60SMS à 2 euros

$ wetboobs fore dresde
* Aujourd'hui:    (27C - 27C) Nuit claire
* Mardi 24:       (29C - 29C) Soleil
* Mercredi 25:    (30C - 30C) Soleil
* Jeudi 26:       (23C - 23C) Soleil

$ boobill d
Unknown command: "d"
Do you mean: download, details?

Comme les alias peuvent changer lors d'ajouts de fonctions aux applications, il n'est pas recommandé de les utiliser dans les scripts. Mais au quotidien, ça permet d'utiliser plus rapidement Weboob.

Cela va d'ailleurs dans le même sens qu'une autre optimisation, cette fois-ci dans boobill uniquement, qui permet de ne pas taper l'identifiant de compte dans les commandes usuelles si le compte est unique. On peut ainsi taper :

$ boobill de

Plutôt que :

$ boobill details 0177-XYGHZF@nettokom

Cette année se sont déroulées les Rencontres Mondiales du Logiciel Libre à Genève.

Pour la première fois, Weboob avait son stand, conjointement à celui de Salut à Toi, ce qui permit non seulement de faire connaître le projet à de nombreuses personnes et d'avoir moult conversations intéressantes, mais aussi et surtout d'avoir un lieu pour poser mes fesses.

Comme il y a deux ans, j'ai donné une conférence au sujet de Weboob. Bien que je l'ai très peu préparée et qu'il y avait moins de monde que la dernière fois, ça s'est plutôt bien passé.

Sont disponibles la vidéo et les slides.

Enfin, concernant Genève même, tout est effectivement très cher, il n'y a pas particulièrement de spécialités culinaires, ce qui fait que j'ai mangé italien, chinois, français… Quant au critère discriminant pour moi, à savoir la bière, il faut savoir qu'il est juste impossible de trouver autre chose que de la pisse.

Qui me fréquente un peu a déjà entendu parler d'un logiciel nommé Weboob, pour Web Out Of Browser. Il est donc naturel d'en parler un jour ou l'autre sur ce blog.

Concrètement, ce logiciel permet d'aller chercher des informations sur des sites Web sans utiliser un navigateur traditionnel. Dans un monde idéal, Weboob n'existerait pas et les données seraient facilement exportables sur tout site Web. Ce n'est cependant que rarement le cas, et Weboob a l'ambitieuse mission de pallier aux carences des sites Web. Cela le rend forcément très riche, car le Web regorge de fonctionnalités... Par une petite suite de billets (probablement deux, ou trois, selon l'humeur) je vais présenter tout ce dont je me sers au quotidien (dans un ordre historique d'utilisation), et ce qui me fait gagner un temps relativement important tous les jours.

Relevés de compte

C'est par boobank que j'ai commencé à utiliser (et contribuer à) Weboob. L'application permet d'aller chercher le solde de ses comptes, l'historique des opérations, les opérations à venir, et même d'exporter les données pour l'intégration à un logiciel de comptabilité. J'étais très demandeur de cette fonctionnalité pour les banques françaises, qui contrairement aux banques allemandes forcent la connexion à leur site bourré de publicités plutôt que de permettre un simple export par des protocoles bien connus. Concrètement, avec le clavier virtuel, le changement régulier obligatoire de mots de passe, le site plutôt lent, je passais un temps fou à suivre mes comptes sur la BNP. Alors qu'en deux clics c'était fait pour la Deutsche Bank.

J'ai donc été bluffé quand j'ai utilisé pour la première fois Weboob et le module de la BNP. En une commande dans le terminal je pouvais enfin vérifier que je n'étais pas dans le rouge. Ça m'a tellement plu que j'ai contribué pour la première fois aux alentours d'août 2010 avec quelques patchs pour la BNP.

Depuis, boobank n'a fait que s'améliorer. L'application permet notamment de faire des virements de compte à compte et l'export en .qif. C'est toujours plus long que de passer par aqbanking à la mode allemande, mais le bénéfice est plus qu'appréciable. Autre chose que j'ai pu apprécier lors de mon changement de banque, c'est l'agrégation des données de toutes les banques configurées. J'avais à ce moment là deux banques, et je pouvais suivre le solde des deux au même endroit.

En cerise sur le gâteau, un script existe pour faire des graphiques des comptes à travers munin. Complètement inutile, donc parfaitement indispensable.

En conclusion, Boobank est l'archétype de « Weboob ne devrait pas exister ». Si les banques françaises arrêtaient de se moquer de leurs clients en forçant des accès à travers leur site Web ou des applications spéciales pour Smartphone (et je ne parle même des options payantes à des prix scandaleux pour recevoir des « alertes »), Boobank disparaîtrait immédiatement. Des solutions existent, comme les banques allemandes le prouvent.

Surveillance de l'Elbe

Vivre à Dresde, c'est vivre dans une ville au niveau du fleuve bien changeant. Ce n'est heureusement pas souvent comme en 2002, mais les variations peuvent tout de même être spectaculaires. Je ne suis pas ultra fan du site officiel (notamment car il ne publie pas un réel historique des données), et j'avais donc programmé un petit module Weboob pour suivre cette évolution, et faire des jolis graphiques comme celui-ci :

elbooc_dresden-year.png

(des observateurs un peu curieux pourront détecter une anomalie en décembre, mon serveur étant en panne...).

J'ai longtemps gardé dans mon coin ce module, qui était vraiment peu configurable et écrit (trop) rapidement pour mes besoins spécifiques. J'ai cependant fini par le proposer dans Weboob, et il est présent depuis la version 0.b dans la version officielle et utilisable à travers Wetboobs. Pour savoir le niveau de l'Elbe (ou de tout autre cours d'eau en Saxe), une commande très simple est maintenant disponible :

$ wetboobs gauges Elbe

On peut aussi aller chercher l'historique d'une sonde, ou bien juste la dernière valeur connue. Bien entendu, si je l'ai fait pour la Saxe, on peut imaginer écrire le même module pour la France entière via Vigicrue. Et coupler Weboob à un petit script pour recevoir un mail en cas d'alerte de crue, c'est assez simple. Le tout en attendant que les autorités publiques allemandes et françaises commencent à publier des Données ouvertes.

La suite

Dans un autre billet... Avec notamment la récupération des articles de journaux, le téléchargement de vidéos, et bien d'autres trucs.

Une contrainte qui avait été relevée par les utilisateurs relative au nouveau système de dépôts, est la possibilité d'être informé lorsque les modules sont mis à jour, notamment pour des problèmes de sécurité.

C'est à cette problématique que j'ai répondu en créant un flux RSS actualisé au moment où sont poussées les nouvelles versions de modules, et contenant les dernières mises à jour accompagnées des messages de commits liés.

Afin d'en profiter, abonnez-vous à l'un de ces deux liens, suivant la version de Weboob que vous utilisez :

Un problème majeur que l'on rencontre avec Weboob, est l'évolutivité imprévisible des sites web supportés, qui peuvent casser un module si celui-ci n'est pas suffisamment tolérant, ou si le changement est trop conséquent.

Pour réduire le désagrément de l'utilisateur qui ne peut plus utiliser les fonctionnalités qui améliorent sa vie au quotidien, il convient d'être réactif sur trois points :

La détection du bug

Ce premier point est effectué soit par le buildbot (via ses tests journaliers et post-commits), soit par l'utilisateur qui va sagement produire un rapport de bug. Une idée d'amélioration permettant l'envoi automatisé d'un bugreport contenant toutes les informations nécessaires sera l'objet d'un futur billet.

L'écriture d'un correctif

Chaque module est en principe maintenu par une personne qui l'utilise au quotidien. Malheureusement, il arrive que des modules voient leur mainteneur disparaître sans donner de nouvelles. Il est toujours possible pour un membre de la Core Team dévoué et chômeur d'intervenir rapidement sur un de ces modules, mais ce n'est malheureusement pas toujours possible, notamment en ce qui concerne les modules supportant des sites bancaires.

La diffusion du correctif

Ce point est celui qui va retenir notre attention aujourd'hui.

Jusqu'alors, les modules étant distribués directement avec les sources de Weboob, il convenait de sortir une nouvelle version de l'ensemble du logiciel pour diffuser le moindre correctif.

Pour l'utilisateur, il convenait par simplicité d'utiliser les dépôts Debian de Weboob pour bénéficier du système APT afin de mettre aisément à jour les modules. Pour une installation depuis les sources (sans passer par git), c'est plus contraignant.

C'est pourquoi il a été décidé de développer un système de dépôts Weboob (inspiré de celui de tucan) pour distribuer les modules et de gérer leurs mises à jour.

Les dépôts

Concrètement, lorsqu'on installe Weboob, seuls le core et les tools sont présents sur le système.

Dès que l'utilisateur cherche à ajouter un backend, si le module correspondant n'est pas installé, l'application s'occupe d'aller le chercher sur les dépôts, puis l'installe dans son ~/.weboob/.

Pour mettre à jour les modules, l'utilisateur se contentera de taper la commande suivante :

$ weboob-config update

Ça va chercher sur les dépôts si de nouvelles versions des modules installés sont présentes, et si oui les installe.

Ce n'est pas plus compliqué que ça pour l'utilisateur, qui se contente de mettre à jour de temps en temps, ou lorsqu'il rencontre un problème avec un module, pour vérifier si un correctif n'est pas déjà mis à disposition.

Architecture

Un dépôt est un répertoire servi par HTTP qui contient un fichier modules.list, ainsi que, pour chaque module, un .tar.gz (le module lui-même) et un .png (son icône).

Il existe un dépôt pour chaque version de Weboob, avec plusieurs déclinaisons : main et nsfw.

Côté client, chaque utilisateur possède un fichier ~/.weboob/sources.list contenant les liens vers les dépôts.
Par défaut, il contient :

# List of Weboob repositories
#
# The entries below override the entries above (with
# backends of the same name).

http://updates.weboob.org/%(version)s/main/
# To enable NSFW backends, uncomment the following line:
#http://updates.weboob.org/%(version)s/nsfw/

# DEVELOPMENT
# If you want to hack on Weboob backends, you may add a reference
# to sources, for example:
#file:///home/rom1/src/weboob/modules/

Un éditeur externe peut donc créer son propre dépôt sur lequel il distribue ses propres modules Weboob.

L'outil weboob-repos permet de gérer un dépôt. Lorsqu'un module est mis à jour, sa version (sous forme AAAAMMJJHHMM) est incrémentée.

Développement

Afin de développer des modules sans avoir à les uploader sur un véritable dépôt pour les tester, il est possible de référencer des pseudo-dépôts locaux.

Pour ce faire, il suffit de spécifier le chemin vers le répertoire contenant les modules de cette manière :

file:///path/to/modules/

Après un weboob-config update, les modules qui s'y trouvent sont utilisables directement.

Contrairement aux dépôts distants, les modules des dépôts locaux n'ont pas à être installés. Ils sont chargés directement depuis le répertoire indiqué dans le sources.list.

Conclusion

Ce mode de distribution des modules permet de pousser des mises à jour de modules immédiatement pour tous les utilisateurs d'une version donnée. Pour tester, il vous suffit d'installer la version git, et de lancer une update pour installer tous les modules référencés dans votre fichier ~/.weboob/backends.

La prochaine étape devrait être de sécuriser le téléchargement de ces modules, en utilisant un système basé sur GPG pour signer les tarballs.

Les applications consoles de Weboob fournissent tout un tas d'options et de paramètres qui peuvent paraître complexes ou superflues pour les utilisateurs qui ont eu l'audace de lire la sortie de --help, mais elles apportent quand même la possibilité d'étendre les usages de Weboob.

Je pense que je vais écrire de courts billets fréquemment pour évoquer des astuces d'utilisation de Weboob qui ne sautent pas aux yeux immédiatement mais qui peuvent se révéler fort utiles.

Aujourd'hui, nous allons voir comment télécharger toutes les vidéos résultant d'une recherche faite avec videoob :

$ videoob search lol -s url \
          --no-keys -f multiline | wget -i-

Si on regarde de plus près :

  • search lol: la commande de recherche et le pattern voulu
  • -s url: dans la sortie, on ne veut que le champ 'url' de chaque vidéo
  • -f multiline: le formateur par défaut (très joli et tout) n'est pas celui que l'on veut, on prend multiline qui affiche un champ par ligne
  • --no-keys: on indique que l'on ne veut pas afficher la clef du champ, mais uniquement la valeur
  • | wget -i-: on pipe à wget en lui indiquant de lire les URLs dans l'entrée standard

On aurait également pu utiliser les options -b ou -n pour sélection un backend particulier ou limiter le nombre de résultats (par défaut à 10), ou même rajouter un ou deux filtres sur les résultats avec le paramètre -c.

À comparer avec une solution ruby :

videoob search lol|ruby -ne 'lambda{|a|puts a if a}[$_.scan(/\* \((.+?)\)/).flatten]'|xargs -I@ videoob download @

Ce matin j'ai effectué un changement au système de browser que je voulais faire depuis un moment et qui est de séparer la classe BaseBrowser en deux.

Nous trouvons donc maintenant deux classes :

  • BaseBrowser, dont le fonctionnement reste inchangé et qui a pour objectif d'être dérivée, afin de gérer toutes les interactions possibles que l'on peut avoir avec le site web, et dont les rouages internes doivent être opaques pour l'utilisateur de l'objet.
  • StandardBrowser, qui se veut être utilisable telle quelle, comme le mechanize.Browser, avec la gestion des proxy, cookies, parsers, exceptions, et autres helpers.

J'ai donc cherché à l'utiliser dans le backend OuiFM qui était cassé, et ça donne ceci. On constate que ça va être très pratique dans l'écriture de backends concis que Noé réclame de pouvoir faire depuis longtemps.

Boobank, l'application de gestion des comptes bancaires de Weboob, est une des plus anciennes, car directement issue d'un script que j'avais développé pour le site de la BNP et qui existait avant Weboob. C'est aussi je pense l'une des applications qui a le plus de potentiel, car elle touche à un élément moteur de la vie quotidienne — l'argent — et qu'elle permet de répondre fortement au vide laissé par le fossé qu'il y a entre les sites web des banques et les besoins des utilisateurs.

En plus de permettre la visualiser de ses comptes et des opérations, et d'effectuer des transferts, ce potentiel de boobank est dans le traitement des données ainsi exportées, et des ordres que l'on pourrait automatiquement programmer. C'est de là qu'est venue dans un premier temps le plugin boobank pour Munin, permettant de grapher l'évoluer de ses comptes bancaires dans le temps :

Mais une idée plus intéressante est une application qui permettrait de mettre en relation non seulement les backends de banque, mais également ceux de messages, afin d'avoir un démon qui monitor ses comptes bancaires automatiquement et qui est capable, suivant des règles pré-établies, d'envoyer des alertes (via ces backends ICapMessagesPost), voire de prendre lui-même une décision.

L'exemple d'un fichier de configuration type de cette application boobank-monitor permet de mettre en avant les possibilités :

[main]
interval = 3600
email = weboob@example.org

[alert:MAIL]
type = mail
address = weboob@example.org

[alert:SMS]
type = message
to = 0623456789@sms
message = Attention mon gars, il ne reste que %(CHEQUE.balance)s neuros sur ton compte chèque et %(LIVRET_A.balance)s sur ton livret A.

[alert:DLFP]
type = message
to = T@dlfp
title = Donnez pour Tuxfamily
message = Bonjour, nal.
 Le compte de Tuxfamily est dans le rouge, envoyez les brouzoufs

[account:CHEQUE]
id = 1234567891234567@bnporc

[account:LIVRET_A]
id = 4567891234567892@bnporc

[account:LIVRET_JEUNE]
id = 9876543219876543@bnporc

[rule:Ctoomuch]
if = CHEQUE.balance > 1500
1 = transfer, CHEQUE, LIVRET_A, LIVRET_A.balance - 1500
2 = alert, MAIL

[rule:Cnotenough]
if = EMPTY_CHEQUE and LIVRET_A_ENOUGH_MONEY
1 = transfer, LIVRET_A, CHEQUE, 500
2 = alert, MAIL
3 = alert, SMS

[rule:Cwtf]
if = EMPTY_CHEQUE or not LIVRET_A_ENOUGH_MONEY
1 = ACTION_PANIC

[condition:EMPTY_CHEQUE]
if = CHEQUE.balance  500

[action:ACTION_PANIC]
1 = alert, MAIL
2 = alert, SMS
3 = alert, DLFP

C'est ce chantier qu'il me semble important de mettre en œuvre car la demande est importante. Rien que le fait de n'avoir commité que très récemment le support du format QIF (et encore, il faudrait supporter cette merde d'OFX) et qui a pourtant été apprécié, montre qu'il y a une attente qu'on ne devrait pas aussi tarder à combler.

Dans le but de consolider les équipes du projet Weboob, j'ai mis en place le Planet Weboob.

Le but sera de permettre à chaque contributeur du projet de publier des articles relatifs à son travail sur tel ou tel backend, les astuces utilisées pour boobiser un site, explication de difficultés rencontrées, etc.

La langue retenue pour le moment est le français. En effet, même si Weboob est un projet qui se veut international, la majorité des contributeurs et des sites supportés sont français, aussi même en maîtrisant l'anglais écrit, il est plus aisé de rédiger de longs textes lyriques dans sa langue natale.
Si par la suite cela s'avère nécessaire, on pourra changer.

Les développeurs de Weboob qui souhaitent donc pouvoir bénéficier de cette tribune peuvent m'envoyer par mail un flux RSS correspondant à une catégorie de leur blog qui ne comportera que des articles relatifs à Weboob. Le but de ce planet n'est pas de réunir les histoires de vacances ou les péripéties sexuelles des gens qui gravitent autour du projet, mais bel et bien des articles consacrés à celui-ci.