Blog of :/blog/weboob/Browser2_:_Parsing_descriptif_du_contenu_des_pages.html

Browser2 : Parsing descriptif du contenu des pages

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