Aperçu de la CGI avec Python

Mis à jour le jeudi 31 octobre 2013

Vous aimeriez rendre votre site interactif, par exemple pour mettre en place un compteur de visites, un formulaire de contact, un petit jeu...
Pour ça, tout le monde utilise le PHP. Mais vous faites partie d'une élite éclairée et vous aimeriez le faire avec Python. C'est faisable !
Voici un aperçu de la CGI avec ce langage.

Juste une chose avant de commencer, si vous n'avez pas d'hébergeur qui vous permette d'utiliser Python sur vos pages web, regardez donc cette liste. Si vous hébergez votre site vous-même, alors sachez qu'il vous suffit d'installer Apache et Python.

Préparation

Pour que vos scripts soient exploitables, il faut tout d'abord configurer Apache.
Créons un fichier .htaccess, que vous placerez à la racine du site (en effet, ce fichier fonctionne récursivement : tous les répertoires situés dans le répertoire contenant votre .htaccess seront assujettis aux règles que vous définirez dans celui-ci). Le voici :

AddHandler cgi-script .py
Options +ExecCGI

Ces deux lignes indiquent que les fichiers .py doivent être interprétés avant d'être envoyés au client. Le comportement par défaut serait d'envoyer le fichier comme s'il s'agissait d'un fichier texte ordinaire.
Une bonne chose à faire est de configurer Apache pour considérer vos fichiers index.py comme les indexes de vos répertoires. Pour cela, vous pouvez ajouter la ligne suivante à votre .htaccess :

DirectoryIndex index.py

Néanmoins, il reste une chose très importante à faire : rendre votre script exécutable. Je dois reconnaître que je me suis souvent fait avoir par ce genre d'erreur. C'est assez pervers car vous ne savez pas pourquoi votre script échoue : vous vous retrouvez face à une erreur 500 fort peu explicite.
Pour ce faire, sous UNIX, il faut utiliser la commande suivante :

chmod +x fichier.py

Vous devez le faire sur chacun de vos scripts.

Maintenant, nous allons voir comment rédiger nos fichiers Python.

La rédaction des scripts

Lorsque vous utilisez Python pour votre site web, vous générez du HTML.
Il vous faut le préciser : vous pourriez très bien renvoyer une image, aussi faut-il informer le client de la nature du document renvoyé.
Pour ce faire, il faut tout simplement utiliser print :

#!/usr/bin/python

print 'Content-type: text/html'
print 
# Là commence votre code.

La première ligne est importante : il s'agit du shebang. Il indique quel interpréteur Python utiliser.
La seconde (en réalité, la troisième car j'ai sauté une ligne) indique le type de document envoyé.
La dernière écrit une ligne vide. En effet, le client et le serveur communiquent par le protocole HTTP. Ce dernier est composé d'un en-tête, dans lequel des informations comme le nom et la version du client, ceux du serveur, le poids du fichier, l'état de la requête (vous savez, 200 OK, 404 Not Found, 500 Internal Server Error...), etc., et du corps de la requête, qui contient, dans le cas d'une requête du serveur vers le client, le fichier envoyé. Ces deux blocs (en-tête et corps) sont séparés par une ligne vide. Pour plus d'informations, regardez donc l'article sur Wikipedia.

Le type de fichier (que j'ai spécifié à la deuxième ligne avec 'Content-type: text/html'), fait partie de l'en-tête de la requête. Or, la page que vous souhaitez envoyer se trouve dans le corps. C'est pourquoi vous devez les séparer à l'aide de cette ligne vide.

Votre script est maintenant prêt. Vous pouvez désormais écrire votre code HTML, toujours à l'aide de print :

#!/usr/bin/python

print 'Content-type: text/html'
print
print '<html><head><title>...'

Vous pouvez, plutôt que de vous esquinter à écrire ligne par ligne :

print '<html>'
print '<head>'
print '<title>Mon super site en Python qui powne tout XdXDxDXDXd</title>'
print '</head>'
print '<body>'
# ... Je vous épargne la suite de cette horreur.

Utiliser la syntaxe suivante, qui n'est pas toujours connue :

print '''
<html>
     <head>
          <title>Mon super site en Python qui powne tout XdXDxDXDXd</title>
     </head>
     <body>
          <p>Vous êtes jaloux, hein ?</p>
     </body>
</html>
'''

Vous constatez qu'à l'intérieur des ''', vous pouvez utilisez l'indentation que vous voulez.
Il est bien entendu important de rendre son site conforme aux normes de la W3C.

Si vous avez eu l'occasion de faire une erreur dans votre code, vous avez sûrement pu admirer une erreur 500. Et, vous l'avez remarqué, c'est très gênant pour le dépistage d'erreurs. Le module cgitb va pouvoir vous aider.
Pour cela, il faut l'importer, puis l'activer :

import cgitb
cgitb.enable()

C'est fait ! Maintenant, les erreurs s'afficheront sur la page. Ces deux lignes vous seront certainement utiles !

Mise en pratique : un compteur de visites

Maintenant que vous connaissez le strict minimum, pourquoi ne pas l'employer ? Nous allons réaliser un compteur de visites.
Il serait judicieux de tenter de le faire seul. Néanmoins, je vous donne ma méthode.
Tout d'abord, je tente d'ouvrir un fichier en mode lecture ('r' pour read). Je stocke son contenu dans la variable nbr_visiteurs, en le transformant en un nombre (car il s'agit pour l'instant d'une chaîne). Si cela ne marche pas (pour la bonne raison que mon fichier n'existe pas, ou que son contenu n'est pas un nombre), alors nbr_visiteurs = 0.
Cela donne :

try: # Pour essayer le code qui suit. Si ledit code ne fonctionne pas, alors except: est appelé. Autrement, on continue.
    fichier = open('compteur','r') # Lecture dans le fichier, appelé 'compteur'.
    nbr_visiteurs = int(fichier.read()) # int() transforme son argument (à savoir, le contenu du fichier retourné par fichier.read()) en un nombre.
except Exception:
    nbr_visiteurs = 0 # Si le fichier est inexistant (= page jamais visitée) ou incorrect, on repart de 0.

Maintenant, nous allons ouvrir le fichier avec l'option 'w' pour write. S'il existe, il sera supprimé et recréé vide. Sinon, il sera tout simplement créé. Dans les deux cas, nous n'aurons plus qu'à écrire dedans la valeur nbr_visiteurs + 1 (car il faut compter le visiteur qui charge la page :-) ), en la transformant en une chaîne avec la fonction str().

fichier = open('compteur','w')
fichier.write(str(nbr_visiteurs+1))

Finalement, on indique au visiteur le nombre de visites :

print nbr_visiteurs+1,'visites \o/'

Ce qui, avec le code minimal, donne :

#!/usr/bin/python

print 'Content-type: text/html'
print

try:
    fichier = open('compteur','r')
    nbr_visiteurs = int(fichier.read())
except Exception:
    nbr_visiteurs = 0
    fichier = open('compteur','w')
fichier.write(str(nbr_visiteurs+1))
print nbr_visiteurs+1,'visites \o/'

Traiter un formulaire

Un des points inévitables en CGI est le traitement de formulaires. En Python, c'est aussi faisable.

Prenons un cas concret. Si le visiteur entre le code suivant :

<script type="text/javascript">alert('OLOL CMT G T PWNED');</script>

Une alerte s'affichera sur l'écran du visiteur ! Et rien ne l'empêche de se faire passer pour vous « Votre session va expirer. Entrez votre mot de passe à nouveau. », de le récupérer puis de se le faire envoyer par mail, le tout en JavaScript !
Pour pallier ce problème, nous allons utiliser une fonction fort pratique : cgi.escape(). C'est un peu l'équivalent de htmlspecialchars() en PHP : elle remplace les caractères < et > par &amp;lt; et &amp;gt;. De ce fait, le code JavaScript cité plus haut s'affichera tel quel, au lieu d'être exécuté. Notez que si vous utilisez une version de python supérieure à la 3.2 il vous faudra utiliser html.escape() à la place de cgi.escape(), car cette dernière est dépréciée.
Aussi, prenez l'habitude de sécuriser toutes les données envoyées par le visiteur à l'aide de cette fonction.

Pour traiter un formulaire, nous devons tout d'abord importer le module CGI. Vous savez comment faire :

import cgi

À partir de là, ça se corse. :-° On va créer une instance de la classe cgi.FieldStorage(), qui contiendra le formulaire envoyé. Puis, on pourra en extraire les informations avec instance.getvalue('nom'). Cela vous semble tordu ? En vérité, c'est très simple.

#!/usr/bin/python

import cgi

print 'Content-type: text/html'
print
formulaire = cgi.FieldStorage()
if formulaire.getvalue('nom') == None:
    print '''
Veuillez remplir le formulaire : 
<form action="formulaire.py" method="post">
<input type="text" name="nom" />
<input type="submit"></form>
    '''
else:
    print 'Ainsi, vous vous appelez',cgi.escape(formulaire.getvalue('nom')),' ?' # N'oubliez pas de sécuriser le code !

Ce code, enregistré dans un fichier formulaire.py (autrement, il vous faudra le modifier, car le formulaire renvoie vers formulaire.py), créera une instance de cgi.FieldStorage() dans la variable formulaire.
Si formulaire.getvalue('nom') == None, alors le champ 'nom' était vide, voire non envoyé.
On présente donc le formulaire au client. Autrement, on récupère la valeur du champ 'nom' avec formulaire.getvalue('nom'), et on l'affiche tout en prenant soin de le sécuriser avec cgi.escape().
Si le champ s'était appelé 'prenom', vous comprenez bien que l'on aurait récupéré sa valeur avec formulaire.getvalue('prenom').

Vous voulez une bonne nouvelle ? Pour un formulaire envoyé avec GET (c'est-à-dire que les valeurs des champs se retrouvent dans l'URL, dans le style page.py?var=valeur&amp;var2=valeur2), c'est exactement pareil.
Ainsi, le script suivant enregistré sous essai.py et appelé avec essai.py?var=1&amp;var2=2 affichera 'Var = 1 et Var2 = 2'.

#!/usr/bin/python

import cgi

print 'Content-type: text/html'
print

form = cgi.FieldStorage()
print 'Var = ',cgi.escape(form.getvalue('var')),' et Var2 = ',cgi.escape(form.getvalue('var2'))

Mise en pratique : un livre d'or

Le livre d'or est une des fonctionnalités que l'on se plaît à implémenter sur son site. Grâce aux formulaires et à la manipulation de fichiers, c'est tout à fait faisable en Python.
Nous allons tout d'abord créer un formulaire tout simple, puis le traiter. Ensuite, nous verrons comment lire le fichier dans lequel seront enregistrés les messages.
Le formulaire ressemblera à ça :

<form action="enregistrement.py" method="post">
  <p>Vous avez une remarque, un commentaire, un conseil ? Signez le livre d'or !</p>
  <p>
    Pseudonyme : <input type="text" name="pseudo" /><br />
    Site web : <input type="text" name="site" /><br />
    Message :<br />
    <textarea name="message" cols="20" rows="2"></textarea><br />
    <input type="submit" />
  </p>
</form>

À vous de le faire correspondre à vos besoins.
Pour la gestion du formulaire :

#!/usr/bin/python

import cgi

print 'Content-type: text/html'
print

print '<html><head><title>Mon super site</title></head><body>'
formulaire = cgi.FieldStorage()
if formulaire.getvalue('message') != None:
    print '<p>Merci d\'avoir particip&amp;eacute; au livre d\'or. Vous pouvez le visiter <a href="livreor.py">ici</a>.</p>'
    message = formulaire.getvalue('message')
    site = formulaire.getvalue('site')
    pseudo = formulaire.getvalue('pseudo')
    try:
        fichier = open('livreor','r')
        livreor = fichier.read()
    except IOError:
        livreor = ''
    message_poste = '<message><auteur>'+cgi.escape(pseudo)+'</auteur><site>'+cgi.escape(site)+'</site><contenu>'+cgi.escape(message)+'</contenu></message>\n'
    fichier = open('livreor','w')
    fichier.write(message_poste+livreor)
else:
    print '''Erreur : vous n'avez pas rempli le formulaire.'''
print '</body></html>'

C'est tout simple :

  • on charge le formulaire dans la variable formulaire avec cgi.FieldStorage ;

  • on teste si le formulaire a été rempli, en vérifiant qu'un message est présent. Vous pouvez bien sûr vérifier si tous les champs sont remplis ;

  • on charge les éléments du formulaire dans des variables ;

  • on essaie de lire le contenu du fichier livreor. Si cela rate (avec une IOError), c'est que le fichier n'existe pas, auquel cas le contenu de livreor sera une chaîne vide ;

  • on ouvre le fichier livreor en mode écriture. On écrit dedans le contenu du nouveau message.

L'écriture est terminée. Chaque message est écrit dans le format :

<message><auteur>Auteur</auteur><site>Site</site><contenu>Contenu</contenu></message>

Nous n'avons plus qu'à le lire !

#!/usr/bin/python

def trouver(chaine,chaine1,chaine2):
    position_chaine1 = chaine.index(chaine1) + len(chaine1)
    position_chaine2 = chaine.index(chaine2,position_chaine1)
    return chaine[position_chaine1:position_chaine2]

def isoler_message(chaine):
    dico = {}
    try:
        message = trouver(chaine,'<message>','</message>')
        dico['auteur'] = trouver(message,'<auteur>','</auteur>')
        dico['site'] = trouver(message,'<site>','</site>')
        dico['contenu'] = trouver(message,'<contenu>','</contenu>')
        return dico
    except Exception:
        return 0

def supprimer(chaine):
    return chaine.replace('<message>'+trouver(chaine,'<message>','</message>')+'</message>','')

print 'Content-type: text/html'
print 
print '''
<html>
<head>
<title>Le livre d'or</title>
</head>
<body>
'''
try:
    fichier = open('livreor','r')
    contenu = fichier.read()
    continuer = True
    while continuer:
        message = isoler_message(contenu)
        if not message:
            continuer = False
        else:
            print '<p>De : <b><a href="',message['site'],'">',message['auteur'],'</a></b></p>'
            print '<p>',message['contenu'],'</p>'
        print '<br /><br />'
        contenu = supprimer(contenu)
except Exception:
    print 'Il n\'y a aucun message dans le livre d\'or'
print '</body></html>'

Ma première fonction, trouver(), retourne la première chaîne trouvée entre chaine1 et chaine2 dans chaine. Son fonctionnement est simple, lisez le code si vous souhaitez le comprendre.

La seconde, isoler_message(), utilise trouver() pour trouver le premier message (la première chaîne entre <message> et </message>), puis extrait les différentes informations (l'auteur, le site et le contenu). Finalement, elle retourne le dictionnaire dans lequel elle a tout enregistré.

La troisième, supprimer(chaine), supprime le premier message (le texte entre <message> et </message>, ainsi que les deux balises), pour que la lecture puisse être recommencée sur le message suivant.

On commence par afficher le début du HTML de la page. Puis, on essaie de lire le fichier livreor, puis d'isoler le premier message, de l'afficher, et de le supprimer de la liste, avant de recommencer, jusqu'à ce qu'il n'y ait plus de messages.
Si cela ne fonctionne pas, on affiche qu'il n'y a pas de message dans le livre d'or.
C'est tout à fait rudimentaire. À vous d'ajouter ce que vous voulez : datez les messages, faites des pages lors de l'affichage...

Vous voyez que, en travaillant des fichiers et en traitant des formulaires, vous pouvez déjà aller loin. Légèrement modifié, ce livre d'or peut vous fournir un système de news.

Afficher son code source

Une dernière astuce dont j'aimerais vous faire part est l'affichage de la source. En effet, il est agréable de pouvoir avoir le code d'un site web sous les yeux, pour pouvoir s'en inspirer, ou même donner des conseils au webmaster.

Pour afficher la source, le client n'aura qu'à entrer l'URL de la page ainsi :
http://site.web/index.py?source=1
Il nous faudra donc utiliser cgi.FieldStorage(), et tester si source=1. Dans ce cas, on lit le fichier, et on l'affiche entre <pre> et </pre>, pour que le code HTML et les sauts à la ligne apparaissent. Puis, on s'arrête là avec sys.exit(0).
Autrement, on envoie la page comme d'habitude.

Voici mon code :

#!/usr/bin/python

import sys
import cgi

form = cgi.FieldStorage()
if form.getvalue('source') == '1':
    print """Content-type: text/plain
"""
    print open('livreor.py').read()
    sys.exit(0)


print """Content-type: text/html
"""
# Suite du code

C'est enfantin mais pratique. ^^

Ce tutoriel n'est ici que pour solliciter votre enthousiasme. Pour pouvoir mettre en place un site web plus complexe, utilisant une base de données par exemple, vous devez vous tourner vers la documentation.
J'espère vous avoir intéressés et donné des idées.
Bonne continuation avec Python !
Merci à delroth et plus généralement à tout #python pour l'aide qui m'a été apportée lors de la création de mon propre site, et sans qui ce tutoriel n'aurait pas vu le jour.

déroulement d'un cours

  • 1

    Dès aujourd'hui, vous avez accès au contenu pédagogique et aux exercices du cours.

  • 2

    Vous progressez dans le cours semaine par semaine. Une partie du cours correspond à une semaine de travail de votre part.

  • !

    Les exercices doivent être réalisés en une semaine. La date limite vous sera annoncée au démarrage de chaque nouvelle partie. Les exercices sont indispensables pour obtenir votre certification.

  • 3

    À l'issue du cours, vous recevrez vos résultats par e-mail. Votre certificat de réussite vous sera également transmis si vous êtes membre Premium et que vous avez au moins 70% de bonnes réponses.

L'auteur

Exemple de certificat de réussite
Exemple de certificat de réussite