8.4. Présentation de BaseHTMLProcessor.py

SGMLParser ne produit rien de lui même. Il ne fait qu’analyser et appeler une méthode pour chaque élément intéressant qu’il trouve, mais les méthodes ne font rien. SGMLParser est un consommateur de HTML : il prend du code HTML et le décompose en petits éléments structurés. Comme nous l’avons vu dans la section précédente, on peut dériver SGMLParser pour définir une classe qui trouve des balises spécifiques et produit quelque chose d’utile, comme une liste de tous les liens d’une page web. Nous allons maintenant aller un peu plus loin en définissant une classe qui prends tout ce que SGMLParser lui envoi et reconstruit entièrement le document HTML. En termes techniques, cette classe sera un producteur de HTML.

BaseHTMLProcessor est dérivé de SGMLParser et fournit les 8 méthodes de prise en charge essentielles : unknown_starttag, unknown_endtag, handle_charref, handle_entityref, handle_comment, handle_pi, handle_decl et handle_data.

Exemple 8.8. Présentation de BaseHTMLProcessor


class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        1
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): 2
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          3
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):          4
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        5
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):            6
        self.pieces.append(text)

    def handle_comment(self, text):         7
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):              8
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals())
1 reset est appelé par SGMLParser.__init__ initialise self.pieces en une liste vide avant d’appeler la méthode ancêtre. self.pieces est une donnée attribut qui contient les éléments du document HTML que nous assemblons. Chaque méthode de prise en charge va reconstruire le code HTML que SGMLParser a analysé et chaque méthode ajoutera la chaîne résultante à self.pieces. Notez que self.pieces est une liste. Vous pouvez être tenté de la définir comme une chaîne et de lui ajouter simplement chaque élément. Cela fonctionnerait mais Python gère les listes de manière bien plus efficiente.[4]
2 Comme BaseHTMLProcessor ne définit aucune méthode pour des balises spécifiques (comme la méthode start_a de urllister.py), SGMLParser appelera unknown_starttag pour chaque balise de début. Cette méthode prend en paramètre la balise (tag) et la liste des paires nom/valeurs de ses attributs (attrs), reconstruit le code HTML originel et l’ajoute à self.pieces. Le formatage de chaîne ici est un peu étrange, nous l’expliquerons (ainsi que la fonction locals à l'air étrange) dans la prochaine section.
3 Reconstruire les balises de fin est beaucoup plus simple, il suffit de prendre le nom de la balise est de l’encadrer de </...>.
4 Lorsque SGMLParser trouve une référence de caractère, il appelle handle_charref avec la référence. Si le document HTML contient la référence &#160;, ref vaudra 160. La reconstruction de la référence de caractère originelle ne demande que d’encadrer ref par &#...;.
5 Les références d’entité sont semblables aux références de caractères, mais sans le signe dièse. La reconstruction de la référence d’entité originelle demande d’encadrer ref par &...;. (En fait, comme un lecteur savant me l’a fait remarquer, c’est un peu plus compliqué que ça. Seulement certaines entités standard du HTML finissent par un point-virgule. Heureusement pour nous, l’ensemble des entités standards est défini dans un dictionnaire dans un module Python appelé htmlentitydefs. C’est l’explication de l’instruction if supplémentaire.)
6 Les blocs de texte sont simplement ajouté à self.pieces sans modification.
7 Les commentaires HTML sont encadrés par <!--...-->.
8 Les instructions de traitement sont encadrés par <?...>.
Important
La spécification HTML exige que tous les éléments non-HTML (comme le JavaScript côté client) soient compris dans des commentaires HTML, mais toutes les pages web ne le font pas (et les navigateurs web récents ne l’exigent pas). BaseHTMLProcessor, lui, l’exige, si le script n’est correctement encadré dans un commentaire, il sera analysé comme s’il était du code HTML. Par exemple, si le script contient des signes inférieurs à ou égal, SGMLParser peut considérer à tort qu’il a trouvé des balises et des attributs. SGMLParser convertit toujours les noms de balises et d’attributs en minuscules, ce qui peut empêcher la bonne exécution du script et BaseHTMLProcessor entoure toujours les valeurs d’attributs entre guillemets (même si le document HTML n’en utilisait pas ou utilisait des guillemets simples), ce qui empêchera certainement l’exécution du script. Protégez toujours vos script côté client par des commentaires HTML.

Exemple 8.9. Sortie de BaseHTMLProcessor

    def output(self):               1
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 2
1 Voici l’unique méthode de BaseHTMLProcessor qui n’est jamais appelée par son ancêtre, SGMLParser. Comme les méthodes de prise en charge stockent le HTML reconstitué dans self.pieces, cette fonction est nécessaire pour assembler toutes ces pièces en une chaîne unique. Comme noté précédemment, Python est bon pour gérer les listes et moyens pour les chaînes, nous ne créons donc la chaîne seulement quand un utilisateur la réclame explicitement.
2 Si vous préférez, vous pouvez plutôt utiliser la méthode join du module string : string.join(self.pieces, "")

Pour en savoir plus

Footnotes

[4] La raison pour laquelle Python gère mieux les listes que les chaînes est que les listes sont modifiables et que les chaînes sont non-modifiables. Cela signifie qu’ajouter à une liste ne fait qu’ajouter l’élément et mettre à jour l’index. Mais comme les chaînes ne peuvent pas être changées après avoir été créées, du code tel que s = s + newpiece créera une nouvelle chaîne à partir de la concaténation de l’original et du nouvel élément, puis jettera la chaîne originelle. Cela implique une gestion mémoire coûteuse et le coût augmente avec la taille de la chaîne, donc faire s = s + newpiece à l’intérieur d’une boucle est fatal. En termes techniques, ajouter n éléments à une liste est O(n), alors qu’ajouter n éléments à une chaîne items est O(n2).