Initiation Python

Joël Maïzi, LIRMM, http://www.lirmm.fr/~maizi/InPy

Objectifs de la séance

  • Utiliser la librairie standard Python.
  • Acquérir les bons réflexes pour développer en Python.

Références (en anglais) :

Presenter Notes

Python et IPython en mode interactif

$ python
Python 3.4.0 (default, Apr 11 2014, 13:05:11)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

IPython offers a combination of convenient shell features, special commands and a history mechanism for both input (command history) and output (results caching, similar to Mathematica). It is intended to be a fully compatible replacement for the standard Python interpreter, while offering vastly improved functionality and flexibility.

$ ipython
Python 3.4.0 (default, Apr 11 2014, 13:05:11)
Type "copyright", "credits" or "license" for more information.

IPython 1.2.1 -- An enhanced Interactive Python.

In [1]:

Cette présentation sera interactive.

Presenter Notes

Un exemple de module

Le matériel de cette présentation vous est fourni par l'intermédiaire d'un fichier tar compressé.

  • Vous allez tout d'abord vérifier que vous disposez des modules tarfile et gzip.

tarfile est un module de la librairie standard Python. Son code (il est écrit en Python) est directement accessible à partir de la page de documentation.

reads and writes gzip, bz2 and lzma compressed archives if the respective modules are available

extraction du fichier du TP avec la commande :

python -m tarfile -e  TPpython_maizi.tar.gz

Presenter Notes

Les différences entre Python 2 et 3

Vous serez sans doute amenés à travailler avec les deux versions !

Presenter Notes

(2 != 3) == True

PEP 3000

Python 3.0 will break backwards compatibility with Python 2.x.

Vous pouvez avoir besoin de spécifier la version de Python que vous utilisez pour exécuter le script :

#!/usr/bin/python3
#!/usr/bin/python2.6

L'écriture #!/usr/bin/python référence "en dur" un interpréteur Python (version 2 ou 3). En revanche, #!/usr/bin/env python référence la première commande python trouvée dans votre PATH ou dans le PATH de celui qui exécute le script.

Vous pouvez utiliser le python défini par votre environnement (Unix) :

#!/usr/bin/env python

Presenter Notes

(2 != 3) == True and "prise en charge"

Comment prendre en charge les différences entre les version 2 et 3 de Python. Le module sys

import sys

if sys.version_info.major < 3:
    # faire quelque chose pour adapter le code Python 2

Par exemple :

if sys.version_info.major < 3:
    input = raw_input # pas vraiment vrai
else:
    unicode = str # quelques petites différences ici aussi

Ou ne pas prendre en charge les différences :

if sys.version_info.major < 3:
    print("Désolé ! Ce programme nécessite une version 3.x de Python.")
    sys.exit()

Presenter Notes

(2 != 3) == True and "l'usage de env"

Par défaut, sous Linux, python référence (aujourd'hui) Python 2.7. Vous pourriez être amené à développer du code qui devra fonctionner indiféremment sous 2.7 et 3+.

Rajoutez la ligne suivante dans votre .profile :

if [ -d "$HOME/bin" ] ; then ; PATH="$HOME/bin:$PATH" ; fi

Créez le répertoire bin

mkdir ~/bin

Créez dans ce répertoire un lien vers l'interpréteur Python avec lequel vous voulez tester votre code :

ln -s /usr/bin/python3 ~/bin/python
hash -r
# remplacer python3 par python2 pour tester avec Python2

Commencez tous vos scripts par :

#!/usr/bin/env python

Presenter Notes

(3 > 2) != False and "améliorations mais..."

Rupture de compatibilité pour apporter des améliorations :

... The most drastic improvement is the better Unicode support (with all text strings being Unicode by default) as well as saner bytes/Unicode separation.

le PEP 358, (Python Enhancement Proposal), décrit la proposition de mise en place du type bytes dans python 2.6.

Mais :

There are a few minor downsides, such as slightly worse library support1 and the fact that most current Linux distributions and Macs are still using 2.x as default

Python 3 Wall of Superpowers, il y a encore quelques temps nommé «Python 3 Wall of shame».

Il reste tout de même des packages qui ne sont pas et en seront sans doute pas portés en Python 3. Si vous voulez utiliser ces packages, vous devrez alors utiliser Python 2.

Presenter Notes

(2 > 3) == False and "unicode (python2)"

UTF-8 (U from Universal Character Set + Transformation Format 8-bit)

$ python2
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
>>> "chaîne"
'cha\xc3\xaene'
>>> "chaîne" == bytes("chaîne")
True
>>> str == bytes
True
>>> "chaîne".decode("utf-8") == unicode("chaîne", "utf-8")
True
>>> "chaîne" == unicode("chaîne", "utf-8").encode("utf-8")
True
  • On décode du bytes (donc une chaîne de caractères) pour obtenir de l'unicode.
  • On encode de l'unicode pour obtenir du bytes.
  • Quelle est la longueur de la chaîne "chaîne" ?
  • Observez dans un interpréteur Python 2 le résultat de

    print([c for c in "chaîne"])
    

Presenter Notes

(3 > 2) == True and "str (python3)"

$ python3
Python 3.4.0 (default, Apr 11 2014, 13:05:11)
>>> "chaîne"
'chaîne'
>>> "chaîne".encode()
b'cha\xc3\xaene'
>>> "chaîne".encode() == bytes("chaîne", "utf-8")
True
>>> "chaîne" == str("chaîne".encode("utf-16"), "utf-16")
True
>>> "chaîne" == bytes("chaîne", "utf-8").decode()
True
>>> "chaîne" == "chaîne".encode().decode()
True

Presenter Notes

Scripts vs. modules

réf. : Scripts, modules

#!/usr/bin/env python
#-*- coding: utf-8 -*-

"""
affiche_kek_chose:
usage :
* python affiche_kek_chose <kek chose>
* ./affiche_kek... (vous n'avez pas oublié chmod +x aff...)
"""

import sys

def afficher(qui, quoi):
    """Affiche qui affiche quoi..."""
    print('{} affiche : "{}"'.format(qui, quoi))

afficher(sys.argv[0], sys.argv[1])
  • exécutez le script affiche_kek_chose.py dans une console
  • lancez l'interpréteur Python et importez affiche_kek_chose (vous devez vous trouver dans le répertoire contenant le script avant de lancer l'intepréteur)

Presenter Notes

Ce script n'est pas un module

Manifestement !

In [1]: import affiche_kek_chose
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-1-7432c9ad8fa9> in <module>()
----> 1 import affiche_kek_chose

/home/joel/TPPYTHON/ScriptsVSModules/ex1/affiche_kek_chose.py in <module>()
     15     print('{} affiche : "{}"'.format(qui, quoi))
     16
---> 17 afficher(sys.argv[0], sys.argv[1])

IndexError: list index out of range

Tout se passe comme si on invoquait le script sans argument.

$ ./affiche_kek_chose.py
Traceback (most recent call last):
  File "./affiche_kek_chose.py", line 17, in <module>
    afficher(sys.argv[0], sys.argv[1])
IndexError: list index out of range

Presenter Notes

Transformer un script en module

  • trouvez un moyen simple (pas plus d'un caractère) pour permettre l'import du module affiche_kek_chose.
  • écrivez un script qui utilise le module affiche_kek_chose.
  • modifiez affiche_kek_chose pour afficher la variable __name__.
    Lancez le script puis importez le module,

A module can discover whether or not it is running in the main scope by checking its own __name__.

  • Rétablissez le fonctionnement du module comme script tout en conservant celui du module (la valeur de la variable __name__ est l'élément discriminent).

exemples d'usage : le code du module fileinput, le code du module string, le code du module webbrowser.

Presenter Notes

if __name__ == "__main__":

A module is a file containing Python definitions and statements. The file name is the module name with the suffix .py appended. Within a module, the module's name (as a string) is available as the value of the global variable name

Pour un script la variable __name__ est égale à la chaîne "__main__".

if __name__ == '__main__':
    # le code à exécuter en cas d'appel direct du module (script)

Presenter Notes

Amélioration du module

  • que se passe t'il si vous lancez la commande suivante :

    python affiche_kek_chose.py kek chose

  • modifiez le programme pour obtenir :

    affiche_kek_chose affiche : "kek chose"

(réf. Arbitrary Argument Lists)

Presenter Notes

Les paramètres des fonctions

Keyword arguments :

def ma_fonction(param1, param2=None, param3='truc'):
    a = param1
    if param2:
        # faire quelque chose
    if param3 == 'turc':
        # le truc
    else:
        # pas le truc

ma_fonction('un', param3='machin')

Une forme souvent rencontrée :

def ma_fonction(param1, *args, **kwargs):
    for arg in args:
        # faire quelque chose
    for key, value in kwargs.items():
        # faire autre chose

ma_fonction('un', ['deux', 'trois'], quatre=4, cinq='cinq')

Presenter Notes

Juste un mot sur les Packages

Packages are a way of structuring Python’s module namespace by using “dotted module names”. For example, the module name A.B designates a submodule named B in a package named A.

Exemple de package : xml

/usr/lib/python3.4$ tree xml | grep -v pyc
xml
├── __init__.py
├── dom
│   ├── __init__.py
│   ├── domreg.py
│   ├── [...]
├── etree
│   ├── __init__.py
│   ├── cElementTree.py
│   ├── [...]

Notez la présence d'un fichier __init__.py dans chaque répertoire du package.

Presenter Notes

Mauvaise blague (exercice)

Quelqu'un vous a fait une mauvaise blague. Il a saccagé un de vos répertoires en y laissant deux fichiers.

  • un fichier ls contenant la liste des fichiers tels qu'ils étaient à l'origine dans le répertoire :

    -rw-rw-r-- 1 joel joel   20887 sept.  6 11:06 fichierA
    -rw-rw-r-- 1 joel joel     384 sept.  7 10:54 fichierB
    -rw-rw-r-- 1 joel joel    1025 sept. 13 09:22 fichierC
    -rw-rw-r-- 1 joel joel     377 juil.  5 16:48 fichierD
    ...
    
  • un fichier bundle contenant tous les fichiers concaténés.

    | ... fichierA ... | ... fichierB | ... |
    

À vous de jouer pour reconstituer le répertoire.

NB. Il n'y a pas de séparateur entre les fichiers dans le fichier bundle. Les fichiers se trouvent dans le répertoire TPPython/AnaLog.

Presenter Notes

MB : le fichier ls

Il contient le résultat de la commande ls -l faite dans le répertoire avant la suppression des fichiers. Les informations d'une ligne ls -l sont dans l'ordre :

  • rights, nlinks, owner, group, size, month, day, hour, name.

Dans un premier temps vous allez extraire de cette liste les noms et tailles des fichiers. Avec le fichier ls suivant :

-rw-rw-r-- 1 joel joel   20887 sept.  6 11:06 fichierA
-rw-rw-r-- 1 joel joel     384 sept.  7 10:54 fichierB
-rw-rw-r-- 1 joel joel    1025 sept. 13 09:22 fichierC
-rw-rw-r-- 1 joel joel     377 juil.  5 16:48 fichierD

le résultat devra être :

fichierA : 20887
fichierB : 384
fichierC : 1025
fichierD : 377

À utiliser :

Presenter Notes

MB : le fichier ls (lecture)

#!/usr/bin/env python
#-*- coding: utf-8 -*-

"""
Lecture du fichier ls
"""

with open('ls') as f:
    for line in f:
        ls_list = line.split()
        print("{}: {}".format(ls_list[8], ls_list[4]))

vs.

f = open('ls')
for line in f:
    [...]
f.close()

En cas de plantage dans la boucle for, la première version fermera correctement le fichier contrairement à la seconde.

Presenter Notes

MB : introduction de la classe Ls

class Ls:
    """Ls(<ls -l line>)

    découpe la ligne et associe chaque élément à l'attribut portant son nom
    """
    def __init__(self, ls_line):
        ls_list = ls_line.split()
        self.rights = ls_list[0]
        self.nlinks = ls_list[1]
        self.owner = ls_list[2]
        self.group = ls_list[3]
        self.size = ls_list[4]
        self.month = ls_list[5]
        self.day = ls_list[6]
        self.hour = ls_list[7]
        self.name = ls_list[8]

Ce qui permet l'écriture d'une boucle plus lisible

with open('ls') as f:
    for ls_line in f:
        ls = Ls(ls_line)
        print("{}: {}".format(ls.name, ls.size))

Presenter Notes

MB : traitement du fichier bundle

class Bundle:
    def __init__(self, directory):
        self.__dir = directory
        with open('{}/ls'.format(self.__dir)) as f:
            self.__lss = [Ls(ls_line) for ls_line in f]
        self.__filenames = [ls.name for ls in self.__lss]

    def list(self):
        for ls in self.__lss:
            print("{}: {}".format(ls.name, ls.size))

if __name__ == '__main__':
    bundle = Bundle(sys.argv[1]) # argv[1] est le répertoire
    bundle.list()
  • Un mot sur les listes de compréhension.
  • Prendre en charge le cas où la lecture du fichier se passe mal.
  • Écrire la méthode __index(self, filename) qui retourne la position du fichier portant le nom filename dans le fichier bundle.
  • Écrire la méthode __offset(self, filename) qui retourne la position du premier caractère du fichier filename dans le fichier bundle.
  • Écrire la méthode __content(self, filename) qui retourne le contenu du fichier filename dans le fichier bundle.

Presenter Notes

Comprendre les listes de compréhension

Exemple de construction alléatoire d'une adresse IP :

>>> from random import randint
>>> randint(10,254)
167
>>> [i for i in range(4)]
[0, 1, 2, 3]
>>> [i for i in range(4) if i % 2]
[1, 3]
>>> [randint(10,254) for i in range(4)]
[81, 97, 157, 169]
>>> [str(randint(10,254)) for i in range(4)]
['56', '135', '212', '58']
>>> print(".".join([str(randint(10,254)) for i in range(4)]))
'239.246.133.137'

La dernière ligne est à comparer avec le code suivant sans liste de compréhension :

from random import randint
res = [] # variable intermédiaire qui contiendra la liste
for i in range(4):
    res.append(str(randint(10,254)))
print(".".join(res))
del(res) # suppression de la variable intermédiaire

Presenter Notes

traitement des erreurs (try, except)

  • Prendre en charge le cas où la lecture du fichier se passe mal.

D'après la doc de open(), en cas de problème, une exception OSError est "levée" (depuis la version 3.3 de Python, IOError pour les versions antérieures).

$ ipython

In [1]: from exceptions import OSError
In [2]: ose = OSError()
In [3]: ose.<TAB>
ose.args      ose.errno     ose.filename  ose.message   ose.strerror

Ce qui pourrait donner :

try:
    with open('{}/ls'.format(self.__dir)) as f:
        self.__lss = [Ls(ls_line) for ls_line in f]
except OSError as err:
    print(err)
    sys.exit()

Presenter Notes

MB : Bundle __index()

  • Écrire la méthode __index(self, filename) qui retourne la position du fichier portant le nom filename dans le fichier bundle.

Rappel :

self.__filenames = [ls.name for ls in self.__lss]
  • Que contient self.__filenames

On utilise la méthode list.index()

def __index(self, filename):
    """Retourne l'indice du fichier filename (commence à 0)
    """
    return self.__filenames.index(filename)

Presenter Notes

MB : Bundle __offset()

  • Écrire la méthode __offset(self, filename) qui retourne la position du premier caractère du fichier filename dans le fichier bundle.

    def __offset(self, filename):
        """Retourne la position du premier octet du fichier filename
        """
        offset = 0
        for i in range(self.__index(filename)):
            offset += self.__lss[i].size
        return offset
    
  • Écrire cette méthode en une line : liste de compréhension + sum()

La méthode __size()

def __size(self, filename):
    """Retourne la taille du fichier filename
    """
    return self.__lss[self.__index(filename)].size

Presenter Notes

MB : bundle __content()

  • Écrire la méthode __content(self, filename) qui retourne le contenu du fichier filename dans le fichier bundle. Utiliser seek() et read().

    !python def content(self, filename): """Le contenu retourné est de type "bytes" """ with open('bundle', "rb") as f: offset = self.offset(filename) size = self.__size(filename) f.seek(offset) return f.read(size)

  • essayez le programme avec "r" à la place de "rb". Que se passe-t'il ?

  • que se passerait-il avec python 2 ?
  • Meta-question : Comment retrouver les méthodes seek() et read() à partir de la fonction open() ? Donner deux méthodes.

Presenter Notes

MB : argparse et les arguments

Cf. le howto concernant l'utilisation du module argparse.

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "repertoire", help="Répertoire contenant les fichiers")
    parser.add_argument(
        "fichiers", nargs="*",
        help="Nom du(des) fichier(s) à afficher ou enregistrer")
    parser.add_argument(
        "-w", "--write", help="Restaure le(s) fichier(s)",
        action="store_true")
    args = parser.parse_args()
    directory = args.repertoire
    fichiers = args.fichiers
    write = args.write
    bundle = Bundle(directory)
    if not fichiers:
        bundle.list()
    else:
        for f_name in fichiers:
            if not args.write: bundle.cat(f_name)
            else: bundle.write(f_name)

Presenter Notes

AnaLog (exercice)

Vous trouverez dans le répertoire Analog, un fichier de log apache. Il correspond à quelques heures d'activité du serveur www.jeuxdemots.org.

Objectifs

  • Présentation de l'anonymiseur d'IP.
  • Présentation du module apachelog (décembre 2010)
    • modification du module pour le rendre compatible Python 3
    • Exercice : spécialisation de la classe Parser en surchargeant la méthode alias.
  • Écriture d'un parser efficace
  • Exemple de script pour extraire les erreurs 500
  • Exercice : écriture d'un module permettant de filtrer les logs suivant :
    • la date (jour, heure, minute)
    • l'IP
    • la requête
    • le code du statut de la réponse HTTP

Presenter Notes

AnaLog : un script anonymiseur d'IP

#!/usr/bin/env python
#-*- coding: utf-8 -*-

"""
génère un fichier <nom_fichier>.ano dans lequel les adresses IP
sont anonymisées.
L'adresse IP doit être en début de ligne.
"""

import sys
from random import randint

def get_random_ip():
    """Anonymiseur d'IP"""
    return ".".join((str(randint(10, 254)) for i in range(4)))

def anon_file(filename):
    """Retourne une à une chaque ligne du fichier après avoir
    anonymisé l'IP.
    """
    # le dico des ip anonymisées
    d_aip = {}
    with open(filename) as f:
        for line in f:
            ip, rline = line.strip().split(" ", 1)
            if not ip in d_aip:
                d_aip[ip] = get_random_ip()
            yield "{} {}".format(d_aip[ip], rline)

if __name__ == '__main__':
    filename = sys.argv[1]
    a_filename = "{}.ano".format(filename)
    with open(a_filename, "w") as sf:
        for line in anon_file(filename):
            sf.write("{}\n".format(line))

Presenter Notes

AnaLog le module apachelog

Takes the Apache logging format defined in your httpd.conf and generates a regular expression which is used to a line from the log file and return it as a dictionary with keys corresponding to the fields defined in the log format.

{
'%>s': '200',
'%b': '2607',
'%h': '212.74.15.68',
'%l': '-',
'%r': 'GET /images/previous.png HTTP/1.1',
[...]
}

You can also re-map the field names by subclassing (or re-pointing) the alias method.

  • Utiliser le module apachelog du répertoire Web (rendu compatible Python 3)
  • créer un module my_apachelog.py contenant la classe MyParser héritant de la classe Parser et redéfinissant '%h' en 'host', '%t' en 'time', '%r' en 'request' et '%>s' en 'status'

Presenter Notes

Analog MyParser

  • créer un module my_apachelog.py contenant la classe MyParser héritant de la classe Parser et redéfinissant '%h' en 'host', '%t' en 'time', '%r' en 'request' et '%>s' en 'status'

    from apachelog import Parser, parse_date
    
    parse_date = parse_date
    
    class MyParser(Parser):
        def alias(self, name):
            d_name = {
                '%h':'host', '%t':'time', '%r':'request', '%>s':'status'}
            if name in d_name:
                return d_name[name]
            return name
    

Presenter Notes

AnaLog Apache

  • Écriture d'un parser efficace.

    #!/usr/bin/env python
    #-*- coding: utf-8 -*-
    
    import sys
    from datetime import datetime
    from my_apachelog import MyParser, parse_date
    
    fmt = r'%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"'
    
    def parse_apache_log(fichier, format_):
        p = MyParser(format_)
        with open(fichier) as f:
            for line in f:
                yield p.parse(line)
    
    if __name__ == '__main__':
        parse_apache_log('jdm_access.log', fmt)
    

Observez comme l'exécution est immédiate. La fonction utilise yield ce qui en fait un générateur.

Presenter Notes

Générateurs

En utilisant la même syntaxe que les listes de compréhension, mais avec des parenthèses au lieu des crochets :

>>> g = (i for i in range(4))
>>> type(g)
<class 'generator'>
>>> l = [i for i in range(4)]
>>> type(l)
<class 'list'>
>>> for i in g: print(i)
...
0
1
2
3
>>> for i in g: print(i)
...
>>>

En utilisant yield dans une fonction.

Dans les deux cas Beware the Python generators

Presenter Notes

Analog Apache (generator vs. sequence)

  1. la version originale

    def parse_apache_log(fichier, format):
        p = Parser(format)
        with open(fichier) as f:
            for line in f:
                yield p.parse(line)
    
  2. un peu plus condensée mais équivalente

    def parse_apache_log(fichier, format):
        p = Parser(format)
        return (p.parse(line) for line in open(fichier))
    
  3. très semblable à la précédente, mais tellement différente...

    def parse_apache_log(fichier, format)
        p = Parser(format)
        return [p.parse(line) for line in open(fichier)]
    

Presenter Notes

AnaLog (Internal server Error)

  • Afficher les lignes ayant statut dont le code est 500.

    import sys
    from analog1 import parse_apache_log, fmt
    
    """
    Exemple d'usage : Afficher les lignes ayant un statut dont le code est
    500 (Internal Server Error)
    """
    
    for line in parse_apache_log('jdm_access.log', fmt):
        if line['status'] == '500':
            print(
                line['host'], line['time'], line['request'], line['status'])
    
  • Transformer ce script en module

Presenter Notes

AnaLog le module

Exercice

Ecrivez un module permettant de filtrer les logs suivant :

  • la date (jour, heure, minute)
  • l'IP
  • la requête
  • le code du statut de la réponse HTTP

Le module devra proposer les méthodes :

  • get_by_status(logfile, status)
  • get_by_host(logfile, host)
  • get_by_request(logfile, request)
  • get_by_datetime(logfile, datetime)
  • get_by_min(logfile, datetime)
  • get_by_hour(logfile, datetime)
  • get_by_day(logfile, datetime)

Presenter Notes

Analog le module (solution)

from analog1 import parse_apache_log, fmt

def get_by_strict_match(logfile, string, field):
    for line in parse_apache_log(logfile, fmt):
        if line[field] == string:
            yield line

def get_by_status(logfile, status):
    return get_by_strict_match(logfile, status, 'status')

def get_by_host(logfile, host):
    return get_by_strict_match(logfile, host, 'host')

Presenter Notes

Analog le module (solution suite)

def get_by_partial_match(logfile, string, field):
    for line in parse_apache_log(logfile, fmt):
        if line[field].find(string) == 0:
            yield line

def get_by_request(logfile, request):
    request = request.split('?')[0]
    return get_by_partial_match(logfile, request, 'request')

def get_by_datetime(logfile, datetime):
    return get_by_partial_match(logfile, datetime, 'time')

get_by_min = get_by_datetime
get_by_hour = get_by_datetime
get_by_day = get_by_datetime

Presenter Notes

Web avec wsgi

WSGI is the Web Server Gateway Interface. It is a specification that describes how web server communicates with web applications, and how web applications can be chained together to process one request.

WSGI is Python standard described in detail in PEP 3333.

Nous partons de l'exemple d'usage proposé dans la documentation du module wsgiref.

Nous allons mettre en place une application qui nous permettra d'interroger notre analyseur de logs par l'intermédiaire d'une page web.

Presenter Notes

Le script d'origine

from wsgiref.util import setup_testing_defaults
from wsgiref.simple_server import make_server

def simple_app(environ, start_response):
    setup_testing_defaults(environ)

    status = '200 OK'
    headers = [('Content-type', 'text/plain; charset=utf-8')]

    start_response(status, headers)

    ret = [("%s: %s\n" % (key, value)).encode("utf-8")
           for key, value in environ.items()]
    return ret

httpd = make_server('', 8000, simple_app)
print("Serving on port 8000...")
httpd.serve_forever()

Presenter Notes

Script n'affichant que ce qui change

Les modifications à apporter figurent ci dessous :

env_orig = None

def simple_app(environ, start_response):
    global env_orig
    if env_orig == None:
        env_orig = environ

    [...]

    ret = [("%s: %s\n" % (key, value)).encode("utf-8")
           for key, value in environ.items() if env_orig[key] != value]
    return ret
  • utilisation d'une variable env_orig (notez le global) pour enregistrer l'environnement d'origine.
  • ajout d'un test dans la liste de compréhension pour ne retrouner que les valeurs qui ont changé.

    if env_orig[key] != value
    

Presenter Notes

PATH_INFO et QUERY_STRING

Pour l'URL http://localhost:8000/?statut=500, c'est la clef QUERY_STRING de l'environnement qui contient l'information.

QUERY_STRING: status=500

Pour http://localhost:8000/statut/500, c'est PATH_INFO

PATH_INFO: /status/500

Il ne nous reste plus qu'à choisir un protocole que saura interpréter notre serveur.

Proposition : le PATH_INFO représentera /<opération>/<valeur>, les opérations étant :

  • ip, requete, statut, heure, minute

Presenter Notes

Parser les logs via le Web

Exercice

  • modifier le serveur wsgi pour qu'il affiche les logs jeuxdemots dont le statut est 500,
  • mettre en place le protocole /<opération>/<value>,
  • afficher en html dans une <table>,
  • rajouter des liens pour rendre tout ça cliquant...
  • the end.

Presenter Notes

Web : affichage des erreurs 500

Solution partielle :

[...]
log_path = "/home/joel/TPPYTHON/AnaLog/"
logfile = '{}/jdm_access.log'.format(log_path)
sys.path.insert(0, log_path)
[...]
def simple_app(environ, start_response):
    [...]
    ret = []
    for line in analogmod.get_by_status(logfile, '500'):
        ret += [
            "{} {} {} {}\n".format(
                line['host'], line['time'],
                line['request'], line['status']).encode("utf-8")
        ]
    return ret
  • rajouter les imports qui vont bien
  • le fichier logfile doit être référencé en absolu

Presenter Notes

Web : mise en place protocole

def to_utf8(line):
    return "{} {} {} {}\n".format(
        line['host'], line['time'],
        line['request'], line['status']).encode("utf-8")

def simple_app(environ, start_response):
    d_op = {
        'status': analogmod.get_by_status,
        'request': analogmod.get_by_request,
        'host': analogmod.get_by_host,
        'time': analogmod.get_by_datetime
    }
    [...]
    path_info = environ['PATH_INFO']
    ret = ["rien".encode("utf-8")]
    if path_info:
        ret = []
        try:
            key, value = path_info.split('/', 2)[1:]
            ret.append(to_utf8(line) for line in d_op[key](logfile, value))
            return ret
        except Exception as err:
            return [str(err).encode("utf-8")]
    return ret

ATTENTION ! Code non testé...

Presenter Notes

Web : mise en place HTML et liens

Solution partielle

def a(line, op):
    elt = line[op]
    return """<a href="/{}/{}">{}</a>""".format(op, elt, elt)

def to_html(line):
    return "<tr>{}</tr>".format("<td>{}</td>".format(
        "</td><td>".join(
            (a(line, 'host'), a(line, 'time'),
             a(line, 'request'), a(line, 'status')
            )
        )
    )).encode("utf-8")

Presenter Notes

Merci

Merci pour votre attention

Presenter Notes