Skip to content

  • Projects
  • Groups
  • Snippets
  • Help
    • Loading...
    • Help
    • Support
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
weboob
weboob
  • Project overview
    • Project overview
    • Details
    • Activity
    • Releases
    • Cycle Analytics
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Charts
  • Issues 180
    • Issues 180
    • List
    • Boards
    • Labels
    • Milestones
  • Merge Requests 53
    • Merge Requests 53
  • CI / CD
    • CI / CD
    • Pipelines
    • Jobs
    • Schedules
    • Charts
  • Wiki
    • Wiki
  • Members
    • Members
  • Collapse sidebar
  • Activity
  • Graph
  • Charts
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
  • weboob
  • weboobweboob
  • Wiki
    • Fr
  • guide weboob

guide weboob

Last edited by ntome May 13, 2020
Page history
  • Weboob
  • Connecteurs
    • CapBank
    • Modèles de données de CapBank
    • Utilisation par une application
  • Bas-niveau
  • Exemple d'un connecteur
    • Fichier __init__.py
    • Fichier module.py
    • Fichier browser.py
      • Navigation
      • Parsing
    • Fichier pages.py
      • Browser 2
      • Extraction d'une liste d'éléments
      • Extraction d'un élément
      • Filtres
      • Constantes
      • Surcharge d'un filtre
    • Gérer le login
  • Commencer à écrire un module
    • Créer une coquille vide
    • Tester son module

Weboob

Weboob est un framework dédié à l'extraction de données provenant de sites web et à l'agrégation de ces données. Il permet d'interagir avec ces sites web.

En plus du framework permettant l'écriture de connecteurs, weboob est fourni avec un ensemble de connecteurs, chacun interagissant avec un site existant. Les connecteurs sont différents car chaque site a un fonctionnement différent. Les sites peuvent êtredes sites bancaires ou facturiers, mais aussi des sites de vidéo, de cuisine, de météo, etc.

Enfin, weboob inclut des applications (graphiques ou en ligne de commande) pour utiliser ces connecteurs. Il est possible d'écrire sa propre application en utilisant l'agrégation des différents connecteurs, grâce à une API simple. Par exemple, des projets externes se servent de weboob comme Kresus qui est un PFM ou Flatisfy qui permet de chercher des annonces immobilières, ou encore Budget-Insight.

Connecteurs

Les connecteurs sont groupés en capabilities, une "capability" étant le type d'action que l'on peut avoir sur un site, par exemple la CapBank concerne l'agrégation bancaire : consultation des comptes et des transactions bancaires.

CapBank

Extrait de weboob/capabilities/bank.py :

class CapBank(CapCollection):
    def iter_accounts(self):
        raise NotImplementedError()

    def get_account(self, id):
        return find_object(self.iter_accounts(), id=id, error=AccountNotFound)

    def iter_history(self, account):
        raise NotImplementedError()

    def iter_coming(self, account):
        raise NotImplementedError()

CapBank est une interface abstraite qui décrit l'API des connecteurs bancaires. C'est cette interface qu'appelleront les applications désirant faire l'agrégation de données bancaires (comme boobank). Chaque connecteur devra donc implémenter les méthodes de CapBank.

Certaines méthodes sont obligatoires et d'autres optionnelles. Par exemple, la méthode iter_accounts() est obligatoire car, sans elle, il ne resterait plus rien. Au contraire, la méthode iter_coming(account) est optionnelle car certains sites ne présentent pas les opérations bancaires à venir, ou du moins pas sur tous les comptes.

Modèles de données de CapBank

Les méthodes de CapBank retourneront des objets du modèle de données associé, par exemple chaque module implémentant la méthode CapBank.iter_accounts() retournera des objets de la classe Account, avec des attributs bien définis, obligatoires et optionnels.

class BaseObject(with_metaclass(_BaseObjectMeta, StrConv, object)):
    id = None
    backend = None
    url = StringField('url')

class BaseAccount(BaseObject, Currency):
    label =          StringField('Pretty label')
    currency =       StringField('Currency', default=None)

class Account(BaseAccount):
    type =      EnumField('Type of account', AccountType, default=TYPE_UNKNOWN)
    owner_type = StringField('Usage of account')  # cf AccountOwnerType class
    balance =   DecimalField('Balance on this bank account')
    coming =    DecimalField('Sum of coming movements')
    iban =      StringField('International Bank Account Number')

Utilisation par une application

Boobank est une application basique en ligne de commande permettant de consulter des comptes bancaires. Indépendamment du connecteur utilisé, boobank passera par l'API de CapBank et appellera des méthodes comme iter_accounts(), ce qui abstrait les différences entre les différents connecteurs.

Bas-niveau

Au contraire de Selenium, weboob est un framework basé sur la bibliothèque requests, qui n'exécute pas le code javascript contenu dans les pages visitées.

Exemple d'un connecteur

Un module weboob est souvent composé par 4 fichiers.

Fichier __init__.py

Ce fichier est obligatoire dans tout dossier contenant plusieurs modules de code Python. Un connecteur weboob n'y fait pas exception. Ce fichier est le point d'entrée du connecteur même s'il ne contient quasiment rien. Il doit faire référence au fichier module.py.

from .module import DemoModule

__all__ = ['DemoModule']

Fichier module.py

Ce fichier est le point d'entrée du connecteur. Il ne contient qu'une seule classe.

Celle-ci contient les métadonnées du connecteur, comme son nom, les informations du créateur du module, la licence.

class DemoModule(Module, CapBank):
    NAME = 'demov1'
    DESCRIPTION = 'Demo v1'
    MAINTAINER = 'John Doe'
    EMAIL = 'johndoe@example.com'
    LICENSE = 'LGPLv3+'
    VERSION = '1.6'

Est aussi présente la description de la configuration nécessaire pour utiliser le connecteur, par exemple s'il faut un renseigner un nom d'utilisateur, un mot de passe, une date de naissance, un choix particuliers/professionnels.

CONFIG = BackendConfig(
    ValueBackendPassword('login', label='Identifiant', masked=False),
    ValueBackendPassword('password', label='Mot de passe')
)

Enfin, le principal, c'est là qu'est implémentée l'interface de la ou les capabilities. Par exemple, un module héritera de la classe abstraite CapBank et implémentera les différentes méthodes, souvent en appelant le browser.

BROWSER = DemoBrowser

def create_default_browser(self):
    return self.create_browser(self.config['login'].get(), self.config['password'].get())

def iter_accounts(self):
    return self.browser.iter_accounts()

def iter_history(self, account):
    return self.browser.iter_history(account)

Fichier browser.py

Ce fichier contient le browser, une classe qui fera toute la navigation sur le site web que l'on veut scraper.

On y déclare par exemple l'adresse principale du site (voir le champ BASEURL), ainsi que toutes les adresses qui serviront durant le scraping. Ce sont des expressions régulières, ce qui permet de gérer les adresses avec des parties variables.

class DemoBrowser(LoginBrowser):
    BASEURL = 'https://people.lan.budget-insight.com/~ntome/fake_bank.wsgi/v1/'

    login = URL(r'login', LoginPage)
    accounts = URL(r'accounts$', AccountsPage)
    history = URL(r'accounts/(?P<account>\w+)', HistoryPage)

Navigation

Une méthode comme iter_accounts() ira sur une ou plusieurs de ces pages afin de pouvoir lister les comptes correctement. Par exemple, si pour un site donné, les cartes de crédit se trouvent sur une page différente de la page des comptes courant, il faudra lister les adresses des 2 pages, et que le browser les charge.

Pour charger une URL, on peut utiliser les adresses pré-enregistrées et appeler la méthode go(). Par exemple, pour aller sur https://people.lan.budget-insight.com/~ntome/fake_bank.wsgi/v1/accounts :

def iter_accounts(self):
    self.accounts.go()

On aurait également pu utiliser :

def iter_accounts(self):
    self.location('https://people.lan.budget-insight.com/~ntome/fake_bank.wsgi/v1/accounts')

Parsing

Il faut explorer le site de la banque avec un navigateur pour voir où il faudra aller pour trouver les informations souhaitées.

Une fois que le browser va sur les bonnes pages, il faut extraire les informations présentes sur celles-ci, et cette extraction se fera dans pages.py. Ainsi, browser.py s'occupe de la navigation et délègue le parsing à pages.py.

def iter_accounts(self):
    self.accounts.go()
    return self.page.iter_accounts()

self.page sera la page courante que l'on vient de visiter, et l'on pourra accéder à ses fonctions (définies dans pages.py, que nous allons voir).

XXX déplacer ?

Évidemment, quand on écrit un nouveau module, on ne peut pas prédire toutes les pages à visiter puisque l'on ne connait qu'un échantillon très restreint des utilisateurs. Quand on part d'un module existant, on peut s'aider des commentaires laissés dans le code, des explications contenues dans les messages de commits faits sur le module, ou bien en accédant aux comptes de vrais utilisateurs.

Fichier pages.py

Ce fichier contient plusieurs classes de pages. Chaque classe correspond à un type de page que l'on peut rencontrer (par exemple, la page de login, la page des comptes courants, la page des relevés des comptes courant, etc.). Le fichier browser.py associe à chaque adresse une classe de page (avec la déclaration des URL).

Chaque classe de page implémentera des méthodes qui extraieront le type d'informations présents sur la page. Par exemple la classe AccountsPage implémentera une méthode iter_accounts(), mais la classe HistoryPage implémentera une méthode iter_history(account). Ainsi, le browser appellera les méthodes de ces classes.

Le parsing peut être effectué à l'aide de méthodes ordinaires qui parcoureront l'arbre DOM des balises HTML ou qui utiliseront des expressions XPath pour sélectionner des balises précises. Cette façon de faire, appelée "Browser 1" au sein de weboob, est celle qui se ferait avec d'autres frameworks classiques de scraping.

Browser 2

Avec weboob, il existe une autre méthode, appelée "Browser 2", qui se rapprochera plus d'une configuration (quoique puissante) que d'un code traditionnel Python. C'est une méthode plus déclarative, où il suffira de configurer quelques expressions XPath pour décrire où se trouvent les données, en ajoutant éventuellement quelques traitements (appelés des "filtres"), par exemple pour appliquer une expression régulière ou convertir en nombre ou en date.

La méthode "Browser 2" est la méthode recommandée pour écrire de nouveaux modules ou quand il faut réécrire du code. L'un des avantages apporté est une structure au code du parsing qui rend plus simple la lecture de modules différents, en imposant des conventions. Un autre avantage est que le code écrit en "Browser 2" est plus concis car il se concentre sur l'essentiel. Sa syntaxe est un peu déroutante au premier abord, mais on peut le voir comme un DSL (Domain-Specific Language), un langage un peu différent de Python, un langage de configuration dédié au scraping.

Voici un petit exemple de code écrit en "browser 2" :

class AccountsPage(LoggedPage, HTMLPage):
    @method
    class iter_accounts(ListElement):
        item_xpath = '//table/tbody/tr'

        class item(ItemElement):
            klass = Account

            obj_label = CleanText('./td[2]')
            obj_balance = CleanDecimal.French('./td[3]')
            obj_type = MapIn(Field('label'), ACCOUNT_LABEL_TO_TYPE, default=Account.TYPE_UNKNOWN)

Et voilà le code équivalent écrit sans "browser 2", tel qu'on pourrait écrire avec d'autres frameworks que weboob :

class AccountsPage(LoggedPage, HTMLPage):
    def iter_accounts(self):
        for el in self.doc.xpath('//table/tbody/tr'):
            account = Account()

            account.label = el.xpath('./td[2]')[0].text_content()

            balance_str = el.xpath('./td[3]')[0].text_content()
            account.balance = Decimal(re.search('[+-][\d,]+', balance_str).group(0))

            for pattern, type_ in ACCOUNT_LABEL_TO_TYPE.items():
                if pattern in account.label:
                    account.type = type_
                    break

            yield account

On peut voir que le code "browser 2" est plus concis mais fait appel à des fonctions que nous ne connaissons pas encore, nous allons détailler ça.

Weboob fournit beaucoup de facilités pour gérer la pagination aussi, etc.

Extraction d'une liste d'éléments

@method
class iter_accounts(ListElement):
    item_xpath = '//table/tbody/tr'

On peut voir que iter_accounts n'est pas une vraie méthode Python mais une classe, mais le décorateur @method va "transformer" cela en une méthode.

Cette classe iter_accounts hérite de ListElement, ce qui indique que plusieurs éléments seront retournés par la méthode. Quand on utilise ListElement, il faut ajouter un champ item_xpath qui contiendra le XPath de tous les éléments de la page à parcourir, et dans ce cas précis ce sera les comptes. Sans avoir besoin d'écrire une boucle for, ListElement va itérer sur tous les éléments trouvés par le XPath.

Extraction d'un élément

    class item(ItemElement):
        klass = Account

        obj_label = CleanText('./td[1]')
        obj_id = Regexp(CleanText('./td[1]'), r'\((\d+)\)')

        obj_currency = 'EUR'
        obj_balance = CleanDecimal.French('./td[2]')

        def obj_type(self):
            label = CleanText('./td[1]')(self)
            types = {
                'compte courant': Account.TYPE_CHECKING,
            }
            return types.get(label, Account.TYPE_UNKNOWN)

Imbriqué dans notre class iter_accounts, se trouve une classe item, héritant d'ItemElement cette fois. Elle décrira le parsing de chacun des éléments de la liste. On commence par indiquer de quel type seront les objets présents dans la liste, ici klass = Account. Puis, on spécifie comment remplir chaque champ de l'objet en déclarant des attributs nommés obj_<nom du champ du modèle Account> avec un ou plusieurs filtres pour extraire l'information.

Pour chaque élément HTML identifié par le item_xpath du ListElement parent, le contenu du ItemElement sera évalué. À chaque fois, un objet Account() sera créé, et les champs seront remplis.

Filtres

Par exemple, le filtre CleanText prendra tout le texte d'un élément HTML identifié par le XPath donné. Commençant par "./", le XPath est relatif à l'élément de la ligne du compte actuel. Le filtre est évalué dans le contexte de l'ItemElement qui est lui même dans le contexte d'un ListElement, comme s'il était dans une boucle for. Ce "./" est similaire au "./" d'un chemin de fichier sur un UNIX.

obj_label = CleanText('./td[1]')

signifie donc : "pour chaque //table/tbody/tr, créer un objet Account et remplir le champ label par le texte du ./td[1]".

Il est possible de combiner des filtres, par exemple on peut récupérer le texte avec CleanText puis appliquer une expression régulière pour ne sélectionner qu'une partie du texte. Voir le obj_id en exemple.

Constantes

Si la valeur à mettre dans un champ est constante, il n'est pas nécessaire de mettre un filtre mais simplement une constante. Attention aux constantes mutables cependant.

Surcharge d'un filtre

Avec Browser 2, il peut arriver qu'il n'existe aucun filtre pour faire ce que l'on veut, car ce que l'on désire est trop spécifique par exemple. Remplir un champ avec des filtres pourrait s'avérer difficile, mais dans ce cas, il est possible d'écrire du code Python classique uniquement pour le champ problématique, et conserver l'utilisation de filtres pour tous les autres champs.

        def obj_type(self):
            types = {
                'compte courant': Account.TYPE_CHECKING,
            }
            return types.get(label, Account.TYPE_UNKNOWN)

permet d'utiliser une fonction Python ordinaire pour remplir le champ type. C'est un exemple, le filtre Map aurait parfaitement convenu ici.

Ainsi, il est possible de "débrayer" le système de Browser 2 ponctuellement, pour les cas où le système déclaratif de Browser 2 se montre trop limité.

TODO De même, si l'on doit sélectionner des éléments d'une manière que XPath ne permet pas ou rend trop compliqué, il est possible de se rabattre sur du code Python ordinaire uniquement sur la sélection d'éléments.

Gérer le login

Pour naviguer sur de nombreuses pages, comme la page des comptes par exemple, il faudra être authentifié auprès du site. Côté browser, on annotera les méthodes comme iter_accounts avec le décorateur @need_login pour indiquer qu'il faudra être authentifié avant d'entrer dans la fonction. Si l'on n'est pas encore authentifié, la méthode do_login sera appelée. Il nous faudra donc également implémenter cette méthode.

Côté browser.py :

def do_login(self):
    self.login.go()
    self.page.do_login(self.username, self.password)
    # self.username et self.password existent toujours dans LoginBrowser

    # ici, vérifier si on est bien authentifié ou s'il y a un message d'erreur

@need_login
def iter_accounts(self):
    self.accounts.go()
    return self.page.iter_accounts()

Côté pages.py, nous devrons implémenter le traitement du login sur la page elle-même :

class LoginPage(HTMLPage):
    def do_login(self, username, password):
        form = self.get_form()
        form['login'] = username
        form['password'] = password
        form.submit()

Il y avait un simple formulaire sur la page, nous remplissons les champs (presque) comme un utilisateur et soumettons le formulaire. Si le mot de passe est incorrect, certains sites nous remettront sur la page de login avec un code de retour 200 et un message d'erreur.

Il faudrait vérifier à la fin de do_login si tout a fonctionné ou s'il y a une erreur. Il faut faire cette vérification côté browser, car on a peut-être changé de page, or l'instance de LoginPage représentera l'état inchangé de l'ancienne page.

Commencer à écrire un module

Créer une coquille vide

./tools/boilerplate/boilerplate.py cap MODULE_NAME CapBank

Cela créera un dossier nommé d'après le module dans ~/dev/weboob/modules avec les fichiers de base. Lancer weboob-config update pour que weboob l'ajoute à sa liste.

Tester son module

Après avoir ajouté de vraies requêtes dans le module, on peut commencer à le tester.

Ajouter dans ~/.config/weboob/backends :

[NOM_BACKEND]
_module = NOM_MODULE
login = LE_LOGIN
password = LE_MOT_DE_PASSE

Puis lancer boobank -b NOM_BACKEND.

Clone repository
  • bank model
  • boobathon 201804
  • boobathon 201911
  • cli tools shell
  • cli tools
  • config files
  • draft maintainer setup
  • faq
  • fr
    • guide scraping
    • guide weboob
  • Home
  • installing weboob on synology (debian chroot method)
  • installing weboob on synology (opkg method)
  • lecture 2010 07 09
  • lecture 2012 12 29
More Pages