8.8. Présentation de dialect.py

Dialectizer est un descendant simple (et humoristique) de BaseHTMLProcessor. Il procède à une série de substitutions dans un bloc de text, mais il s’assure que tout ce qui est contenu dans un bloc <pre>...</pre> passe sans altération.

Pour traiter les blocs <pre>, nous définissons deux méthodes dans Dialectizer: start_pre et end_pre.

Exemple 8.17. Traitement de balises spécifiques

    def start_pre(self, attrs):             1
        self.verbatim += 1                  2
        self.unknown_starttag("pre", attrs) 3

    def end_pre(self):                      4
        self.unknown_endtag("pre")          5
        self.verbatim -= 1                  6
1 start_pre est appelé à chaque fois que SGMLParser trouve une balise <pre> dans le source HTML. (Nous verrons bientôt comment cela se fait.) La méthode prend un paramètre unique, attrs, qui contient les attributs de la balise (s’il y en a). attrs est une liste de tuples clé/valeur, exactement le paramètre que prend unknown_starttag.
2 Dans la méthode reset, nous initialisons un attribut qui sert de compteur de balises <pre>. Chaque fois que nous rencontrons une balise <pre>, nous incrémentons le compteur ; chaque fois que nous rencontrons une balise fermante </pre>, nous décrémentons le compteur. (Nous pourrions l’utiliser simplement comme un drapeau en le mettant à 1 puis à 0, mais c’est aussi facile de cette manière et permet de traiter le cas improbable (mais possible) de balises <pre> imbriquées.) Dans une minute, nous verrons comment ce compteur est mis à profit.
3 C’est tout, c’est le seul traitement particulier que nous faisons pour la balise <pre>. Maintenant, nous passons la liste des attributs à unknown_starttag pour qu’il puisse faire le traitement par défaut.
4 end_pre est appelé chaque fois que SGMLParser trouve une balise fermante </pre>. Comme les balises fermantes ne peuvent pas contenir d’attributs, la méthode ne prend pas de paramètre.
5 D’abord nous voulons effectuer le traitement par défaut, comme pour toute balise fermante.
6 Ensuite, nous décrémentons notre compteur pour signaler que ce bloc <pre> a été fermé.

Arrivé à ce point, il est temps d’examiner plus en détail SGMLParser. J’ai prétendu jusqu’à maintenant (et vous avez dû me croire sur parole) que SGMLParser cherche et appelle des méthodes spécifiques pour chaque balise, si elles existent. Par exemple, nous venons juste de voir la définition de start_pre et end_pre pour traiter <pre> et </pre>. Mais comment est-ce que cela se produit ? Et bien, ce n’est pas de la magie, simplement de la bonne programmation Python.

Exemple 8.18. SGMLParser

    def finish_starttag(self, tag, attrs):               1
        try:                                            
            method = getattr(self, 'start_' + tag)       2
        except AttributeError:                           3
            try:                                        
                method = getattr(self, 'do_' + tag)      4
            except AttributeError:                      
                self.unknown_starttag(tag, attrs)        5
                return -1                               
            else:                                       
                self.handle_starttag(tag, method, attrs) 6
                return 0                                
        else:                                           
            self.stack.append(tag)                      
            self.handle_starttag(tag, method, attrs)    
            return 1                                     7

    def handle_starttag(self, tag, method, attrs):      
        method(attrs)                                    8
1 A ce niveau, SGMLParser a déjà trouvé une balise ouvrante et lu la liste d’attributs. La seule chose restant à faire est de trouver s’il existe un méthode spécifique pour cette balise ou si il faut la traiter avec la méthode par défaut (unknown_starttag).
2 La «magie» de SGMLParser n’est rien de plus que notre vieille connaissance getattr. Ce que vous n’aviez peut-être pas réalisé auparavant, c’est que getattr peut trouver des méthodes définies dans les descendants d’un objet aussi bien que dans l’objet lui-même. Ici, l’objet est self, l’instance de la méthode qui l’appelle. Donc si tag est 'pre', cet appel à getattr cherchera une méthode start_pre dans l’instance, qui est une instance de la classe Dialectizer.
3 getattr déclenche une exception AttributeError si la méthode qu’il cherche n’existe pas dans l’objet (ni dans aucun de ses descendants), mais cela n’est pas un problème puisque nous avons encadré l’appel à getattr dans un bloc try...except et explicitement intercepté l’exception AttributeError.
4 Comme nous n’avons pas trouvé de méthode start_xxx, nous cherchons également une méthode do_xxx avant d’abandonner. Cette désignation alternative est généralement utilisée pour les balises isolées, telles que <br>, qui n’ont pas de balise fermante correspondante. Vous pouvez utiliser l’une comme l’autre, comme vous le voyez SGMLParser essaie les deux pour chaque balise (vous ne devez pas définir à la fois une méthode start_xxx et une méthode do_xxx pour la même balise, seule la méthode start_xxx serait appelée).
5 Encore une exception AttributeError, ce qui veut dire que l’appel à getattr a échoué pour do_xxx. Comme nous n’avons trouvé ni un méthode start_xxx ni une méthode do_xxx pour cette balise, nous interceptons l’exception et nous rabattons sur la méthode par défaut, unknown_starttag.
6 Rappelez-vous que les blocs try...except peuvent avoir une clause else, qui est appelée si aucune exception n’est déclenchée au cours du bloc try...except. Logiquement, cela signifie que nous avons trouvé une méthode do_xxx pour la balise, donc nous allons l’appeler.
7 Au fait, ne vous inquiétez pas pour ces valeurs de retour différentes. En théorie elles signifient quelque chose mais elles ne sont jamais réellement utilisées. Ne vous inquiétez pas non plus de self.stack.append(tag), SGMLParser vérifie trace en interne si vos balises ouvrantes correspondent à des balises fermantes, mais il ne fait rien non plus de cette information. En théorie, vous pourriez utiliser ce module pour vérifier que vos balises sont équilibrée, mais cela n’en vaut sans doute pas la peine et cela dépasse le cadre de ce chapitre. Nous avons des choses bien plus importantes auxquelles penser maintenant.
8 Les méthodes start_xxx et do_xxx ne sont pas appelées directement. La balise, la méthode et les attributs sont passés à cette fonction, handle_starttag, de manière à ce que des classes dérivées puissent la redéfinir pour changer la manière dont toutes les balises ouvrantes sont traitées. Nous n’avons pas besoin d’un tel niveau de contrôle, donc nous laissons simplement cette méthode faire ce qu’elle doit faire, c’est à dire appeler la méthode (start_xxx ou do_xxx) avec la liste des attributs. Rappelez-vous que method est une fonction, retournée par getattr et que les fonctions sont des objets (je sais que vous en avez assez de l’entendre et je promets d’arrêter de le dire dès que nous aurons cessé de trouver de nouvelles manières de l’utiliser à notre avantage). Ici, l’objet fonction est passé à cette méthode d’appel en argument et cette méthode appelle la fonction. Arrivés là, nous n’avons pas à savoir quelle est la fonction, quel est son nom ni l’endroit où elle a été définie. La seule chose que nous devons savoir est qu’elle est appelée avec un argument, attrs.

Revenons à nos moutons : Dialectizer. Nous l’avons laissé au moment de définir des méthodes spéciales pour le traitement des balises <pre> et </pre>. Il n’y a plus qu’une chose à faire et c’est de traiter les blocs de texte avec nos substitutions prédéfinies. Pour cela nous devons redéfinir la méthode handle_data.

Exemple 8.19. Redéfinition de la méthode handle_data

    def handle_data(self, text):                                         1
        self.pieces.append(self.verbatim and text or self.process(text)) 2
1 handle_data est appelée avec un seul argument, le texte à traiter.
2 Dans la classe parente BaseHTMLProcessor, la méthode handle_data ne fait qu’ajouter le texte au tampon de sortie, self.pieces. Ici, la logique n’est qu’un petit peu plus compliquée. Si nous sommes au milieu d’un bloc <pre>...</pre>, self.verbatim sera une valeur quelconque supérieure à 0, nous voulons alors ajouter le texte au tampon de sortie sans modification. Sinon, nous appellerons une méthode spécifique pour appliquer les substitutions, puis ajouterons le résultat de ce traitement dans le tampon de sortie. En Python, cela se fait en une ligne, en utilisant l’astuce and-or.

Nous sommes près de comprendre complètement Dialectizer. Le seul chaînon manquant concerne la nature des substitutions de texte elle-mêmes. Si vous connaissez un peu de Perl, vous savez que lorsque des substitutions de texte complexes sont nécessaires, la seule vrai solution est d’utiliser les expressions régulières. Les classes suivantes de dialect.py définissent une série d'expressions régulières qui opèrent sur le texte entre les balises HTML. Mais nous venons d'avoir un chapitre entier sur les expressions régulières. Je pense que nous en avons assez appris pour un chapitre.