Python et le web

Pour le protocole http :

  • urllib permet d'effectuer facilement des opérations simples (ouvrir une URL comme un fichier, GET et POST)
  • urllib2 permet des transactions plus avancées (authentification, cookies, redirections ...)
  • urlparse analyse ou construit des URL complexes
  • SimpleHTTPServer permet de monter un serveur (de test !) en quelques lignes
  • cgi permet d'écrire des serveurs de scripts
  • httplib, BaseHTTPServer, CGIHTTPServer : fonctionnalités de plus bas niveau, à éviter si possible
  • xmlrpclib Web services (minimaliste)

Pour le traitement du HTML :

Pour la construction de sites :

Django, Turbogears, Flask (frameworks) Zope (serveur d'applications), Plone (gestion de contenu) Bibliothèques (externes) pour SOAP, CORBA, REST ...

Le module urllib

Fonctions de base pour lire des données à partir d'une URL. Protocoles : http, https, ftp, gopher, file.

In [1]:
import urllib
s = urllib.urlopen('http://igm.univ-mlv.fr/~jyt/M1_python').read()
In [2]:
print s
<html>
	<body>
		<H1> M1 Informatique : Python </H1>
		<ul>
			<li> Commencer ici : <a href="http://www.python.org/">python.org</a>
			<li> La <a href="http://docs.python.org/2.7">documentation</a> officielle
			<li> Le <a href="http://www.pythonchallenge.com/"> Python Challenge</a>
			<li> <a href="http://diveintopython.net/"><i>Dive into Python</i></a>  
			par <a href="http://en.wikipedia.org/wiki/Mark_Pilgrim_%28software_developer%29">Mark Pilgrim</a> (un livre recommand&eacute;);
			<a href="http://www.diveintopython3.net/">version Python 3</a>. 
			<li> La <a href="http://fr.wikipedia.org/wiki/Python_%28langage%29">page</a> de Wikip&eacute;dia contient un bon r&eacute;sum&eacute;
			<li> Introduction au <i>notebook</i> ipython ou jupyter : <a href="intro_jupyter.html">html</a>, <a href="intro_jupyter.ipynb">ipynb</a>
			<ul> Cours 1 (29/09/2017)
				<li> Python 2 <a href="python_M1_2017-1.html">html</a>, <a href="python_M1_2017-1.ipynb">ipynb</a>
				<li> Python 3 <a href="M1_python3-1.html">html</a>, <a href="M1_python3-1.ipynb">ipynb</a> 
			</ul>
			<a href="td1.html"> TD 1</a> (29/09/2017)
			<li> Corrig&eacute; du TD 1
			<ul>
				<li><a href="td1_sol.html">Python 2</a>
				<li><a href="td1_sol_3.html">Python 3</a>
			</ul>
			<a href="script.py.html">Mod&egrave;le</a> de script
			<li> Cours 2 (13/10/2017) : <a href="python_M1_2017-2.html">html</a>, <a href="python_M1_2017-2.ipynb">ipynb</a>
			<li><a href="td2.html">TD 2</a> (13/10/2017)
			

		</ul>
	</body>
</html>

		


urllib.urlopen renvoie un objet "file-like".

Méthodes :

read(), readline(), readlines(), fileno(), close(), et en plus info(), geturl().

In [3]:
 f = urllib.urlopen('http://igm.univ-mlv.fr/~jyt/M1_python')
In [4]:
f.fileno()
Out[4]:
58
In [5]:
print dir(f)
['__doc__', '__init__', '__iter__', '__module__', '__repr__', 'close', 'code', 'fileno', 'fp', 'getcode', 'geturl', 'headers', 'info', 'next', 'read', 'readline', 'readlines', 'url']

In [6]:
f.headers
Out[6]:
<httplib.HTTPMessage instance at 0x236bd40>
In [7]:
list(_)
Out[7]:
['content-length',
 'accept-ranges',
 'server',
 'last-modified',
 'connection',
 'etag',
 'date',
 'content-type']
In [8]:
f.geturl()
Out[8]:
'http://igm.univ-mlv.fr/~jyt/M1_python/'
In [9]:
f.info()
Out[9]:
<httplib.HTTPMessage instance at 0x236bd40>
In [10]:
f.info().items()
Out[10]:
[('content-length', '1509'),
 ('accept-ranges', 'bytes'),
 ('server', 'Apache'),
 ('last-modified', 'Fri, 13 Oct 2017 08:46:53 GMT'),
 ('connection', 'close'),
 ('etag', '"4ff4105-5e5-55b69b2cc9940"'),
 ('date', 'Tue, 17 Oct 2017 13:10:41 GMT'),
 ('content-type', 'text/html')]
In [11]:
f.close()

Exemple

Pour savoir si un nouveau document a été mis en ligne (le fichier etag_python doit avoir été initialisé) :

In [12]:
#import urllib

url = 'http://igm.univ-mlv.fr/~jyt/M1_python'
t = open('etag_python').read()
d = urllib.urlopen(url).info()
s = d['etag']
print d['last-modified']
if s <> t :
    print "La page du cours de Python a été modifiée"
    open('etag_python','w').write(s)
else: print "Aucune modification"
---------------------------------------------------------------------------
IOError                                   Traceback (most recent call last)
<ipython-input-12-e1940ee152b5> in <module>()
      2 
      3 url = 'http://igm.univ-mlv.fr/~jyt/M1_python'
----> 4 t = open('etag_python').read()
      5 d = urllib.urlopen(url).info()
      6 s = d['etag']

IOError: [Errno 2] No such file or directory: 'etag_python'

urlopen prend un paramètre optionnel, data. Si

data est None, elle envoie une requête GET, sinon une requête POST.

In [13]:
url='http://oeis.org'
query={'q':'1,1,3,11,49,257','language':'english', 'go':'Search'}
data = urllib.urlencode(query)

data
Out[13]:
'q=1%2C1%2C3%2C11%2C49%2C257&go=Search&language=english'
In [14]:
s = urllib.urlopen(url,data).read()
In [15]:
s[:500]
Out[15]:
'\n<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<html>\n  \n  <head>\n  <style>\n  tt { font-family: monospace; font-size: 100%; }\n  p.editing { font-family: monospace; margin: 10px; text-indent: -10px; word-wrap:break-word;}\n  p { word-wrap: break-word; }\n  </style>\n  <meta http-equiv="content-type" content="text/html; charset=utf-8">\n  <meta name="keywords" content="OEIS,integer sequences,Sloane" />\n  \n  \n  <title>1,1,3,11,49,257 - OEIS</title>\n  <link rel="search" type="application/opens'
In [16]:
urllib.unquote(data)
Out[16]:
'q=1,1,3,11,49,257&go=Search&language=english'

Le module urlparse

La manipulation des URLs est facilitée par le module urlparse

In [17]:
x='http://www.google.fr/search?as_q=python&hl=fr&num=10&btnG=Recherche+Google&as_epq=&as_oq=&as_eq=&lr=&cr=&as_ft=i&as_filetype=pdf&as_qdr=all&as_occt=any&as_dt=i&as_sitesearch=univ-mlv.fr&as_rights=&safe=images'

from urlparse import *

y = urlsplit(x)
y
Out[17]:
SplitResult(scheme='http', netloc='www.google.fr', path='/search', query='as_q=python&hl=fr&num=10&btnG=Recherche+Google&as_epq=&as_oq=&as_eq=&lr=&cr=&as_ft=i&as_filetype=pdf&as_qdr=all&as_occt=any&as_dt=i&as_sitesearch=univ-mlv.fr&as_rights=&safe=images', fragment='')
In [18]:
y.path
Out[18]:
'/search'
In [19]:
urlunsplit(y)
Out[19]:
'http://www.google.fr/search?as_q=python&hl=fr&num=10&btnG=Recherche+Google&as_epq=&as_oq=&as_eq=&lr=&cr=&as_ft=i&as_filetype=pdf&as_qdr=all&as_occt=any&as_dt=i&as_sitesearch=univ-mlv.fr&as_rights=&safe=images'
In [20]:
parse_qs(urlsplit(x).query)
Out[20]:
{'as_dt': ['i'],
 'as_filetype': ['pdf'],
 'as_ft': ['i'],
 'as_occt': ['any'],
 'as_q': ['python'],
 'as_qdr': ['all'],
 'as_sitesearch': ['univ-mlv.fr'],
 'btnG': ['Recherche Google'],
 'hl': ['fr'],
 'num': ['10'],
 'safe': ['images']}

URL schemes : file, ftp, gopher, hdl, http, https, imap, mailto, mms, news, nntp, prospero, rsync, rtsp, rtspu, sftp, shttp, sip, sips, snews, svn, svn+ssh, telnet, wais

Le module urllib2

La fonction urllib.urlopen suffit pour les applications les plus courantes.

Elle supporte les proxys pourvu qu'ils ne demandent pas d'authentification. Il suffit de positionner les variables d'environnement

http_proxy, ftp_proxy, etc.
$ http_proxy="http://www.monproxy.com:1234"
$ export http_proxy
$ python
Pour un contrôle plus fin (authentification, user-agent, cookies) on peut utiliser urllib2. Le fonction urllib2.urlopen prend comme paramètre un objet de type Request
In [21]:
import urllib2
url='http://oeis.org'
req = urllib2.Request(url)
dir(req)
Out[21]:
['_Request__fragment',
 '_Request__original',
 '__doc__',
 '__getattr__',
 '__init__',
 '__module__',
 '_tunnel_host',
 'add_data',
 'add_header',
 'add_unredirected_header',
 'data',
 'get_data',
 'get_full_url',
 'get_header',
 'get_host',
 'get_method',
 'get_origin_req_host',
 'get_selector',
 'get_type',
 'has_data',
 'has_header',
 'has_proxy',
 'header_items',
 'headers',
 'host',
 'is_unverifiable',
 'origin_req_host',
 'port',
 'set_proxy',
 'type',
 'unredirected_hdrs',
 'unverifiable']

En spécifiant les en-têtes, on peut par exemple se faire passer pour IE et envoyer un cookie :

import urllib2
url='http://www.hargneux.com/patteblanche.php'
req=urllib2.Request(url)
req.add_header('Accept','text/html')
req.add_header('User-agent','Mozilla/4.0 
        (compatible; MSIE 5.5; Windows NT)')
req.add_header('Cookie',
       'info=En-veux-tu%3F%20En%20voil%E0%21')
handle=urllib2.urlopen(req)

Quand on ouvre une URL, on utilise un opener. On peut remplacer l'opener par défaut pour gérer l'authentification, les proxys, etc. Les openers utilisent des handlers.

build_opener est utilisé pour créer des objets opener, qui permettent d'ouvrir des URLs avec des handlers spécifiques. Les handlers peuvent gérer des cookies, l'authentification, et autres cas communs mais un peu spécifiques. Les objets Opener ont une méthode open, qui peut être appelée directement pour ouvrir des urls de la même manière que la fonction urlopen. install_opener peut être utilisé pour rendre l'objet opener l'opener par défaut. Cela signifie que les appels à urlopen l'utiliseront. ### Exemple : authentification basique. Pour demander une authentification, le serveur envoie le coded'erreur 401 et un en-tête du type www-authenticate: SCHEME realm="REALM"

Le client doit alors re-essayer la requête avec un couple username, password) correct pour le domaine (realm).

On peut gérer cela avec une instance de HTTPBasicAuthHandler et un opener pour utiliser ce handler.

HTTPBasicAuthHandler utilise un "password manager" pour gérer la correspondance entre les URIs et realms (domaines) et les couple (password, username).

En général un seul domaine (realm) par URI : HTTPPasswordMgrWithDefaultRealm.

password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None,
    top_level_url, username, password)
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(handler)

opener.open(a_url)

urllib2.install_opener(opener)

Pour en savoir plus : urllib2 - The Missing Manual

Il existe un module tiers (requests) beaucoup plus pratique.

In [22]:
import requests
In [23]:
url = "http://igm.univ-mlv.fr/~jyt/secret/"
In [24]:
r = requests.get(url,auth=('jyt','toto'))
In [25]:
r.status_code
Out[25]:
200
In [26]:
r.text
Out[26]:
u"<html>\n\nIl n'y a rien a voir ici ...\n</html>\n"
In [27]:
r.headers
Out[27]:
CaseInsensitiveDict({'content-length': '45', 'accept-ranges': 'bytes', 'server': 'Apache', 'last-modified': 'Sun, 14 Oct 2012 16:46:23 GMT', 'etag': '"30089a9-2d-4cc07a93c95c0"', 'date': 'Tue, 17 Oct 2017 13:10:56 GMT', 'content-type': 'text/html'})

Côté serveur

Les modules BaseHTTPServer, SimpleHTTPServer et CGIHTTPServer permettent de mettre en place un serveur web opérationnel en quelques lignes.

En fait, en une ligne :

[jyt@scriabine ~]$ python -m SimpleHTTPServer 8000
Serving HTTP on 0.0.0.0 port 8000 ...
scriabine - - [13/Oct/2012 19:19:10] "GET / HTTP/1.1" 200 -
...

Et sous forme de programme :

import SimpleHTTPServer
import SocketServer

PORT = 8000
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler

httpd = SocketServer.TCPServer(("", PORT), Handler)

print "serving at port", PORT
httpd.serve_forever()

Un serveur de scripts minimal serait (cf. TD4)

#!/usr/bin/python

import os

from BaseHTTPServer import HTTPServer
from CGIHTTPServer import CGIHTTPRequestHandler
srvaddr = ('127.0.0.1',80)
server = HTTPServer(srvaddr, CGIHTTPRequestHandler)
server.serve_forever()

Le script cgi devra être placé dans un sous-répertoire cgi-bin, et le serveur devra avoir le droit d'exécution. Seul root peut lancer le serveur sur le port 80. En tant qu'utilisateur normal, on pourra le lancer sur un port libre, par exemple 8888. Le formulaire sera alors déclaré avec l'action

ACTION="http://127.0.01:8888/cgi-bin/monscript.cgi"

Traitement du HTML

Pour extraire des informations d'une page web, on peut parfois se débrouiller avec des expressions régulières.

Mais on a aussi souvent besoin d'une analyse complète.

Il existe pour cela un module HTMLParser, qui exporte une classe du même nom.

On l'illustrera sur un exemple tiré de la première édition de "Dive into Python" : traduire à la volée des pages web dans des dialectes farfelus tels que ceux proposés ici.

On trouvera dans le livre de Pilgrim des grammaires simple pour ({\it chef, fudd, olde}). Les textes sont supposés en anglais, donc en ASCII.

Dans le cours, on utilisera plutôt le touilleur de texte vu en TD pour brouiller une page web sans modifier sa mise en page.

Pour d'autres exemples, voir ce site.

Exemple: Elmer Fudd cf. Bugs Bunny)

Les dialectes sont définis par des substitutions, attribut subs d'une sous-classe Dialectizer de BaseHTMLProcessor, elle même dérivée de HTMLParser.

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.'))

Le module HTMLParser

Il contient la classe HTMLParser, qui réalise l'analyse syntaxique du HTML.

Dès qu'un élément utile est identifié (une balise ouvrante, start tag ¹lt;a ...> par exemple), une méthode handle_starttag, do_tag, ... est appelée.

HTMLParser analyse le HTML en 8 types de données, et appelle une méthode différente pour chaque cas :

  • start tag (balise ouvrante) : handle_starttag, handle_startendtag (pour les $<.../ >$ en xhtml)
  • end tag (balise fermante) : handle_endtag
  • character reference : par exemple &#160; handle_charref
  • entity reference : par exemple &copy;. handle_entityref
  • comment : handle_comment
  • processing instruction : <? ... >. handle_pi
  • declaration : <! ... >. handle_decl
  • text data : handle_data.

Pour l'utiliser, il faut créer une classe dérivée de HTMLParser, et surcharger ces 8 méthodes.

In [38]:
from HTMLParser import HTMLParser


class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print "Balise ouvrante :", tag, attrs

    def handle_endtag(self, tag):
        print "Balise fermante :", tag

    def handle_data(self, data):
        print "Texte  :", repr(data)

# On créee une instance
parser = MyHTMLParser()

# Et on lui donne à manger

from urllib import urlopen
s = urlopen('http://igm.univ-mlv.fr/~jyt/M1_python/').read()
parser.feed(s)
Balise ouvrante : html []
Texte  : '\n\t'
Balise ouvrante : body []
Texte  : '\n\t\t'
Balise ouvrante : h1 []
Texte  : ' M1 Informatique : Python '
Balise fermante : h1
Texte  : '\n\t\t'
Balise ouvrante : ul []
Texte  : '\n\t\t\t'
Balise ouvrante : li []
Texte  : ' Commencer ici : '
Balise ouvrante : a [('href', 'http://www.python.org/')]
Texte  : 'python.org'
Balise fermante : a
Texte  : '\n\t\t\t'
Balise ouvrante : li []
Texte  : ' La '
Balise ouvrante : a [('href', 'http://docs.python.org/2.7')]
Texte  : 'documentation'
Balise fermante : a
Texte  : ' officielle\n\t\t\t'
Balise ouvrante : li []
Texte  : ' Le '
Balise ouvrante : a [('href', 'http://www.pythonchallenge.com/')]
Texte  : ' Python Challenge'
Balise fermante : a
Texte  : '\n\t\t\t'
Balise ouvrante : li []
Texte  : ' '
Balise ouvrante : a [('href', 'http://diveintopython.net/')]
Balise ouvrante : i []
Texte  : 'Dive into Python'
Balise fermante : i
Balise fermante : a
Texte  : '  \n\t\t\tpar '
Balise ouvrante : a [('href', 'http://en.wikipedia.org/wiki/Mark_Pilgrim_%28software_developer%29')]
Texte  : 'Mark Pilgrim'
Balise fermante : a
Texte  : ' (un livre recommand'
Texte  : ');\n\t\t\t'
Balise ouvrante : a [('href', 'http://www.diveintopython3.net/')]
Texte  : 'version Python 3'
Balise fermante : a
Texte  : '. \n\t\t\t'
Balise ouvrante : li []
Texte  : ' La '
Balise ouvrante : a [('href', 'http://fr.wikipedia.org/wiki/Python_%28langage%29')]
Texte  : 'page'
Balise fermante : a
Texte  : ' de Wikip'
Texte  : 'dia contient un bon r'
Texte  : 'sum'
Texte  : '\n\t\t\t'
Balise ouvrante : li []
Texte  : ' Introduction au '
Balise ouvrante : i []
Texte  : 'notebook'
Balise fermante : i
Texte  : ' ipython ou jupyter : '
Balise ouvrante : a [('href', 'intro_jupyter.html')]
Texte  : 'html'
Balise fermante : a
Texte  : ', '
Balise ouvrante : a [('href', 'intro_jupyter.ipynb')]
Texte  : 'ipynb'
Balise fermante : a
Texte  : '\n\t\t\t'
Balise ouvrante : ul []
Texte  : ' Cours 1 (29/09/2017)\n\t\t\t\t'
Balise ouvrante : li []
Texte  : ' Python 2 '
Balise ouvrante : a [('href', 'python_M1_2017-1.html')]
Texte  : 'html'
Balise fermante : a
Texte  : ', '
Balise ouvrante : a [('href', 'python_M1_2017-1.ipynb')]
Texte  : 'ipynb'
Balise fermante : a
Texte  : '\n\t\t\t\t'
Balise ouvrante : li []
Texte  : ' Python 3 '
Balise ouvrante : a [('href', 'M1_python3-1.html')]
Texte  : 'html'
Balise fermante : a
Texte  : ', '
Balise ouvrante : a [('href', 'M1_python3-1.ipynb')]
Texte  : 'ipynb'
Balise fermante : a
Texte  : ' \n\t\t\t'
Balise fermante : ul
Texte  : '\n\t\t\t'
Balise ouvrante : a [('href', 'td1.html')]
Texte  : ' TD 1'
Balise fermante : a
Texte  : ' (29/09/2017)\n\t\t\t'
Balise ouvrante : li []
Texte  : ' Corrig'
Texte  : ' du TD 1\n\t\t\t'
Balise ouvrante : ul []
Texte  : '\n\t\t\t\t'
Balise ouvrante : li []
Balise ouvrante : a [('href', 'td1_sol.html')]
Texte  : 'Python 2'
Balise fermante : a
Texte  : '\n\t\t\t\t'
Balise ouvrante : li []
Balise ouvrante : a [('href', 'td1_sol_3.html')]
Texte  : 'Python 3'
Balise fermante : a
Texte  : '\n\t\t\t'
Balise fermante : ul
Texte  : '\n\t\t\t'
Balise ouvrante : a [('href', 'script.py.html')]
Texte  : 'Mod'
Texte  : 'le'
Balise fermante : a
Texte  : ' de script\n\t\t\t'
Balise ouvrante : li []
Texte  : ' Cours 2 (13/10/2017) : '
Balise ouvrante : a [('href', 'python_M1_2017-2.html')]
Texte  : 'html'
Balise fermante : a
Texte  : ', '
Balise ouvrante : a [('href', 'python_M1_2017-2.ipynb')]
Texte  : 'ipynb'
Balise fermante : a
Texte  : '\n\t\t\t'
Balise ouvrante : li []
Balise ouvrante : a [('href', 'td2.html')]
Texte  : 'TD 2'
Balise fermante : a
Texte  : ' (13/10/2017)\n\t\t\t\n\n\t\t'
Balise fermante : ul
Texte  : '\n\t'
Balise fermante : body
Texte  : '\n'
Balise fermante : html
Texte  : '\n\n\t\t\n'

In [38]:
### Exemple : URLLister

But : une classe pour extraire les liens d'une page web.
In [48]:
from HTMLParser import HTMLParser

class URLLister(HTMLParser):
    def reset(self):                              
        HTMLParser.reset(self)
        self.urls = []

    def handle_starttag(self, tag, attrs):                     
        href = [v for k, v in attrs if k=='href'] # attrs est une liste de couples
        if href:
            self.urls.extend(href)
In [49]:
s = urlopen('http://igm.univ-mlv.fr/~jyt/M1_python').read()
p = URLLister()
p.feed(s)
p.close()

for u in p.urls: print u
http://www.python.org/
http://docs.python.org/2.7
http://www.pythonchallenge.com/
http://diveintopython.net/
http://en.wikipedia.org/wiki/Mark_Pilgrim_%28software_developer%29
http://www.diveintopython3.net/
http://fr.wikipedia.org/wiki/Python_%28langage%29
intro_jupyter.html
intro_jupyter.ipynb
python_M1_2017-1.html
python_M1_2017-1.ipynb
M1_python3-1.html
M1_python3-1.ipynb
td1.html
td1_sol.html
td1_sol_3.html
script.py.html
python_M1_2017-2.html
python_M1_2017-2.ipynb
td2.html

En appliquant ceci à la page du cours, on pourrait tester (avec une regexp) si un nouveau pdf a été mis en ligne et le récuperer automatiquement ...

Le problème

Il s'agit de reproduire à l'identique le document HTML, en transformant seulement le texte, sauf s'il est encadré par une balise <pre&#gt;.

Pour varier les plaisirs, on pourra traduire de l'anglais en texan :

(^|" ")"American"            changeCase(" Amerkin");
(^|" ")"California"            changeCase(" Caleyfornyuh");
(^|" ")"Dallas"                changeCase(" Big D.");
(^|" ")"Fort Worth"            changeCase(" Fowert Wurth");
(^|" ")"Houston"            changeCase(" Useton");
(^|" ")"I don't know"            changeCase(" I-O-no");
(^|" ")"I will"|" I'll"            changeCase(" Ahl");
...

La classe BaseHTMLProcessor

On commence par construire une classe dérivée qui ne fait rien : elle recompose la page analysée sans la modifier.

On surchargera ensuite la méthode handle_data pour modifier le texte à notre convenance.

In [57]:
import htmlentitydefs

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

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

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

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

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

    def handle_data(self, text):  # A surcharger         
        self.pieces.append(text)

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

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

    def output(self):               
            """Return processed HTML as a single string"""
            return "".join(self.pieces) 
In [58]:
b=BaseHTMLProcessor()
b.feed(s)
In [59]:
b.close()
In [61]:
print b.pieces
['<html>', '\n\t', '<body>', '\n\t\t', '<h1>', ' M1 Informatique : Python ', '</h1>', '\n\t\t', '<ul>', '\n\t\t\t', '<li>', ' Commencer ici : ', '<a href="http://www.python.org/">', 'python.org', '</a>', '\n\t\t\t', '<li>', ' La ', '<a href="http://docs.python.org/2.7">', 'documentation', '</a>', ' officielle\n\t\t\t', '<li>', ' Le ', '<a href="http://www.pythonchallenge.com/">', ' Python Challenge', '</a>', '\n\t\t\t', '<li>', ' ', '<a href="http://diveintopython.net/">', '<i>', 'Dive into Python', '</i>', '</a>', '  \n\t\t\tpar ', '<a href="http://en.wikipedia.org/wiki/Mark_Pilgrim_%28software_developer%29">', 'Mark Pilgrim', '</a>', ' (un livre recommand', '&eacute', ';', ');\n\t\t\t', '<a href="http://www.diveintopython3.net/">', 'version Python 3', '</a>', '. \n\t\t\t', '<li>', ' La ', '<a href="http://fr.wikipedia.org/wiki/Python_%28langage%29">', 'page', '</a>', ' de Wikip', '&eacute', ';', 'dia contient un bon r', '&eacute', ';', 'sum', '&eacute', ';', '\n\t\t\t', '<li>', ' Introduction au ', '<i>', 'notebook', '</i>', ' ipython ou jupyter : ', '<a href="intro_jupyter.html">', 'html', '</a>', ', ', '<a href="intro_jupyter.ipynb">', 'ipynb', '</a>', '\n\t\t\t', '<ul>', ' Cours 1 (29/09/2017)\n\t\t\t\t', '<li>', ' Python 2 ', '<a href="python_M1_2017-1.html">', 'html', '</a>', ', ', '<a href="python_M1_2017-1.ipynb">', 'ipynb', '</a>', '\n\t\t\t\t', '<li>', ' Python 3 ', '<a href="M1_python3-1.html">', 'html', '</a>', ', ', '<a href="M1_python3-1.ipynb">', 'ipynb', '</a>', ' \n\t\t\t', '</ul>', '\n\t\t\t', '<a href="td1.html">', ' TD 1', '</a>', ' (29/09/2017)\n\t\t\t', '<li>', ' Corrig', '&eacute', ';', ' du TD 1\n\t\t\t', '<ul>', '\n\t\t\t\t', '<li>', '<a href="td1_sol.html">', 'Python 2', '</a>', '\n\t\t\t\t', '<li>', '<a href="td1_sol_3.html">', 'Python 3', '</a>', '\n\t\t\t', '</ul>', '\n\t\t\t', '<a href="script.py.html">', 'Mod', '&egrave', ';', 'le', '</a>', ' de script\n\t\t\t', '<li>', ' Cours 2 (13/10/2017) : ', '<a href="python_M1_2017-2.html">', 'html', '</a>', ', ', '<a href="python_M1_2017-2.ipynb">', 'ipynb', '</a>', '\n\t\t\t', '<li>', '<a href="td2.html">', 'TD 2', '</a>', ' (13/10/2017)\n\t\t\t\n\n\t\t', '</ul>', '\n\t', '</body>', '\n', '</html>', '\n\n\t\t\n']

In [63]:
b.output()
Out[63]:
'<html>\n\t<body>\n\t\t<h1> M1 Informatique : Python </h1>\n\t\t<ul>\n\t\t\t<li> Commencer ici : <a href="http://www.python.org/">python.org</a>\n\t\t\t<li> La <a href="http://docs.python.org/2.7">documentation</a> officielle\n\t\t\t<li> Le <a href="http://www.pythonchallenge.com/"> Python Challenge</a>\n\t\t\t<li> <a href="http://diveintopython.net/"><i>Dive into Python</i></a>  \n\t\t\tpar <a href="http://en.wikipedia.org/wiki/Mark_Pilgrim_%28software_developer%29">Mark Pilgrim</a> (un livre recommand&eacute;);\n\t\t\t<a href="http://www.diveintopython3.net/">version Python 3</a>. \n\t\t\t<li> La <a href="http://fr.wikipedia.org/wiki/Python_%28langage%29">page</a> de Wikip&eacute;dia contient un bon r&eacute;sum&eacute;\n\t\t\t<li> Introduction au <i>notebook</i> ipython ou jupyter : <a href="intro_jupyter.html">html</a>, <a href="intro_jupyter.ipynb">ipynb</a>\n\t\t\t<ul> Cours 1 (29/09/2017)\n\t\t\t\t<li> Python 2 <a href="python_M1_2017-1.html">html</a>, <a href="python_M1_2017-1.ipynb">ipynb</a>\n\t\t\t\t<li> Python 3 <a href="M1_python3-1.html">html</a>, <a href="M1_python3-1.ipynb">ipynb</a> \n\t\t\t</ul>\n\t\t\t<a href="td1.html"> TD 1</a> (29/09/2017)\n\t\t\t<li> Corrig&eacute; du TD 1\n\t\t\t<ul>\n\t\t\t\t<li><a href="td1_sol.html">Python 2</a>\n\t\t\t\t<li><a href="td1_sol_3.html">Python 3</a>\n\t\t\t</ul>\n\t\t\t<a href="script.py.html">Mod&egrave;le</a> de script\n\t\t\t<li> Cours 2 (13/10/2017) : <a href="python_M1_2017-2.html">html</a>, <a href="python_M1_2017-2.ipynb">ipynb</a>\n\t\t\t<li><a href="td2.html">TD 2</a> (13/10/2017)\n\t\t\t\n\n\t\t</ul>\n\t</body>\n</html>\n\n\t\t\n'

Commentaires sur la syntaxe

Ce code utilise quelques astuces typiquement pythonesques.

locals() et globals() renvoient des dictionnaires de variables locales et globales ...

In [65]:
def f(x):
    y = 'toto'
    print locals()
In [66]:
f(8)
{'y': 'toto', 'x': 8}

Si on venait de lancer l'interpréteur, on aurait

>>> print globals()
{'f': <function f at 0x402d35a4>, '__builtins__': 
<module '__builtin__' (built-in)>, '__name__': '__main__', 
'__doc__': None}
>>> dir()
['__builtins__', '__doc__', '__name__', 'f']

Mais dans un notebook jupyter, on en a beaoucoup trop pour pouvoir les afficher ...

Les lignes

if __name__ == "__main__":
    for k, v in globals().items():
        print k, "=", v

ajoutées à la fin du fichier BaseHTMLProcessor.py décrit dans le livre produiraient l'effet suivant, quand le programme est lancé en ligne de commande :

$ python BaseHTMLProcessor.py
__copyright__ = Copyright (c) 2001 Mark Pilgrim
HTMLParser = HTMLParser.HTMLParser
__license__ = Python
__builtins__ = <module '__builtin__' (built-in)>
__file__ = BaseHTMLProcessor.py  [ ... snip ...]

Rappel : formatage par dictionnaire

In [70]:
d = {'animal':'cheval', 'parent':'cousin', 'aliment':'foin', 'jour':'dimanche'}
s = 'Le %(animal)s de mon %(parent)s ne mange du %(aliment)s que le %(jour)s'

s % d
Out[70]:
'Le cheval de mon cousin ne mange du foin que le dimanche'

On peut donc utiliser locals() pour remettre en place les attributs des balises sans avoir à les connaître :

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

C'est ce procédé qui permet de reconstituer (essentiellement) le HTML qu'on ne souhaite pas modifier.

Pourquoi essentiellement ? A cause des "guillemets":

In [74]:
htmlSource = """        
    <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>"""


parser = BaseHTMLProcessor()
parser.feed(htmlSource) 
parser.close()
print parser.output()   
        
    <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>

Le dialectiseur

On construit ensuite une classe Dialectizer qui dérive de BaseHTMLProcessor. Son rôle est de "traduire" le texte de la page, sauf lorsqu'il doit être rendu verbatim (<pre>...</pre>).

Il lui faudra donc un attribut verbatim qui permet de savoir si l'on doit traduire ou pas :

Ceci étant acquis, on peut maintenant surcharger handle_data :

def handle_data(self, text):                                         
    self.pieces.append(self.verbatim 
                       and text 
                       or self.process(text))

La méthode process dépendra de la traduction désirée.

On remarquera l'usage astucieux des booléens

>>> (1==1) and 'toto'
'toto'
>>> (1==0) and 'toto'
False
>>> (1==0) or 'toto'
'toto'
>>> (1==0) and 'toto' or 'titi'
'titi'

Explication (attention à l'ordre !) :

>>> 'toto' and (1==1)
True
>>> 'toto' and (1==0)
False
>>>

La sémantique est

x or y   -->  if x is false, then y, else x
x and y  -->  if x is false, then x, else y

Brouilleur de page web : le code complet

Il faut un module de traduction, par exemple celui-ci :

# module touille.py
import random, re

p = re.compile('(\w)(\w\w+)(\w)', re.M|re.L|re.U)

def touille(m):
    milieu = list(m.group(2))
    random.shuffle(milieu)
    return m.group(1) + ''.join(milieu) + m.group(3)


def blurr(s):
    return p.sub(touille,s)

On suppose que la classe BaseHTMLProcessor codée plus haut (et différente de celle du livre) se trouve dans un module du même nom.

Le programme original (en Python 1) utilisait SGMLParser. Il ne complétait pas les liens et ne récupérait pas les images.

#!/usr/bin/env python
"""Web page blurrer for Python

This program is adapted from "Dive Into Python", a free Python book for
experienced programmers.  Visit http://diveintopython.org/ for the
latest version.

New version using HTMLParser and including images.
"""

__author__ = "Mark Pilgrim (mark@diveintopython.org)"
__updated_by__ = "Jean-Yves Thibon"
__version__ = "$Revision: 1.3 $"
__date__ = "$Date: 2009/02/12 $"
__copyright__ = "Copyright (c) 2001 Mark Pilgrim"
__license__ = "Python"

import re

from BaseHTMLProcessor import BaseHTMLProcessor

from touille import blurr
import codecs
from urlparse import urljoin

class Dialectizer(BaseHTMLProcessor):
    subs = ()
    def __init__(self,root_url=None):# added root_url
        BaseHTMLProcessor.__init__(self)
        self.url = root_url

    def __compl(self, x):# new: url completion; x is a pair (key, value)
        if x[0] == 'href' or x[0] == 'src':
            return (x[0], urljoin(self.url,x[1]))
        else: return x

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

    def handle_starttag(self,tag,args):
        if self.url:
            args = [self.__compl(x) for x in args]
        if tag in ["pre","script","style"]: self.verbatim += 1
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in args])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def handle_endtag(self,tag):
        # called for every </pre> tag in HTML source
        # Decrement verbatim mode count
        if tag in ["pre","script","style"]: self.verbatim -= 1
        self.pieces.append("</%(tag)s>" % locals())

    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

        text = blurr(text)
        return text


def translate(url):
    """fetch URL and blurr"""
    from urllib import urlopen
    html = urlopen(url).read()
    s = html.decode('utf8') # On suppose pour simplifier la page en utf8 ici
    parser = Dialectizer(url)#test
    parser.feed(s)
    parser.close()
    return parser.output()

def test(url):
    """test  against URL"""

    outfile = "touillage.html"
    f = codecs.open(outfile, "wb",encoding='UTF-8')
    f.write(translate(url))
    f.close()
    import webbrowser # pour visuliser le résultat
    K = webbrowser.Konqueror()
    webbrowser.register('konqueror',None,K) # par exemple

    K.open_new(outfile)

if __name__ == "__main__":
    s = test("http://igm.univ-mlv.fr/~jyt/")
In []: