IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Plongez au coeur de Python ,De débutant à expert


précédentsommairesuivant

VIII. Traitement du HTML

VIII-A. Plonger

Je vois souvent sur comp.lang.python des questions comme «Comment faire une liste de tous les [en-têtes|images|liens] de mon document HTML ?» «Comment faire pour [parser|traduire|transformer] le texte d'un document HTML sans toucher aux balises ?» «Comment faire pour [ajouter|enlever|mettre entre guillemets] des attributs de mes balises HTML d'un coup ?» Ce chapitre répondra à toutes ces questions.

Voici un programme Python complet et fonctionnel en deux parties. La première partie, BaseHTMLProcessor.py, est un outil générique destiné à vous aider à traiter des fichiers HTML en parcourant les balises et les blocs de texte. La deuxième partie, dialect.py, est un exemple montrant comment utiliser BaseHTMLProcessor.py pour traduire le texte d'un document HTML sans toucher aux balises. Lisez les doc strings et les commentaires pour avoir une vue d'ensemble de ce qui se passe. Une grande partie va avoir l'air magique parce qu'il n'est pas évident de voir comment ces méthodes de classes sont appelées. Ne vous inquiétez pas, tout vous sera bientôt expliqué.

Exemple 8.1. BaseHTMLProcessor.py

Si vous ne l'avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.

 
Sélectionnez
from sgmllib import SGMLParser
import htmlentitydefs

class BaseHTMLProcessor(SGMLParser):
    def reset(self):                       
        # extend (called by SGMLParser.__init__)
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs):
        # called for each start tag
        # attrs is a list of (attr, value) tuples
        # e.g. for <pre class="screen">, tag="pre", attrs=[("class", "screen")]
        # Ideally we would like to reconstruct original tag and attributes, but
        # we may end up quoting attribute values that weren't quoted in the source
        # document, or we may change the type of quotes around the attribute value
        # (single to double quotes).
        # Note that improperly embedded non-HTML code (like client-side Javascript)
        # may be parsed incorrectly by the ancestor, causing runtime script errors.
        # All non-HTML code must be enclosed in HTML comment tags (<!-- code -->)
        # to ensure that it will pass through this parser unaltered (in handle_comment).
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("< %(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):         
        # called for each end tag, e.g. for </pre>, tag will be "pre"
        # Reconstruct the original end tag.
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):         
        # called for each character reference, e.g. for " ", ref will be "160"
        # Reconstruct the original character reference.
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):       
        # called for each entity reference, e.g. for "&copy;", ref will be "copy"
        # Reconstruct the original entity reference.
        self.pieces.append("&%(ref)s" % locals())
        # standard HTML entities are closed with a semicolon; other entities are not
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):           
        # called for each block of plain text, i.e. outside of any tag and
        # not containing any character or entity references
        # Store the original text verbatim.
        self.pieces.append(text)

    def handle_comment(self, text):        
        # called for each HTML comment, e.g. <!-- insert Javascript code here -->
        # Reconstruct the original comment.
        # It is especially important that the source document enclose client-side
        # code (like Javascript) within comments so it can pass through this
        # processor undisturbed; see comments in unknown_starttag for details.
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):             
        # called for each processing instruction, e.g. < ?instruction>
        # Reconstruct original processing instruction.
        self.pieces.append("< ?%(text)s>" % locals())

    def handle_decl(self, text):
        # called for the DOCTYPE, if present, e.g.
        # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        #     "http://www.w3.org/TR/html4/loose.dtd">
        # Reconstruct original DOCTYPE
        self.pieces.append("<!%(text)s>" % locals())

    def output(self):              
        """Return processed HTML as a single string"""
        return "".join(self.pieces)

Exemple 8.2. dialect.py

 
Sélectionnez
import re
from BaseHTMLProcessor import BaseHTMLProcessor

class Dialectizer(BaseHTMLProcessor):
    subs = ()

    def reset(self):
        # extend (called from __init__ in ancestor)
        # Reset all data attributes
        self.verbatim = 0
        BaseHTMLProcessor.reset(self)

    def start_pre(self, attrs):            
        # called for every <pre> tag in HTML source
        # Increment verbatim mode count, then handle tag like normal
        self.verbatim += 1                 
        self.unknown_starttag("pre", attrs)

    def end_pre(self):                     
        # called for every </pre> tag in HTML source
        # Decrement verbatim mode count
        self.unknown_endtag("pre")         
        self.verbatim -= 1                 

    def handle_data(self, text):                                        
        # override
        # called for every block of text in HTML source
        # If in verbatim mode, save text unaltered;
        # otherwise process the text with a series of substitutions
        self.pieces.append(self.verbatim and text or self.process(text))

    def process(self, text):
        # called from handle_data
        # Process text block by performing series of regular expression
        # substitutions (actual substitions are defined in descendant)
        for fromPattern, toPattern in self.subs:
            text = re.sub(fromPattern, toPattern, text)
        return text

class ChefDialectizer(Dialectizer):
    """convert HTML to Swedish Chef-speak
    
    based on the classic chef.x, copyright (c) 1992, 1993 John Hagerman
    """
    subs = ((r'a([nu])', r'u\1'),
            (r'A([nu])', r'U\1'),
            (r'a\B', r'e'),
            (r'A\B', r'E'),
            (r'en\b', r'ee'),
            (r'\Bew', r'oo'),
            (r'\Be\b', r'e-a'),
            (r'\be', r'i'),
            (r'\bE', r'I'),
            (r'\Bf', r'ff'),
            (r'\Bir', r'ur'),
            (r'(\w*?)i(\w*?)$', r'\1ee\2'),
            (r'\bow', r'oo'),
            (r'\bo', r'oo'),
            (r'\bO', r'Oo'),
            (r'the', r'zee'),
            (r'The', r'Zee'),
            (r'th\b', r't'),
            (r'\Btion', r'shun'),
            (r'\Bu', r'oo'),
            (r'\BU', r'Oo'),
            (r'v', r'f'),
            (r'V', r'F'),
            (r'w', r'w'),
            (r'W', r'W'),
            (r'([a-z])[.]', r'\1.  Bork Bork Bork!'))

class FuddDialectizer(Dialectizer):
    """convert HTML to Elmer Fudd-speak"""
    subs = ((r'[rl]', r'w'),
            (r'qu', r'qw'),
            (r'th\b', r'f'),
            (r'th', r'd'),
            (r'n[.]', r'n, uh-hah-hah-hah.'))

class OldeDialectizer(Dialectizer):
    """convert HTML to mock Middle English"""
    subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
            (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
            (r'ick\b', r'yk'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
            (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
            (r'([aeiou])re\b', r'\1r'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
            (r'tion\b', r'cioun'),
            (r'ion\b', r'ioun'),
            (r'aid', r'ayde'),
            (r'ai', r'ey'),
            (r'ay\b', r'y'),
            (r'ay', r'ey'),
            (r'ant', r'aunt'),
            (r'ea', r'ee'),
            (r'oa', r'oo'),
            (r'ue', r'e'),
            (r'oe', r'o'),
            (r'ou', r'ow'),
            (r'ow', r'ou'),
            (r'\bhe', r'hi'),
            (r've\b', r'veth'),
            (r'se\b', r'e'),
            (r"'s\b", r'es'),
            (r'ic\b', r'ick'),
            (r'ics\b', r'icc'),
            (r'ical\b', r'ick'),
            (r'tle\b', r'til'),
            (r'll\b', r'l'),
            (r'ould\b', r'olde'),
            (r'own\b', r'oune'),
            (r'un\b', r'onne'),
            (r'rry\b', r'rye'),
            (r'est\b', r'este'),
            (r'pt\b', r'pte'),
            (r'th\b', r'the'),
            (r'ch\b', r'che'),
            (r'ss\b', r'sse'),
            (r'([wybdp])\b', r'\1e'),
            (r'([rnt])\b', r'\1\1e'),
            (r'from', r'fro'),
            (r'when', r'whan'))

def translate(url, dialectName="chef"):
    """fetch URL and translate using dialect
    
    dialect in ("chef", "fudd", "olde")"""
    import urllib                      
    sock = urllib.urlopen(url)         
    htmlSource = sock.read()           
    sock.close()                       
    parserName = "%sDialectizer" % dialectName.capitalize()
    parserClass = globals()[parserName]                    
    parser = parserClass()                                 
    parser.feed(htmlSource)
    parser.close()         
    return parser.output() 

def test(url):
    """test all dialects against URL"""
    for dialect in ("chef", "fudd", "olde"):
        outfile = "%s.html" % dialect
        fsock = open(outfile, "wb")
        fsock.write(translate(url, dialect))
        fsock.close()
        import webbrowser
        webbrowser.open_new(outfile)

if __name__ == "__main__":
    test("http://diveintopython.org/odbchelper_list.html")

Exemple 8.3. Sortie de dialect.py

Ce script effectue la traduction de la Section 3.2, «Présentation des listes» en pseudo-Chef Suédois (des Muppets), pseudo-Elmer Fudd (de Bugs Bunny) et en pseudo-ancien Anglais (librement adapté de The Canterbury Tales de Chaucer). Si vous regardez le source HTML de la sortie, vous verrez que toutes les balises HTML et les attributs sont intacts mais que le texte entre les balises a été «traduit» dans le pseudo-langage. Si vous regardez plus attentivement, vous verrez qu'en fait, seuls les titres et les paragraphes ont été traduits, les listing de code et les exemples d'écrans ont été laissé intacts.

 
Sélectionnez
<div class="abstract">
<p>Lists awe <span class="application">Pydon</span>'s wowkhowse datatype.
If youw onwy expewience wif wists is awways in
<span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe
in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow
<span class="application">Pydon</span> wists.</p>
</div>

VIII-B. Présentation de sgmllib.py

Le traitement du HTML est divisé en trois étapes : diviser le HTML en éléments, modifier les éléments et reconstruire le HTML à partir des éléments. La première étape est réalisée par sgmllib.py, qui fait partie de la bibliothèque standard de Python.

La clé de la compréhension de ce chapitre est de réaliser que le HTML n'est pas seulement du texte, c'est du texte structuré. La structure est dérivée de la séquence plus ou moins hiérarchique de balises de début et de fin. Habituellement, on ne travaille pas de cette manière sur du HTML, on travaille textuellement dans un éditeur de texte ou visuellement dans un navigateur ou un éditeur de pages web. sgmllib.py présente le HTML de manière structurelle.

sgmllib.py contient une classe principale : SGMLParser. SGMLParser analyse le HTML et le décompose en éléments utiles, comme des balises de début et de fin. Dès qu'il parvient à extraire des données un élément utile, il appelle une de ses propres méthodes en fonction de l'élément trouvé. Pour utiliser l'analyseur, on dérive une classe de SGMLParser et on redéfinit ces méthodes. C'est ce que j'entendais par présentation du HTML de manière structurelle : la structure du code HTML détermine la séquence d'appels de méthodes et les arguments passés à chaque méthode.

SGMLParser décompose le HTML en 8 sortes de données et appelle une méthode différente pour chacune d'entre elles :

  • Balise de début

    Une balise HTML qui ouvre un bloc, comme <html>, <head>, <body> ou <pre>, ou une balise autonome comme <br> ou <img>. Lorsqu'il trouve une balise de début tagname, SGMLParser cherche une méthode nommée start_tagname ou do_tagname. Par exemple, lorsqu'il trouve une balise <pre>, il cherche une méthode nommée start_pre ou do_pre. S'il la trouve, SGMLParser l'appelle avec une liste des attributs de la balise en paramètre, sinon il appelle unknown_starttag avec le nom de la balise et la liste de ses attributs en paramètre.

  • Balise de fin

    Une balise HTML qui ferme un bloc, comme </html>, </head>, </body> ou </pre>. Lorsqu'il trouve une balise de fin, SGMLParser cherche une méthode nommée end_tagname. S'il la trouve, SGMLParser appelle cette méthode, sinon il appelle unknown_endtag avec le nom de la balise.

  • Référence de caractère

    Un caractère référencé par son équivalent décimal ou hexadécimal, comme &#160;. Lorsqu'il en trouve une, SGMLParser appelle handle_charref avec le texte de l'équivalent décimal ou hexadécimal.

  • Référence d'entité

    Une entité HTML, comme &copy;. Lorsqu'il en trouve une, SGMLParser appelle handle_entityref avec le nom de l'entité HTML.

  • Commentaire

    Un commentaire HTML, encadré par <!-- ... -->. Lorsqu'il en trouve un, SGMLParser appelle handle_comment avec le corps du commentaire.

  • Instruction de traitement

    Une instruction de traitement HTML, encadrée par < ? ... >. Lorsqu'il en trouve une, SGMLParser appelle handle_pi avec le corps de l'instruction.

  • Déclaration

    Une déclaration HTML, comme un DOCTYPE, encadrée par <! ... >. Lorsqu'il en trouve une, SGMLParser appelle handle_decl avec le corps de la déclaration

  • Données texte

    Un bloc de texte. Tout ce qui n'entre dans aucune des 7 catégories précédentes. Lorsqu'il en trouve un, SGMLParser appelle handle_data avec le texte.

Python 2.0 avait un bogue qui empêchait SGMLParser de reconnaître les déclarations (handle_decl n'était jamais appelé), ce qui veut dire que les DOCTYPEs étaient ignorés silencieusement. Ce bogue est corrigé dans Python 2.1.

sgmllib.py est accompagné d'une suite de tests pour illustrer cela. Si on exécute sgmllib.py en lui passant le nom d'un fichier HTML en argument de ligne de commande, il affichera les balises et les autres éléments au fur et à mesure qu'il analyse le fichier. Il fait cela en dérivant une classe de SGMLParser et en définissant des méthodes comme unknown_starttag, unknown_endtag, handle_data et autres qui ne font qu'afficher leur argument.

Dans l'IDE ActivePython sous Windows, vous pouvez spécifier des arguments de ligne de commande dans la boîte de dialogue «Run script». Séparez les différents arguments par des espaces.

Exemple 8.4. Exemple de test de sgmllib.py

Voici un extrait de la table des matières de la version HTML de ce livre. Bien sûr, les chemins peuvent être différents. Si vous n'avez pas téléchargé la version HTML du livre, vous pouvez le faire à l'adresse suivante : http://diveintopython.org/.

 
Sélectionnez
c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html"

<!DOCTYPE html
  PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
   
      <title>Dive Into Python</title>
      <link rel="stylesheet" href="diveintopython.css" type="text/css">

... nous coupons la suite pour rester bref ...

En l'utilisant avec la suite de tests de sgmllib.py, on obtient la sortie suivante :

 
Sélectionnez
c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html"
data: '\n\n'
start tag: <html lang="en" >
data: '\n   '
start tag: <head>
data: '\n      '
start tag: <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" >
data: '\n   \n      '
start tag: <title>
data: 'Dive Into Python'
end tag: </title>
data: '\n      '
start tag: <link rel="stylesheet" href="diveintopython.css" type="text/css" >
data: '\n      '

... nous coupons la suite pour rester bref ...

Voici le plan du reste de ce chapitre :

  • Dérivation de SGMLParser pour créer des classes qui extraient des données intéressantes de documents HTML.
  • Dérivation de SGMLParser pour créer BaseHTMLProcessor, qui redéfinit les 8 méthodes de gestion et les utilise pour reconstruire le code HTML à partir de ses éléments.
  • Dérivation de BaseHTMLProcessor pour créer Dialectizer, qui ajoute des méthodes traitant des balises HTML spécifiques et redéfinit la méthode handle_data pour fournir une structure de traitement de blocs de tests entre les balises HTML.
  • Dérivation de Dialectizer pour créer des classes qui définissent des règles de traitement du texte utilisées par Dialectizer.handle_data.
  • Ecriture d'une suite de tests qui récupère une véritable page web de http://diveintopython.org/ et en traite le contenu.

En cours de route, vous apprendrez également locals, globals et le formatage de chaînes à l'aide de dictionnaire.

VIII-C. Extraction de données de documents HTML

Pour extraire des données de documents HTML, on dérive une classe de SGMLParser et on définit des méthodes pour chaque balise ou entité que l'on souhaite traiter.

La première étape pour extraire des données d'un document HTML est d'obtenir le HTML. Si vous avez un fichier HTML, vous pouvez le lire à l'aide des fonctions de fichier, mais le plus intéressant est d'obtenir le HTML depuis des sites web.

Exemple 8.5. Présentation de urllib

 
Sélectionnez
>>> import urllib                                       ***1***
>>> sock = urllib.urlopen("http://diveintopython.org/") ***2***
>>> htmlSource = sock.read()                            ***3***
>>> sock.close()                                        ***4***
>>> print htmlSource                                    ***5***
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head>
      <meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'>
   <title>Dive Into Python</title>
<link rel='stylesheet' href='diveintopython.css' type='text/css'>
<link rev='made' href='mailto:mark@diveintopython.org'>
<meta name='keywords' content='Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free'>
<meta name='description' content='a free Python tutorial for experienced programmers'>
</head>
<body bgcolor='white' text='black' link='#0000FF' vlink='#840084' alink='#0000FF'>
<table cellpadding='0' cellspacing='0' border='0' width='100%'>
<tr><td class='header' width='1%' valign='top'>diveintopython.org</td>
<td width='99%' align='right'><hr size='1' noshade></td></tr>
<tr><td class='tagline' colspan='2'>Python&nbsp;for&nbsp;experienced&nbsp;programmers</td></tr>

[...coupé...]

***1*** Le module urllib fait partie de la bibliothèque standard de Python. Il comprend des fonctions permettant d'obtenir des information et des données à partir d'URLs (principalement des pages web).

***2*** L'usage le plus simple de urllib est de lire le texte complet d'une page web à l'aide de la fonction urlopen. L'ouverture d'une URL est semblable à l'ouverture d'un fichier. La valeur de retour de urlopen est un objet semblable à un objet-fichier et dont certaines méthodes sont les mêmes que celles d'un objet-fichier.

***3*** La chose la plus simple à faire avec l'objet retourné par urlopen est d'appeler read, qui lit l'ensemble du code HTML de la page web en une chaîne unique. L'objet permet également l'emploi de readlines, qui lit le code ligne par ligne et le stocke dans une liste.

***4*** Quand vous n'en avez plus besoin, assurez vous de fermer l'objet par un close, comme pour un objet fichier.

***5*** Nous avons maintenant l'ensemble du code HTML de la page d'accueil de http://diveintopython.org/ dans une chaîne et nous sommes prêts à l'analyser.

Exemple 8.6. Présentation de urllister.py

Si vous ne l'avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.

 
Sélectionnez
from sgmllib import SGMLParser

class URLLister(SGMLParser):
    def reset(self):                              ***1***
        SGMLParser.reset(self)
        self.urls = []

    def start_a(self, attrs):                     ***2***
        href = [v for k, v in attrs if k=='href'] ***3*** ***4***
        if href:
            self.urls.extend(href)

***1*** reset est appelé par la méthode __init__ de SGMLParser et peut également être appelé manuellement quand une instance de l'analyseur a été créée. Donc, si vous avez une quelconque initialisation à faire, faites la dans reset et pas dans __init__, de manière à ce que la réinitialisation se fasse correctement lorsque quelqu'un réutilise une instance de l'analyseur.

***2*** start_a est appelé par SGMLParser à chaque fois qu'il trouve une balise <a>. La balise peut contenir un attribut href et/ou d'autres attributs comme name ou title. Le paramètre attrs est une liste de tuples, [(attribut, valeur), (attribut, valeur), ...]. La balise peut aussi être un simple <a>, ce qui est une balise HTML valide (bien qu'inutile), dans ce cas attrs sera une liste vide.

***3*** Nous pouvons savoir si la balise <a> a un attribut href à l'aide d'une simple mutation de liste multi-variable.

***4*** Les comparaisons de chaînes comme k=='href' sont toujours sensibles à la casse, mais ça ne pose pas de problème ici car SGMLParser convertit les noms d'attributs en minuscules lors de la création de attrs.

VIII-C-3. Exemple 8.7. Utilisation de urllister.py

 
Sélectionnez
>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read())         ***1***
>>> usock.close()                     ***2***
>>> parser.close()                    ***3***
>>> for url in parser.urls: print url ***4***
toc/index.html
#download
#languages
toc/index.html
appendix/history.html
download/diveintopython-html-5.0.zip
download/diveintopython-pdf-5.0.zip
download/diveintopython-word-5.0.zip
download/diveintopython-text-5.0.zip
download/diveintopython-html-flat-5.0.zip
download/diveintopython-xml-5.0.zip
download/diveintopython-common-5.0.zip


... nous coupons la suite pour rester bref ...

***1*** Appelez la méthode feed, définie dans SGMLParser, pour charger le code HTML dans l'analyseur.(3) La méthode prend une chaîne en argument, ce qui est ce que usock.read() retourne.

***2*** Comme pour les fichiers, vous devez fermer vos objets URL par close dès que vous n'en avez plus besoin.

***3*** Vous devez également fermer l'objet analyseur par close, mais pour une raison différente. La méthode feed ne garantit pas qu'elle traite tout le code HTML que vous lui passez, elle peut la garder dans un tampon en attendant que vous lui en passiez plus. Quand il n'y en a plus, appelez close pour vider le tampon et forcer l'analyse de tout le code.

***4*** Une fois le parser fermé par close, l'analyse est complète et parser.urls contient une liste de toutes les URLs pour lesquelles il y a un lien dans le document HTML.

VIII-D. 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

 
Sélectionnez
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  , 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 < ?...>.

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
 
Sélectionnez
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

VIII-E. locals et globals

Laissons de coté le traitement du HTML une minute pour parler de la manière dont Python gère les variables. Python a deux fonctions prédéfinies permettant d'accéder aux variables locales et globales sous forme de dictionnaire : locals et globals.

Vous vous rappelez de locals ? Vous l'avez vu pour la première fois ici :

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

Mais vous ne pouvez rien apprendre sur locals pour le moment. Vous devez d'abord apprendre les espaces de noms. C'est un concept un peu aride mais important, lisez donc attentivement.

Python utilise ce que l'on appelle des espaces de noms pour suivre les variables. Un espace de noms est semblable a un dictionnaire dans lequel les clés sont les noms des variables et les valeurs du dictionnaire sont les valeurs des variables. En fait, on accède à un espace de noms comme à un dictionnaire Python, comme nous le verrons un peu plus loin.

A n'importe quel point dans un programme Python, il y a plusieurs espaces de noms disponibles. Chaque fonction a son propre espace de noms, appelé espace de noms local, qui suit les variables de la fonction, y compris ses arguments et les variables définies localement. Chaque module a son propre espace de noms, appelé l'espace de noms global, qui suit les variables du module, y compris les fonctions, les classes, les modules importés et les variables et constantes du module. Il y a également un espace de noms prédéfini, accessible de n'importe quel module et qui contient les fonctions et exceptions du langage.

Lorsqu'une ligne de code demande la valeur d'une variable x, Python recherche cette variable dans tous les espaces de noms disponibles dans l'ordre suivant :

  1. Espace de noms local - spécifique à la fonction ou méthode de classe en cours. Si la fonction a défini une variable locale x, ou si elle a un argument x, Python l'utilise et arrête sa recherche.
  2. Espace de noms global - spécifique au module en cours. Si le module a défini une variable, une fonction ou une classe nommée x, Python l'utilise et arrête sa recherche.
  3. Espace de noms prédéfini - global à tous les modules. En dernière instance, Python considère que x est le nom d'une fonction ou variable du langage.

Si Python ne trouve x dans aucun de ces espaces de noms, il abandonne et déclenche une exception NameError avec le message There is no variable named 'x', que vous avez vu tout au début au chapitre 1, mais à ce moment là vous ne pouviez pas savoir tout le travail que Python fait avant de vous renvoyer cette erreur.

Python 2.2 a introduit une modification légère mais importante qui affecte l'ordre de recherche dans les espaces de noms : les portées imbriquées. Dans les versions précédentes de Python, lorsque vous référenciez une variable dans une fonction imbriquée ou une fonction lambda, Python recherchait la variable dans l'espace de noms de la fonction (imbriquée ou lambda) en cours, puis dans l'espace de noms du module. Python 2.2 recherche la variable dans l'espace de noms de la fonction (imbriquée ou lambda) en cours, puis dans l'espace de noms de la fonction parente, puis dans l'espace de noms du module. Python 2.1 peut adopter l'un ou l'autre de ces comportements, par défaut il fonctionne comme Python 2.0, mais vous pouvez ajouter la ligne de code suivante au début de vos modules pour les faire fonctionner comme avec Python 2.2 :
 
Sélectionnez
from __future__ import nested_scopes

Vous êtes perdu ? Ne vous inquiétez pas, je vous promet que c'est très utile. Comme beaucoup de chose en Python, les espaces de noms sont directement accessibles durant l'exécution. L'espace de noms local est accessible par la fonction prédéfinie locals et l'espace de noms global (du module) est accessible par la fonction prédéfinie globals.

Exemple 8.10. Présentation de locals

 
Sélectionnez
>>> def foo(arg): ***1***
...     x = 1
...     print locals()
...     
>>> foo(7)        ***2***
{'arg': 7, 'x': 1}
>>> foo('bar')    ***3***
{'arg': 'bar', 'x': 1}

***1*** La fonction foo a deux variables dans son espace de noms local : arg, dont la valeur est passée à la fonction et x, qui est définie dans la fonction.

***2*** locals retourne un dictionnaire de paires nom/valeur. Les clés du dictionnaire sont les noms des variables sous forme de chaînes, les valeurs du dictionnaire sont les valeurs des variables. Donc, appeler foo avec 7 affiche un dictionnaire contenant les deux variables locales de la fonction : arg (7) et x (1).

***3*** Rappelez-vous que Python est à typage dynamique, vous pouvez donc passer une chaîne pour arg, la fonction (et l'appel à locals) fonctionne tout aussi bien. locals fonctionne avec toutes les variables de tous les types.

Ce que fait locals pour l'espace de noms local (de la fonction), globals le fait pour l'espace de noms global (du module). globals est cependant plus intéressant, parce que l'espace de noms d'un module est plus intéressant. L'espace de noms d'un module ne comprend pas seulement les variables et constantes du module mais aussi l'ensemble des fonctions et classes définies dans le module. De plus, il contient tout ce qui a été importé dans le module.

Vous rappelez-vous de la différence entre from module import et import module ? Avec import module, le module lui-meme est importé mais il garde son propre espace de noms, c'est pourquoi vous devez utiliser le nom du module pour accéder à ses fonctions ou attributs : module.fonction. Mais avec from module import, vous importez des fonctions et des attributs spécifiques d'un autre module dans votre propre espace de noms, c'est pourquoi vous y accédez directement sans référence au module dont ils viennent. Avec la fonction globals, vous pouvez voir ce qui se passe.

Exemple 8.11. Présentation de globals

Regardez me bloc de code suivant, à la fin de BaseHTMLProcessor.py:

 
Sélectionnez
if __name__ == "__main__":
    for k, v in globals().items():             ***1***
        print k, "=", v

***1*** Ne soyez pas intimidé, vous avez déjà vu tout cela. La fonction globals retourne un dictionnaire et nous parcourons le dictionnaire à l'aide de la méthode items et de l'assignement multiple. Le seul élément nouveau ici est la fonction globals.

Maintenant, exécuter le programme de la ligne de commande nous donne la sortie suivante (notez qu'elle peut être légèrement différente en fonction de votre plate-forme et de l'endroit où vous avez installé Python) :

 
Sélectionnez
c:\docbook\dip\py> python BaseHTMLProcessor.py

SGMLParser = sgmllib.SGMLParser                ***1***
htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> ***2***
BaseHTMLProcessor = __main__.BaseHTMLProcessor ***3***
__name__ = __main__                            ***4***
... rest of output omitted for brevity...

***1*** SGMLParser a été importé de sgmllib, en utilisant from module import. Cela veut dire qu'il a été importé directement dans l'espace de noms du module, et nous le voyons donc s'afficher.

***2*** Comparez avec htmlentitydefs, qui a été importé avec import. Le module htmlentitydefs lui-même est dans notre espace de noms, mais la variable entitydefs définie dans htmlentitydefs ne l'est pas.

***3*** Ce module ne définit qu'une classe, BaseHTMLProcessor et la voici. Notez que la valeur est ici la classe elle-même et non une instance quelconque de cette classe.

***4*** Vous rappelez-vous de l'astuce if __name__ ? Lorsque vous exécutez un module (plutôt que de l'importer d'un autre module), l'attribut prédéfini __name__ a une valeur spéciale, __main__. Comme nous avons exécuté ce module comme un programme de la ligne de commande, __name__ vaut __main__, c'est pourquoi notre petit code de test qui affiche globals est exécuté.

A l'aide des fonctions locals et globals, vous pouvez obtenir la valeur d'une variable quelconque dynamiquement, en fournissant le nom de la variable sous forme de chaîne. C'est une fonctionnalité semblable à celle de la fonction getattr, qui vous permet d'accéder à une fonction quelconque dynamiquement en fournissant le nom de la fonction sous la forme d'une chaîne.

Il y a une différence importante entre locals et globals que vous devez apprendre maintenant pour ne pas qu'elle vous joue des tours plus tard. Elle vous jouera des tours de toute manière mais au moins vous vous souviendrez que vous l'avez appris.

Exemple 8.12. locals est en lecture seule, globals ne l'est pas

 
Sélectionnez
def foo(arg):
    x = 1
    print locals()    ***1***
    locals()["x"] = 2 ***2***
    print "x=",x      ***3***

z = 7
print "z=",z
foo(3)
globals()["z"] = 8    ***4***
print "z=",z          ***5***

***1*** Puisque foo est appelé avec 3 en paramètre, cela affichera {'arg': 3, 'x': 1}. Cela ne devrait pas surprendre.

***2*** locals est une fonction qui retourne un dictionnaire et ici vous changez une valeur dans ce dictionnaire. Vous pourriez penser que cela change la valeur de la variable locale x à 2, mais ce n'est pas le cas. locals ne retourne pas vraiment l'espace de noms local mais une copie, modifier le dictionnaire retourné ne change pas les variables de l'espace de noms local.

***3*** Ceci affiche x= 1, pas x= 2.

***4*** Après avoir été déçu par locals, vous pourriez penser que cela ne va pas changer la valeur de z, mais ce serait une erreur. Pour des raisons ayant trait à l'implémentation de Python (dans le détail desquelles je ne rentrerais pas, ne les comprenant pas totalement), globals retourne l'espace de noms global lui-même et non une copie, le comportement inverse de locals. Donc, toute modification du dictionnaire retourné par globals affecte les variables globales.

***5*** Ceci affiche z= 8, pas z= 7.

VIII-F. Formatage de chaînes à l'aide d'un dictionnaire

Pourquoi avoir appris locals et globals ? Pour apprendre le formatage de chaînes à l'aide d'un dictionnaire. Comme vous vous le rappelez, le formatage de chaînes permet d'insérer facilement des valeurs dans des chaînes. Les valeurs sont énumérées dans un tuple et insérées dans l'ordre dans la chaîne à la place de chaque marqueur de formatage. Bien que ce soit efficace, cela ne donne pas le code le plus simple à lire, surtout quand de multiples valeurs sont insérées. Vous ne pouvez pas simplement lire la chaîne en une fois pour comprendre ce que le résultat va être, vous devez constamment passer de la chaîne au tuple.

Il existe une technique de formatage de chaînes alternative utilisant un dictionnaire au lieu de valeurs stockées dans un tuple.

Exemple 8.13. Présentation du formatage de chaînes à l'aide d'un dictionnaire

 
Sélectionnez
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> "%(pwd)s" % params                                    ***1***
'secret'
>>> "%(pwd)s is not a good password for %(uid)s" % params ***2***
'secret is not a good password for sa'
>>> "%(database)s of mind, %(database)s of body" % params **3***
'master of mind, master of body'

***1*** Au lieu d'un tuple de valeurs explicites, ce type de formatage de chaînes utilise un dictionnaire, params. Et au lieu d'un simple marqueur %s dans la chaîne, le marqueur contient un nom entre parenthèses. Ce nom est utilisé comme clé dans le dictionnaire params et la valeur correspondante, secret, est substituée au marqueur %(pwd)s.

***2*** Le formatage à l'aide d'un dictionnaire fonctionne avec n'importe quel nombre de clés nommées. Chaque clé doit exister dans le dictionnaire, sinon le formatage échouera avec une erreur KeyError.

***3*** Vous pouvez même insérer la même clé plusieurs fois, chaque occurrence sera remplacée avec la même valeur.

Quand utiliser le formatage à l'aide d'un dictionnaire ? Il est un peu exagéré de mettre en place un dictionnaire de clés et de valeurs simplement pour formater une ligne, c'est en fait plus approprié lorsque vous avez déjà un dictionnaire. Comme par exemple locals.

Exemple 8.14. Formatage à l'aide d'un dictionnaire dans BaseHTMLProcessor.py

 
Sélectionnez
    def handle_comment(self, text):        
        self.pieces.append("<!--%(text)s-->" % locals()) ***1***

***1*** L'utilisation de la fonction prédéfinie locals est le cas le plus commun d'emploi du formatage à l'aide d'un dictionnaire. Cela vous permet d'utiliser les noms des variables locales dans votre chaîne (dans ce cas, text, qui a été passé en argument à la méthode de classe) et chaque variable sera remplacée par sa valeur. Si text est 'Begin page footer', le formatage de chaîne "<!--%(text)s-->" % locals() se traduira par la chaîne '<!--Begin page footer-->'.

Exemple 8.15. Autres exemples de formatage à l'aide d'un dictionnaire

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

***1*** Lorsque cette méthode est appelée, attrs est une liste de tuples clé/valeur, comme les items (éléments) d'un dictionnaire, ce qui signifie que nous pouvons utiliser l'assignement multiple pour la parcourir. Cela devrait être un motif familier maintenant, mais il se passe beaucoup de choses ici, détaillons-les :

  1. Supposez que attrs vaut [('href', 'index.html'), ('title', 'Go to home page')].
  2. Durant la première étape de la list comprehension, la clé (key) sera 'href' et la valeur (value) 'index.html'.
  3. Le formatage de chaîne ' %s="%s"' % (key, value) donnera ' href="index.html"'. Cette chaîne devient le premier élément de la valeur de retour de la list comprehension.
  4. Durant la seconde étape, key sera 'title' et value 'Go to home page'.
  5. Le formatage de chaîne donnera ' title="Go to home page"'.
  6. La list comprehension retourne une liste de ces deux chaînes et strattrs joindra les deux éléments de cette liste pour former ' href="index.html" title="Go to home page"'.

***2*** Maintenant, en utilisant le formatage à l'aide d'un dictionnaire, nous insérons la valeur de tag et de strattrs dans une chaîne. Donc si tag vaut 'a', le résultat final sera <b>'<a href="index.html" title="Go to home page">'</b> et c'est ce qui sera ajouté à self.pieces.

L'utilisation du formatage de chaîne à l'aide d'un dictionnaire avec locals est une manière pratique de rendre des expressions de formatage complexes plus lisibles, mais elle a un prix. Il y a une petite baisse de performance due à l'appel de locals, puisque locals effectue une copie de l'espace de noms local.

VIII-G. Mettre les valeurs d'attributs entre guillemets

Une question courante sur comp.lang.python est la suivante : «J'ai plein de documents HTML avec des valeurs d'attributs sans guillemets et je veux les mettre entre guillemets. Comment faire ?»(5)(C'est en général du à un chef de projet qui pratique la religion du HTML-est-un-standard et proclame que toutes les pages doivent passer les tests d'un validateur HTML. Les valeurs d'attributs sans guillemets sont une violation courante du standard HTML). Quelle que soit la raison, les valeurs d'attributs peuvent se voir dotées de guillemets en soumettant le HTML à BaseHTMLProcessor.

BaseHTMLProcessor prend du HTML en entrée (puisqu'il est dérivé de SGMLParser) et produit du HTML, mais le HTML en sortie n'est pas identique à l'entrée. Les balises et les noms d'attributs sont mis en minuscules et les valeurs d'attributs sont mises entre guillemets, quel qu'ait été les format en entrée. C'est de cet effet de bord que nous pouvons profiter.

Exemple 8.16. Mettre les valeurs d'attributs entre guillemets

 
Sélectionnez
	>>> htmlSource = """        ***1***
	...     <html>
	...     <head>
	...     <title>Test page</title>
	...     </head>
	...     <body>
	...     <ul>
	...     <li><a href=index.html>Home</a></li>
	...     <li><a href=toc.html>Table of contents</a></li>
	...     <li><a href=history.html>Revision history</a></li>
	...     </body>
	...     </html>
	...     """
	>>> from BaseHTMLProcessor import BaseHTMLProcessor
	>>> parser = BaseHTMLProcessor()
	>>> parser.feed(htmlSource) ***2***
	>>> print parser.output()   ***3***
	<html>
	<head>
	<title>Test page</title>
	</head>
	<body>
	<ul>
	<li><a href="index.html">Home</a></li>
	<li><a href="toc.html">Table of contents</a></li>
	<li><a href="history.html">Revision history</a></li>
	</body>
	</html>

***1*** Notez que la valeur de l'attribut href de la balise <a> n'est pas entre guillemets. (Notez aussi que nous utilisons des triples guillemets pour quelque chose d'autre qu'une doc string et directement dans l'IDE. Elles sont très utiles.)
***2*** On passe la chaîne au parser.
***3*** En utilisant la fonction output définie dans BaseHTMLProcessor, nous obtenons la sortie sous forme d'une chaîne unique, avec les valeurs d'attributs entre guillemets. Cela peut sembler évident, mais réfléchissez à tout ce qui s'est passé ici : SGMLParser a analysé le document HTML en entier, le décomposant en balises, références, données etc. ; BaseHTMLProcessor a utilisé tous ces éléments pour reconstruire des pièces de HTML (qui sont encore stockées dans parser.pieces, si vous voulez les voir) ; finalement, nous avons appelé parser.output, qui a assemblé l'ensemble des pièces de HTML en une chaîne.

VIII-H. 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

 
Sélectionnez
  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

 
Sélectionnez
   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

 
Sélectionnez
    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.

VIII-I. Assembler les pièces

Il est temps d'utiliser tout ce que nous avons appris. J'espère que vous avez été attentif.

Exemple 8.20. La fonction translate, première partie

 
Sélectionnez
def translate(url, dialectName="chef"): ***1***
    import urllib                       ***2***
    sock = urllib.urlopen(url)          ***3***
    htmlSource = sock.read()           
    sock.close()

***1*** La fonction translate a un argument optionnel dialectName, qui est une chaîne spécifiant le dialecte que nous allons utiliser. Nous allons voir comment il est employé dans une minute.

***2*** Attendez une seconde, il y a une instruction import dans cette fonction ! C'est parfaitement légal en Python. Vous êtes habitué à voir des instructions import au début d'un programme, ce qui signifie que le module importé est disponible n'importe où dans le programme. Mais vous pouvez également importer des modules dans une fonction, ce qui signifie que le module importé n'est disponible qu'à l'intérieur de cette fonction. Si un module n'est utilisé que dans une seule fonction, c'est un bon moyen de rendre votre code plus modulaire (quand vous vous rendrez compte que votre bidouille du week-end est devenue une oeuvre respectable de 800 lignes et que vous déciderez de la segmenter en une dizaine de modules réutilisables, vous apprécierez cette possibilité).

***3*** Ici, nous obtenons le code source de l'URL passée en paramètre.

Exemple 8.21. La fonction translate, deuxième partie : de bizarre en étrange

 
Sélectionnez
 parserName = "%sDialectizer" % dialectName.capitalize() ***1***
    parserClass = globals()[parserName]                     ***2***
    parser = parserClass()                                  ***3***

***1*** capitalize est une méthode de chaîne que nous n'avons pas encore vue. Elle met simplement en majuscule la première lettre d'une chaîne et met le reste en minuscules. En la combinant à un formatage de chaîne, nous avons pris le nom d'un dialecte et l'avons transformé en un nom de classe Dialectizer lui correspondant. Si dialectName est la chaîne 'chef', parserName sera la chaîne 'ChefDialectizer'.

***2*** Nous avons le nom d'une classe sous forme de chaîne (parserName) et l'espace de noms sous forme de dictionnaire (globals()). En les combinant, nous pouvons obtenir une référence à la classe désignée par la chaîne (rappelez-vous que les classes sont des objets et peuvent être assignés à des variables comme n'importe quel autre objet). Si parserName est la chaîne 'ChefDialectizer', parserClass sera la classe ChefDialectizer.

***3*** Maintenant nous avons un objet de classe (parserClass) et nous voulons une instance de la classe. Nous savons déjà comment on fait ça, en appelant la classe comme une fonction. Le fait que la classe soit référencée par une variable locale ne fait absolument aucune différence, nous appelons simplement la variable locale comme une fonction et obtenons une instance de la classe. Si parserClass est la classe ChefDialectizer, parser sera une instance de la classe ChefDialectizer.

Pourquoi un tel effort ? Après tout, il n'y a que 3 classes Dialectizer, pourquoi ne pas utiliser simplement une instruction case (il n'y a pas de case en Python, mais nous pourrions utiliser une série d'instructions if) ? Pour une seule raison, l'extensibilité. La fonction translate n'a absolument aucune idée du nombre de classes Dialectizer que nous avons défini. Imaginez que nous définissions une nouvelle classe FooDialectizer demain, translate continuerait de fonctionner en recevant 'foo' en paramètre dialectName.

Encore mieux, imaginez que nous mettions FooDialectizer dans un module séparé et que nous l'importions par from module import. Nous avons déjà vu que cela l'ajoute à globals(), donc translate fonctionnerait toujours sans modification, même si FooDialectizer était dans un autre fichier.

Maintenant, imaginez que le nom du dialecte provienne de l'extérieur du programme, par exemple d'une base de données ou d'une valeur entrée par un utilisateur dans un formulaire. Vous pouvez utiliser n'importe quelle architecture Python côté serveur pour générer dynamiquement des pages Web, cette fonction pourrait prendre une URL et un nom de dialecte (les deux sous la forme de chaîne) dans la chaîne d'une requête de page Web et renvoyer la page Web «traduite».

Finalement, imaginez un framework Dialectizer avec une architecture de plug-ins. Vous pourriez mettre chaque classe Dialectizer dans un fichier séparé, laissant uniquement la fonction translate dans dialect.py. Avec un modèle de nommage uniforme, la fonction translate pourrait importer dynamiquement la classe appropriée du fichier approprié, uniquement à partir du nom de dialecte (vous n'avez pas encore vu d'importation dynamique, mais je promet de la traiter dans un prochain chapitre). Pour ajouter un nouveau dialecte, vous ajouteriez simplement un nouveau fichier correctement nommé dans le répertoire des plug-ins (par exemple foodialect.py contenant la classe FooDialectizer). Appeler la fonction translate avec le nom du dialecte 'foo' ferait charger le module foodialect.py, importer la classe FooDialectizer et lancer la traduction.

Exemple 8.22. La fonction translate, troisième partie

 
Sélectionnez
   parser.feed(htmlSource) ***1***
    parser.close()          ***2***
    return parser.output()  ***3***

***1*** Après tout ce que je vous ai demandé d'imaginer, cela va sembler plutôt ennuyeux, mais la fonction feed est responsable de toute la transformation. Nous avons l'ensemble du source HTML rassemblé en une seule chaîne, donc nous n'avons à appeler feed qu'une seule fois. Cependant, vous pouvez appeler feed autant de fois que vous le voulez et le parser continuera son travail. Si vous vous inquiétez de l'utilisation mémoire (ou si vous savez que vous aurez à traiter de très grandes pages HTML), vous pouvez écrire une boucle dans laquelle vous lisez quelques lignes de HTML et les passez au parser. Le résultat serait le même.

***2*** Comme feed gère un tampon interne, vous devez toujours appelez la méthode close du parser lorsque vous avez terminé (même si vous lui avez passé la totalité en une seule fois comme nous venons de le faire). Dans le cas contraire vous risquez de vous apercevoir que votre sortie est tronquée.

***3*** Rappelez-vous que output est la fonction que nous avons définie dans BaseHTMLProcessor qui assemble toutes les pièces de sortie que nous avons stockées en tampon et les retourne sous forme d'une chaîne unique.

Et rien qu'en faisant ça, nous avons «traduit» une page Web, rien qu'à partir d'une URL et d'un nom de dialecte.

Pour en savoir plus

  • Vous croyiez que je plaisantais quand je parlais de traitement côté serveur. C'est ce que je pensais aussi, jusqu'à ce que je trouve ce «traducteur» en ligne. Malheureusement, le code source n'a pas l'air d'être disponible.

VIII-J. Résumé

Python vous fournit un outil puissant, sgmllib.py, pour manipuler du code HTML en transformant sa structure en modèle objet. Vous pouvez utiliser cet outil de nombreuses manières.

  • analyser le code HTML en cherchant quelque chose de précis
  • assembler les résultats, comme le fait URL lister
  • modifier la structure à la volée, comme le fait attribute quoter
  • transformer le code HTML en quelque chose d'autre en manipulant le texte sans toucher aux balises, comme le fait Dialectizer

En plus de ces exemples, vous devriez vous sentir à l'aise pour :

  • Utiliser locals() et globals() pour accéder aux espaces de noms
  • Formater des chaînes à l'aide d'un dictionnaire

précédentsommairesuivant
Le terme technique pour un analyseur comme SGMLParser est consommateur: il consomme du HTML est le décompose. On peut penser que le nom de feed (nourrir) a été choisi pour cadrer avec ce modèle du «consommateur». Personnellement, ça me fait penser à une cage sombre dans un zoo, sans arbres ni plantes ni trace de vie d'aucune sorte, mais où vous pouvez deviner, si vous vous tenez tout à fait immobile, deux yeux en vrilles qui vous regardent en retour dans le coin du fond à gauche, mais vous arrivez à vous convaincre que c'est votre esprit qui vous joue des tours et la seule chose qui vous permet de dire que ce n'est pas une cage vide est une petite pancarte sur la rambarde sur laquelle est écrit «Ne pas nourrir l'analyseur.» Mais peut-être que j'ai trop d'imagination. De toute manière, c'est une image mentale intéressante.
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).
Bon, en fait ce n'est pas une question si courante. Elle n'est pas aussi courante que «Quel éditeur faut-il utiliser pour écrire du code Python ?» (réponse : Emacs) ou «Python est-il meilleur ou moins bon que Perl?» (réponse : «Perl est moins bon que Python parce que les gens voulaient qu'il soit moins bon.» Larry Wall, 14/10/1998) Mais des questions sur le traitement du HTML apparaissent sous une forme ou l'autre à peu près une fois par mois et parmi ces questions celle-ci est fréquente.