Initiation à Python - Joël Maïzi

Dernière màj. 13/10/2014

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

Objectifs de la séance

  • Utiliser la librairie standard Python.
  • Prendre quelques bons réflexes pour développer en Python.

Références (en anglais) :

Pour naviguer dans ce document, utilisez les flèches gauche et droite ou la molette de la souris. Pour obtenir de l'aide, tapez la lettre 'h'.

Presenter Notes

Python et IPython en mode interactif

Un des premiers réflexes à acquérir : le 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]:

Presenter Notes

Exemple d'usage du module tarfile

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

  • Exo1 : Vérifiez 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

Exo1 (solution)

Un module doit pouvoir être importé. Deux possibilités pour vérifier rapidement si les modules tarfile et gzip sont présents sur votre poste de travail :

  1. python -c 'import tarfile, gzip'
  2. en mode interactif, taper la commande import tarfile, gzip

Si les modules sont présents rien ne sera affiché à l'écran. En cas d'erreur en revanche, l'affichage suivant apparaîtra :

$ python -c 'import toto'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named toto

La dernière ligne indique que le module toto n'a pas été trouvé.

Presenter Notes

Les différences entre Python 2 et 3

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

Suit une très succinte présentation des raisons qui ont amené au passage à Pyhton 3 et des incompatibilités qui existent entre les deux versions.

Presenter Notes

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

PEP 3000

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

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.

There are a few minor downsides, such as slightly worse library support 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) == True

Prendre en charge une version ou une autre

Vous pouvez avoir besoin de spécifier la version de Python que vous utilisez pour exécuter le script. Sur un système Unix, la première ligne si elle commence par #! (shebang) indique au système l'interpréteur qu'il devra utiliser. Par exemple :

#!/usr/bin/python3

ou

#!/usr/bin/python2.6

L'écriture #!/usr/bin/python référence "en dur" l'interpréteur Python installé sur votre système (en règle générale la version 2).

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.

Si vous utilisez un gestionnaire de version de code (git, mercurial, ...) et si vous devez tester votre code avec différentes version de Python, la seconde option est à privilégier.

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

démonstration de la mise en place du lien :

  • avec_env.py
  • sans_env.py
  • hash -r (pour rehash)

(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

str and sys.version_info.major

Les deux "transparents" suivants présentent les différences du type str entre les version 2 et 3 de Python.

Presenter Notes

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

En python2 str et une séquence d'octets.

$ 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)"

En python3, str est de l'unicode. En revanche, le mot clef unicode n'existe plus.

$ 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
  • On décode du bytes pour obtenir une chaîne de caractères (str).
  • On encode de l'unicode (str) pour obtenir du bytes.
  • L'encodage par défaut est UTF-8 pour str.encode() et bytes.decode(). Ça n'est pas le cas en Pyhon 2 où il faut spécifier le type d'encodage de caractères.

    print([c for c in "chaîne"]) # affiche correctement les caractères
    

Presenter Notes

Scripts vs. modules

Nous allons maintenant voir les différences qui existent entre scripts et modules et comment transformer (ou écrire) un script pour qu'il soit utilisable comme module.

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

réf. : Scripts, modules

Presenter Notes

le script affiche_kek_chose.py

#!/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 ! Si on cherche à l'importer, une erreur se produit :

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
  • Observez les deux messages d'erreur. Quelle ligne pose problème ?
  • Exo 2 : Expliquer le message d'erreur.

Presenter Notes

Exo 2 (solution)

La ligne posant problème est : afficher(sys.argv[0], sys.argv[1])

  • Question : quel est le type d'objet sys.argv ? Jetez un oeil à sa documentation

    >>> import sys
    >>> type(sys.argv)
    

La documentation dit :

If no script name was passed to the Python interpreter, argv[0] is the empty string.

>>> sys.argv[0]
''

En revanche, si sys.argv[0] est toujours défini, c'est sys.argv[1] qui ne l'est pas dans le cas du lancement du script sans argument comme dans le cas de la tentative d'import.

Presenter Notes

Transformer un script en module

Exo 3 : trouvez un moyen simple pour permettre l'import du module affiche_kek_chose. Le moyen le plus "économique" consiste à rajouter un caractère....

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

Exo 4 :

  • modifiez affiche_kek_chose pour afficher la variable __name__.
    Lancez le script puis importez le module,
  • 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

Exos 3 et 4 (solutions)

Exo 3 : Le moyen le plus simple consiste à commenter la ligne posant problème :

# afficher(sys.argv[0], sys.argv[1])

Exo 4 : Dans le cas du script, l'affichage de la variable __name__ affiche la valeur __main__. Dans le cas de l'import, __name__ affiche affiche_kek_chose. C'est le nom qui est utilisé pour réaliser l'import

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__". La solution consiste donc à n'exécuter l'appel de la fonction que si __name__ est égale à __main__.

[...]
if __name__ == '__main__':
    afficher(sys.argv[0], sys.argv[1])

Il est maintenant possible d'utiliser le script comme un module.

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

a

Les paramètres des fonctions

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

# les trois invocations de ma_fonction sont équivalentes
ma_fonction('un', 'deux', 'trois', quatre=4, cinq='cinq')
l = ['deux', 'trois']
d = {'quatre': 4, 'cinq': 'cinq'}
ma_fonction('un', *l, **d)

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

  • noter la présence de __init__.py dans chaque répertoire
  • illustration avec ipython et les tabulations
    >>> import xml. <TAB> ...

Presenter Notes

a

Exercices

Suivent 3 exercices correspondant à des cas pratiques :

  • Mauvaise blague, nous permettra de manipuler des fichiers et lecture et écriture et d'introduire les classes.
  • AnaLog, est un cas pratique d'analyse de Logs apache.
  • Web, reprend les résultats de l'exercice précédent pour mettre en place un service web (wsgi) permettant de naviguer dans les logs.

Les pages suivantes seront remaniées dans les jour/semaines qui viennent.

Merci de m'adresser vos questions/suggestions pour permettre d'améliorer ce document.

joel.maizi at lirmm.fr 13/10/2014

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/MauvaiseBlague/data.

Presenter Notes

a

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

a

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

a

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

a

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

explication sur les listes de compréhension (cf. slide suivant)

Presenter Notes

a

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

a

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

a

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

a

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

    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

a

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

a

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

a

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

Il n'était pas question d'exposer les IP réelles ce qui a été l'occasion d'écrire un script permettant l'anonymisation des IP. Les IP du fichier jdm_access.log ne sont pas les IP des machines ayant effectué les requêtes.

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

a

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

a

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

a

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

a

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

a

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