Site original : Sam & Max: Python, Django, Git et du cul
Bottle : le plus petit disponible (tient dans un fichier). Génial pour du code jetable ou des petits sites, etc. Fantastique outil d’enseignement et d’apprentissage. En plus, ça tient plutôt bien la charge compte tenu de la taille.
django : le framework Python le plus connu. On peut tout faire avec, l’écosystème est fantastique (il y a des apps djangos tierces partout pour tout et n’importe quoi, c’est un truc de fou). Mais le maîtriser prend du temps. Efficace pour construire un site web avec beaucoup de logique personnalisée. Au final, si on veut être sérieux dans en programmation Web avec Python, on finit toujours par passer par Django.
flask : sa taille est entre django et bottle, et avec tout ce qu’il faut pour faire un site de taille moyenne. Il commence à avoir pas mal de plugins disponibles un peu partout sur la toile, et est une alternative très sympa quand on n’a pas besoin de charger les 3 tonnes de Django.
wep2py : se positionne en alternative à Django et Flask, mais avec une philosophie très différente. Pas mal de magie (à la rails), des interfaces graphiques pour l’admin, composants très couplés mais très intégrés… Je ne suis pas fan, mais on m’en a dit du bien.
cherrypy : un framework WSGI pure Python qui a de très bonnes perfs sans rien rajouter, même en prod. Mais depuis qu’ils ont rendu disponible sa partie serveur et qu’on peut l’utiliser pour Django/flask/bottle, ça ne vaut plus le coup d’utiliser sa partie framework qui n’a rien de fantastique.
pyramid : le plus gros compétiteur de Django en termes de fonctionnalités. Beaucoup moins monolithique, et bien plus flexible, donnant pas mal de contrôle. Mais l’intégration des composants de django le rend beaucoup plus facile à maîtriser, et son écosystème est 100 fois meilleur (j’ai toujours été en froid avec l’écosystème Zope).
twisted un framework internet asynchrone en pur Python. J’ai dit INTERNET, pas WEB. On peut faire HTTP, mais aussi SSH, IMAP, FTP, et à peu près n’importe quoi. Surpuissant, et ses performances sont incroyables. Son API est aussi la plus merdique de tout l’univers Python. Apprendre et utiliser Twisted, c’est comme se branler avec des gants de boxe en récitant l’alphabet à l’envers.
tornado : framework Web asynchrone. Techniquement le seul compétiteur de nodejs en pur Python (pas de gevent, d’extension C, etc) si on exclue crossbar encore trop immature. L’API n’est pas incroyable, mais pas trop dur, et les perfs sont bonnes. Les Websocket marchent clé en main.
cyclone : tornado qui tourne dans l’event loop de twisted. Pour pouvoir utiliser l’API de tornado pour le Web, mais les outils de twisted pour le reste. Si vous êtes capable d’utiliser ça à son plein potentiel, qu’est-ce que vous foutez à lire cet article ?
webpy : un ancêtre. Un fantôme du passé. Do not use.
Il existe ensuite tout un tas d’expérimentations de framework avec asyncio, gevent, etc. C’est plus du bricolage qu’autre chose pour le moment, alors je ne recommande pas de base un gros projet dessus à moins d’être déjà très à l’aise avec la prog web python.
Tout est accessible en Python. Il n’y a pas de variables privées.
Quand on veut une variable à usage interne, la convention est de la nommer avec un underscore devant :
class Monique: def __init__(self): self._private = "lessons"
Ça n’empêche rien, mais les devs savent qu’il faut éviter d’utiliser cette variable car elle ne fait pas partie de l’API publique de la classe, et l’implémentation pourrait changer plus tard.
Comme self
, c’est une convention forte puisque la plupart des libs de completion du code la prennent en compte.
Il existe une fonctionalité moins connue, qui fait que quand on utilise deux underscores, accéder directement à la variable lève un AttributeError
:
class Monique: def __init__(self): self.__private = "lessons" m = Monique() print(m.__private) AttributeError: 'Monique' object has no attribute '__private'
Du coup, quelques rares personnes ont utilisé cette feature pour émuler des attributs privés, ignorant le fait qu’on vous dit partout sur la toile que tout est accessible en Python.
C’est une mauvaise idée.
C’est une mauvaise idée car si votre objectif est la sécurité, ça ne sert à rien puisque votre variable est accessible de tas de manières détournées différentes. Par exemple :
print(m.__dict__['_Monique__private']) # lessons m.__dict__['_Monique__private'] = None print(m.__dict__['_Monique__private']) # None
C’est une mauvaise idée car si votre objectif est de créer une API publique, un seul underscore suffit.
Enfin c’est une mauvaise idée car si vous fournissez une lib qui ne correspond pas à un besoin, quelqu’un peut toujours contourner le problème en monkey patchant votre code le temps que vous trouviez une solution plus propre. En rendant la variable “privée”, vous rendez ceci plus difficile, sans le rendre impossible. Tout le monde y perd.
Pas une question que l’on se pose souvent, à moins d’avoir une lib qui expose une classe avec beaucoup d’introspection.
Ceci dit pour la culture G, je me suis dit que ça pourrait intéresser quelques geeks de savoir comment lister les enfants d’une classe, récursivement.
def subclasses(cls, _found=()): """ Retourne toutes les classes héritant d'une classe """ # On check que la classe est hérite bien de "object". # Utile uniquement pour un code 2.x qui utiliserait # des old style classes, qui n'ont pas "__subclasses__". if not isinstance(cls, type): raise TypeError('subclasses works only with new-style classes' ', not %s' % cls) # On va stocker les classes rencontrées dans un set # pour éviter les doublons car on peut tomber sur # un héritage en forme de diamant. _found = _found or set() try: subs = cls.__subclasses__() except TypeError: # Erreur levée si cls == type, petits vicieux subs = cls.__subclasses__(cls) for sub in subs: if sub not in _found: _found.add(sub) # Un appel récursif comme on les aimes subclasses(sub, _found) return _found
Pas très compliqué, donc, si on connait les piègeounets : ça ne marche pas sur les old syle classes, et il y a un traitement spécial pour type
. Celà dit, qui utilise encore des old styles classes ? Et franchement lister les héritiers de type
, à par pour satisfaire sa curiosité…
Aller, juste pour le fun, sur un virtualenv vierge:
for x in subclasses(object): print x ## <type 'callable-iterator'> ## <class '_abcoll.Sized'> ## <type 'exceptions.EOFError'> ## <class '_abcoll.Mapping'> ## <type 'exceptions.UnicodeWarning'> ## <type 'exceptions.IOError'> ## <type 'memoryview'> ## <type 'code'> ## <type 'exceptions.ImportWarning'> ## <type 'sys.flags'> ## <type 'iterator'> ## <class 'warnings.catch_warnings'> ## <type 'exceptions.Warning'> ## <type 'exceptions.EnvironmentError'> ## <type 'dict'> ## <class 'encodings.utf_8.IncrementalEncoder'> ## <type 'weakproxy'> ## <class '_abcoll.Iterator'> ## <type 'sys.version_info'> ## <type 'posix.stat_result'> ## <type 'frozenset'> ## <class 'codecs.CodecInfo'> ## <class 'zipimport.ZipImportError'> ## <type 'exceptions.StopIteration'> ## <type 'exceptions.ImportError'> ## <class 'site._Printer'> ## <type 'weakref'> ## <class '_abcoll.Sequence'> ## <type 'property'> ## <class 'encodings.utf_8.IncrementalDecoder'> ## <type 'imp.NullImporter'> ## <type 'exceptions.TypeError'> ## <type 'bytearray'> ## <type 'int'> ## <type 'exceptions.UnicodeTranslateError'> ## <type 'instance'> ## <type 'exceptions.BaseException'> ## <type 'exceptions.SyntaxWarning'> ## <type 'file'> ## <class '_abcoll.MappingView'> ## <class '_weakrefset._IterationGuard'> ## <type 'exceptions.UnicodeDecodeError'> ## <type 'classobj'> ## <type 'exceptions.UnicodeError'> ## <class 'abc.abstractproperty'> ## <type 'exceptions.DeprecationWarning'> ## <type 'complex'> ## <type 'set'> ## <type 'buffer'> ## <type 'generator'> ## <type 'exceptions.Exception'> ## <class 'codecs.IncrementalDecoder'> ## <type 'exceptions.UserWarning'> ## <type 'staticmethod'> ## <type 'exceptions.UnicodeEncodeError'> ## <class 'encodings.CodecRegistryError'> ## <class '_abcoll.MutableSequence'> ## <class '_abcoll.KeysView'> ## <type 'cell'> ## <type 'exceptions.StandardError'> ## <type 'enumerate'> ## <type 'exceptions.KeyboardInterrupt'> ## <type 'exceptions.BufferError'> ## <class 'warnings.WarningMessage'> ## <type 'EncodingMap'> ## <class 'codecs.BufferedIncrementalDecoder'> ## <type 'exceptions.ValueError'> ## <class '_abcoll.Set'> ## <type 'exceptions.MemoryError'> ## <type 'exceptions.SystemExit'> ## <type 'reversed'> ## <class '_abcoll.Hashable'> ## <type 'bool'> ## <type 'classmethod'> ## <type 'exceptions.ReferenceError'> ## <type 'exceptions.GeneratorExit'> ## <class '_abcoll.Container'> ## <class '_abcoll.ItemsView'> ## <type 'xrange'> ## <type 'exceptions.PendingDeprecationWarning'> ## <type 'basestring'> ## <type 'exceptions.SystemError'> ## <type 'exceptions.ZeroDivisionError'> ## <class 'site.Quitter'> ## <type 'long'> ## <type 'NotImplementedType'> ## <type 'super'> ## <type 'fieldnameiterator'> ## <class '_abcoll.Callable'> ## <class '_abcoll.MutableSet'> ## <type 'exceptions.ArithmeticError'> ## <class 'abc.ABCMeta'> ## <type 'exceptions.OverflowError'> ## <type 'exceptions.FutureWarning'> ## <type 'sys.float_info'> ## <type 'formatteriterator'> ## <type 'type'> ## <type 'exceptions.AssertionError'> ## <type 'traceback'> ## <type 'exceptions.FloatingPointError'> ## <type 'function'> ## <type 'instancemethod'> ## <type 'sys.long_info'> ## <type 'builtin_function_or_method'> ## <class 'signal.ItimerError'> ## <class 'site._Helper'> ## <type 'exceptions.LookupError'> ## <type 'wrapper_descriptor'> ## <type 'exceptions.KeyError'> ## <class '_abcoll.ValuesView'> ## <type 'slice'> ## <class '_weakrefset.WeakSet'> ## <type 'list'> ## <type 'exceptions.IndexError'> ## <type 'getset_descriptor'> ## <type 'posix.statvfs_result'> ## <type 'exceptions.SyntaxError'> ## <type 'frame'> ## <class 'codecs.BufferedIncrementalEncoder'> ## <type 'PyCapsule'> ## <type 'exceptions.IndentationError'> ## <type 'NoneType'> ## <type 'zipimport.zipimporter'> ## <type 'member_descriptor'> ## <type 'exceptions.AttributeError'> ## <type 'ellipsis'> ## <type 'exceptions.UnboundLocalError'> ## <type 'unicode'> ## <class '_abcoll.Iterable'> ## <type 'exceptions.TabError'> ## <type 'exceptions.NameError'> ## <type 'tuple'> ## <class 'warnings._OptionError'> ## <type 'exceptions.NotImplementedError'> ## <type 'exceptions.RuntimeWarning'> ## <type 'str'> ## <class '_abcoll.MutableMapping'> ## <type 'exceptions.BytesWarning'> ## <type 'module'> ## <type 'exceptions.RuntimeError'> ## <class 'codecs.IncrementalEncoder'> ## <type 'float'> ## <type 'exceptions.OSError'> ## <type 'dictproxy'> ## <type 'weakcallableproxy'>
Putain, ça c’est du remplissage ! 20minutes serait fier de moi.
Ceci est un post invité de 01ivier posté sous licence creative common 3.0 unported.
Il est des choses inutiles plus passionnantes que d’autres.
Il semblerait que j’ai un petit faible pour les choses inutiles que je fais moi-même.
Car, afficher le contenu de sa webcam dans une console, ça ne sert clairement pas à grand chose mais j’ai pourtant crié très fort en serrant les poings quand j’y suis arrivé.
Sous Nunux, il existe un joli petit paquet tout beau tout chaud qui permet de gérer du flux vidéo en Python : python-opencv.
Alors, zou :
sudo apt-get install python-opencv
Et pour afficher sa webcam, seules quelques lignes suffisent :
#-*- coding: utf-8 -*- import cv2 # Choix du périphérique de capture. 0 pour /dev/video0. cap = cv2.VideoCapture(0) while True: # On capture l'image. ret,im = cap.read() # On l'affiche. cv2.imshow('Ma Webcam à moi',im) # Et on attend 40 millisecondes pour avoir du 25 images par seconde. key = cv2.waitKey(40)
GO !!
Je vais bien me garder de vous faire un tuto sur OpenCV tant cette librairie est puissante (comprendre : j’y panne que dalle). Si vous êtes curieux, vous pouvez aller faire un tour ici.
Et, parce que j’avais bien d’autres choses plus intéressantes à faire que d’aller visiter le lien ci-dessus, j’ai fait un petit print du im précédent et découvert une liste toute mignonne dont voici la structure:
# Avec des valeurs pour les niveaux comprises entre 0 et 255. im[n° de ligne][n° de colonne][niveau de bleu, niveau de vert, niveau de rouge]
J’étais content car j’allais pouvoir récupérer les valeurs de chaque pixel de cette façon:
for ligne in range(hauteur): for colonne in range(largeur): niv_bleu = im[ligne][colonne][0] niv_vert = im[ligne][colonne][1] niv_rouge = im[ligne][colonne][2]
Si, pour un autre projet formidable, je n’avais pas eu à travailler avec une RasberryPi et ses petites cuisses de bébé, je pense que j’utiliserai encore cette méthode de bourrin.
Mais voilà, c’est juste ridicule quand on sait que la liste en question est en fait un array numpy et qu’il est 10, 100 fois plus rapide de faire:
for ligne in range(hauteur): for colonne in range(largeur): niv_bleu = im.item(ligne, colonne, 0) niv_vert = im.item(ligne, colonne, 1) niv_rouge = im.item(ligne, colonne, 2)
Je n’en suis pas à me dire que la prochaine fois que j’achèterai un grille-pain, je lirai la notice avant de l’utiliser, mais presque…
Petite remarque en passant : les valeurs de rouge, de vert et de bleu étant comprise entre 0 et 255, cela nous donne 256 valeurs pour chaque couleur.
Soit 256 x 256 x 256. Soit 2⁸ x 2⁸ x 2⁸. Soit 2²⁴ ==> les couleurs de notre flux sont codées par défaut sur 24 bits.
À noter qu’il est tout à fait possible d’analyser une image sans avoir à l’afficher, ce qui permet d’utiliser OpenCV dans un environnement sans gestionnaire de fenêtre.
Bon. J’avais mes niveaux RVB pour chaque pixel. Il me fallait désormais afficher de la couleur dans la console.
Quelques requêtes Duck Duck Go plus tard, je découvre termcolor qui fait très bien le job mais dont on peut se passer en regardant les codes ANSI de plus près.
Bien entendu, la grande majorité des consoles étant limitées à 8 couleurs, soit 2³, soit 3-bit je me suis restreint à cette qualité.
Démonstration :
Il est possible d’obtenir beaucoup plus de couleurs en combinant les fonds, les caractères et les intensités comme le fait la libcaca, mais on ne joue pas vraiment dans la même cour.
Perso, quand ça s’est affiché en rouge pour la première fois, j’ai eu des frissons partout. Parce qu’il faut bien comprendre que je n’ai toujours aucune idée de ce que “\033[” et autres “m” veulent dire. J’ai copié/collé, c’est tout. Et dans ces cas là, quand ça marche, c’est toujours la fête.
Ce qui pourrait être un problème, on le voit à l’image, c’est qu’une fois que j’ai écrit TATA YOYO en rouge, le prompt devient lui aussi rouge, et ainsi de suite à chaque changement de couleur. Pour remédier à ça, il faut ajouter \033[0m à la fin du texte à afficher pour que le reste soit écrit avec la couleur par défaut du terminal.
Démonstration :
C’est d’ailleurs ce que fait termcolor, sauf que, dans notre cas, nous n’avons pas besoin de revenir à cette valeur par défaut à chaque affichage de pixel vu que le suivant sera lui aussi coloré.
Je vous en parle seulement parce que vous avez l’air sympa.
J’ajouterai qu’après avoir effectué un benchmark de folie exploitant brillamment les deux points qui clignotent à chaque seconde sur mon radio-réveil, il s’est avéré que la solution “maison” était plus performante que termcolor : mon choix était fait.
J’ai renoué contact avec le █ il n’y a pas si longtemps. Aussi étonnant que cela puisse paraître, alors que je baigne quasi quotidiennement dans l’informatique depuis 30 ans, il n’est pas impossible que notre dernière rencontre remonte à 1986 sur le Commodore 64 familial.
Pour vous donner une idée de l’émotion qui m’a traversé quand j’ai revu le █, vous pourriez très clairement user de l’expression “le █ d’Olivier” en lieu et place de “la madeleine de Proust” dans vos discussions. Mais, à l’oral, le █ passe mal, et c’est bien dommage.
Pour info, le pseudo unicode de █ c’est \u2588.
Et, pour afficher un █ en couleur, il suffit de faire comme vu au dessus.
Reste à trouver un moyen de passer des millions de couleurs potentielles de notre vidéo aux huit de notre console.
C’est là que vous allez comprendre pourquoi je me suis acharné avec mes captures d’écrans. C’était pour bien vous faire intégrer l’association entre les couleurs et la valeur qui les code. À savoir :
1 : rouge
2 : vert
3 : jaune
4 : bleu
5 : violet
6 : turquoise
7 : blanc
Et là qu’est qu’on remarque ?
Que cela respecte la synthèse additive si on attribue 1 au rouge, 2 au vert et 4 au bleu, bien entendu !
1 + 2 = 3 et en synthèse additive rouge + vert = jaune
1 + 4 = 5 et en synthèse additive rouge + bleu = violet
2 + 4 = 6 et en synthèse additive vert + bleu = turquoise
1 + 2 + 4 = 7 et en synthèse additive rouge + vert + bleu = blanc
Mettez-vous à ma place: je venais de découvrir l’Amérique !
Bon, rétrospectivement, cela ne constitue vraiment rien d’extraordinaire en soi dans la mesure où c’est ce qui découle logiquement d’un codage sur 3 bits mis en place par un être humain qui a juste envie de faire simple plutôt que de faire compliqué.
Mais tout de même, sur le moment…
… L’AMÉRIQUE BORDEL ! L’AMÉRIQUE !
Il devenait alors facile d’évaluer le degré de présence de chaque composante RVB d’un pixel puis de déterminer laquelle des 8 couleurs lui correspondait le plus.
Voilà comment je m’y suis pris :
# On initialise à 0 l'indice du pixel analysé indice_couleur = 0 # On analyse le niveau de Bleu du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 0) > seuil : #...on ajoute 4 à l'indice. indice_couleur += 4 # On analyse le niveau de Vert du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 1) > seuil : #...on ajoute 2 à l'indice. indice_couleur += 2 # On analyse le niveau de Rouge du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 2) > seuil : # ...on ajoute 1 à l'indice. indice_couleur += 1
L’indice obtenu correspond alors au code couleur ANSI à utiliser !!
Je veux dire.
Tout de même.
C’est super, non ?
…
Hum…
…
Bien, bien…
C’est bientôt fini, il me reste juste…
1) Le noir ANSI est en fait du gris, et c’est bien moche, j’ai donc préféré partir du principe que la console aurait un fond noir et afficher un “espace” pour chaque pixel noir.
2) print(“\033[H\033[2J”) permet d’effacer la console comme le fait os.system(‘clear’).
Mais, j’imagine que ça devait faire trop de 033 dans le script pour moi, parce que, psychologiquement, ça ne passait pas.
J’ai un peu discuté avec moi-même et on a fini par décider d’utiliser le clear.
3) J’ai commencé par utiliser la concaténation pour ajouter mes █ colorés à mon texte_image final :
texte_image += u"\033[3{0}m█".format(indice_couleur))
Mais, Stack Overflow a tapé à la fenêtre et il m’a dit qu’il était beaucoup plus rapide de créer une liste puis d’en joindre les éléments.
J’ai benchmarké avec mon radio-réveil.
Stack Overflow avait raison.
4) Par contre, ce que Stack Overflow s’était bien gardé de me dire, c’est que l’affichage en console avait ses propres limites internes.
Bilan, après avoir optimisé mon code du mieux que je le pouvais, j’ai constaté que le script calculait de toutes façon les texte_image plus vite que la console ne pouvait les afficher.
Ce qui relève un peu du FAIL quand on y pense.
Donc, si vous avez une idée pour que ça ne scintille plus au delà de 25 lignes de hauteur, je suis preneur, sachant que je suis tout à fait à même d’entendre que j’ai fait n’importe quoi dès le début.
ÉDIT: Dans les commentaires, Tmonjalo a proposé une solution qui résout le problème du scintillement en faisant revenir le curseur en haut à gauche plutôt que d’effacer la console. J’ai donc édité le code en conséquence. Merci à lui.
Voici le script final. Il est diffusé sous les termes de la très sérieuse WTFPL.
Les variables à modifier pour faire des tests sont le seuil, la largeurOut et la hauteurOut.
À noter aussi que si vous faite un petit…
cap = cv2.VideoCapture("VotreFilm.avi")
… au lieu d’ouvrir la webcam en /dev/video0, et bien vous allez voir VotreFilm.avi dans la console. Super génial !
#-*- coding: utf-8 -*- import cv2 import os # Définition du flux capturé. # Comme elle sera, de toutes façons, retaillée à la baisse, # elle est fixée à la valeur la plus petite supportée par la webcam. # À noter que cette valeur minimale peut varier en fonction de votre cam. largeurIn = 160 hauteurIn = 120 # Définition du flux qui s'affichera en console. # À savoir le nombre de caractères en largeur et en hauteur. largeurOut = 60 hauteurOut = 20 # Seuil de présence des couleurs rouge, vert, bleu dans un pixel. # Entre 0 et 255. seuil = 120 # Choix du périphérique de capture. # Ici /dev/video0 cap = cv2.VideoCapture(0) # Configuration de la définition du flux. cap.set(3, largeurIn) cap.set(4, hauteurIn) # On efface la console. os.system('clear') # On définit une position de référence pour le curseur. # En haut à gauche, donc, puisqu'on vient juste d'effacer la console. print ('\033[s') # Pendant... tout le temps... while True: # On capture une image. ret, img = cap.read() # On retaille l'image capturée. img = cv2.resize(img,(largeurOut, hauteurOut)) # On initialise une liste qui contiendra tous les éléments de l'image liste_image = [] # Pour chaque ligne de l'image. for ligne in range(hauteurOut): # Pour chaque colonne de chaque ligne. for colonne in range(largeurOut): # On initialise à 0 l'indice du pixel analysé indice_couleur = 0 # On analyse le niveau de bleu du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 0) > seuil : #...on ajoute 4 à l'indice. indice_couleur += 4 # On analyse le niveau de bleu du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 1) > seuil : #...on ajoute 2 à l'indice. indice_couleur += 2 # On analyse le niveau de bleu du pixel. # S'il est au dessus du seuil... if img.item(ligne, colonne, 2) > seuil : # ...on ajoute 1 à l'indice. indice_couleur += 1 # Si l'indice obtenu est différent de 0... if indice_couleur: # ...on ajoute un █ coloré à la liste. liste_image.append(u"\033[3{0}m█".format(indice_couleur)) # Si l'indice est égal à 0... if not indice_couleur: # ...on ajoute un espace (noir ?) à la liste. liste_image.append(" ") # On fait en sorte que le terminal retrouve sa couleur initiale liste_image.append("\n\033[00m") # On produit un string en mettant bout à bout tous les éléments de la liste texte_image = ''.join(liste_image) # On affiche l'image. print(texte_image) # On replace le curseur à la position de référence. print ('\033[u') # On attend 40 millisecondes pour obtenir du 25 images par seconde. key = cv2.waitKey(40)
Voici une petite vidéo réalisée pour promouvoir un événement à nous. À partir de la 44ème seconde, on peut m’y voir coiffé d’un masque de soudeur en train de faire tenir au plafond un donut géant au moyen d’une batte de base-ball en aluminium :
Pour plus d’information sur cet événement vous pouvez allez voir ici et admirer, par la même occasion, notre magnifique affiche réalisée en pur Python.
Enfin, compte tenu des tauliers du site, je ne pouvais passer à côté de la figure imposée.
Je vous propose donc un extrait de Deep 3-bit, hommage appuyé à Vuk Cosic et son légendaire Deep ASCII.
Plus je fais du dev Web, plus je m’aperçois que beaucoup de mes collègues n’ont aucune idée de comment fonctionne HTTP sous le capot. Comme vous le savez, ma règle numéro 1 c’est qu’il n’y a rien d’évident, donc petit tuto pour expliquer les bases.
On ne va pas rentrer dans les petits détails, juste une petite intro histoire de savoir ce qui se passe derrière ce script PHP ou cette application bottle.
Article long, vous connaissez la chanson.
Et puis c’est dans le ton de l’actu ^^
Se balader sur le Web, c’est comme aller au resto. On est un client, on demande quelque chose au serveur, le serveur va voir en cuisine, et revient avec la bouffe, ou une explication sur l’absence de la bouffe (mais jamais pourquoi la bouffe est dégueulasse, allez comprendre):
Ce cycle requête/réponse se déroule des centaines de fois quand on parcours un site Web. Chaque lien cliqué déclenche une requête GET, et la page suivante ne s’affiche que parce qu’on a reçu la réponse contenant le HTML de celle-ci. Les formulaires, les requêtes AJAX, la mise à jour de vos Tweets sur votre téléphone, tout ça fonctionne sur le même principe.
Donc, sur votre site Web, Firefox, Chrome et les autres vont faire une requête à une machine, votre machine contient un code (si vous êtes dev, votre code :) qui va recupérer cette requête et générer une réponse.
Généralement, les développeurs utilisent des outils sophistiqués pour écrire des sites Web. Si bien que quand on écrit du code, on ne voit pas vraiment la requête et la réponse, mais des fonctions, des objets, du HTML… Comment ça arrive et comment ça repart est géré automatiquement.
Regardons ce qui se passe quand ce n’est PAS géré automatiquement.
Voici le code d’un petit serveur HTTP écrit en Python 3. Il accepte n’importe quelle requête sur le port 7777 et retourne toujours une page avec marqué “Coucou”.
import asyncio # Le texte de la réponse qu'on va renvoyer COUCOU = b"""HTTP/1.1 200 OK Date: Fri, 16 Jun 2014 23:59:59 UTC Content-Type: text/html <html> <body> <h1>Coucou</h1> </body> </html>""" # La fonction qui gère chaque requête et qui renvoie une réponse # pour chacune d'entre elles. La même réponse à chaque fois pour cet # exemple, mais on peut fabriquer une réponse différente si on veut. def handle_request(request, response): # On lit la requête data = yield from request.read(1000000) # On affiche son contenu. Surprise, c'est que du texte ! print(data.decode('ascii')) # On écrit notre réponse, que du texte aussi ! response.write(COUCOU) # On ferme la connexion : le protocole HTTP est stateless, # c'est à dire qu'il n'y a pas de maintien d'un état # côté client ou serveur et chaque requête est indépendante # de toutes les autres. response.close() if __name__ == '__main__': # Machinerie pour faire tourner le serveur : # Récupération de la boucle d'événements. loop = asyncio.get_event_loop() # Création du serveur qui écoute sur le port 7777 # et qui va appeler notre fonction quand il reçoit # une requête. f = asyncio.start_server(handle_request, port=7777) # Installation du serveur dans la boucle d'événements. loop.run_until_complete(f) # On démarre la boucle d'événement. print("Serving on localhost:7777") loop.run_forever() # Si vous êtes dev, vous commencez à comprendre combien # les libs et frameworks qui gèrent tout ce bordel pour # vous sont fantastiques.
Si on lance le serveur et qu’on visite la page http://localhost:7777
sur un navigateur, on va alors voir ceci dans le terminal où tourne le serveur :
$ python3 server.py Serving on localhost:7777 GET / HTTP/1.1 Host: 127.0.0.1:7777 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Connection: keep-alive Cache-Control: max-age=0
C’est la requête envoyée par notre client, et reçue par notre serveur. “Aller” sur l’adresse veut dire en fait que votre navigateur envoie ce morceau de texte.
Juste après, mon serveur renvoie aussi du texte. Toujours le même dans notre cas :
HTTP/1.1 200 OK Date: Fri, 16 Jun 2014 23:59:59 UTC Content-Type: text/html <html> <body> <h1>Coucou</h1> </body> </html>
Le navigateur l’interprète, et vous fabrique cette page :
Comme vous pouvez le voir, ce n’est que du texte tout simple qui est reçu et envoyé.
Quand quelqu’un développe un site Web, ce qu’il fait vraiment, c’est ça. La plupart du temps il ne le voit pas car ses outils lui facilitent la tâche en analysant le texte et en lui donnant des fonctions et des objets pour le manipuler. Mais derrière, c’est juste du texte.
Quand quelqu’un surf le Web, ce qu’il fait vraiment, c’est ça. Mais le navigateur se charge de le cacher derrière des jolis contrôles, et affiche un résultat bien plus agréable à regarder.
Le texte d’une requête est divisé en 3 parties :
Prenons notre précédente requête d’exemple :
GET / HTTP/1.1 Host: 127.0.0.1:7777 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Connection: keep-alive Cache-Control: max-age=0
L’action demandée sur la ressource est toujours sur la première ligne :
GET / HTTP/1.1
Elle se divise en 3 parties :
Notez bien que le chemin de la ressource n’a pas à correspondre à un chemin réel d’un fichier sur l’ordinateur. Une ressource est quelque chose de complètement virtuel, quelque chose que mon programme met à disposition selon mes désirs. Si c’est un fichier, très bien, mais ce n’est pas obligatoire, et je peux générer n’importe quelle réponse qui me plait.
En effet, si je vais sur l’adresse http://127.0.0.1:7777/user/monique/profile/
, vous notez que mon serveur continue de marcher. Il reçoit simplement la requête :
GET /user/monique/profile/ HTTP/1.1 ...
C’est à mon serveur de décider ce qu’il choisit de faire avec le chemin /user/monique/profile/
. Ici il est un peu con et continue de renvoyer “coucou”.
Bien, on vient de voir “L’action demandée sur la ressource”, voyons maintenant les headers :
Host: 127.0.0.1:7777 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Connection: keep-alive Cache-Control: max-age=0
Les headers sont des informations supplémentaires que le client fourni à mon serveur sous la forme :
Nom-De-L-Information: contenu
Les sauts de ligne sont très importants. La première ligne de la requête est “L’action demandée sur la ressource”, puis on met un saut de ligne, ensuite vient un header, puis un saut de ligne, puis un header… Chaque header doit tenir sur une ligne.
En HTTP/1.1, seul le header Host
est obligatoire, mais comme aucun header n’était obligatoire en HTTP/1.0, beaucoup de serveurs acceptent l’absence de tout header.
Les headers contiennent généralement des informations sur le client (ex: Accept-Language
vous dit quelles langues le client accepte), le contenu de la requête (ex: Content-Length
indique la taille de la requête) ou demande un comportement du server (Cache-Control
précise comment gérer les ressources qu’on peut mettre en cache).
Pour “Le corps de la requête”, il nous faut ajouter du contenu à notre requête.
Pour cela, faisons une page avec un petit formulaire HTML :
<html> <head> <title>Test post</title> </head> <body> <form method="post" action="http://127.0.0.1:7777/"> <p><input type="test" value="coucou, tu veux voir mon POST ?" name="salut"> <input type="submit"></p> </form> </body> </html>
Ce qui nous donne cette page :
Si on active le formulaire, notre serveur affiche cette requête :
POST / HTTP/1.1 Host: 127.0.0.1:7777 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 41 salut=coucou%2C+tu+veux+voir+mon+POST+%3F
Déjà, vous notez que le verbe d’action a changé : POST / HTTP/1.1
.
Ensuite, à la fin de la requête, on laisse une ligne vide, puis on a de nouveau du texte :
salut=coucou%2C+tu+veux+voir+mon+POST+%3F
C’est le corps de la requête.
C’est comme ça que le serveur reçoit le contenu des données des formulaires, et c’est ce qu’on retrouve dans la variable $_POST
en PHP ou request.POST
en Django
Encore une fois, les outils de programmation évitent au codeur de travailler avec du charabia, et le transforme en quelque chose de facile à comprendre et à manipuler.
C’est pareil, ma bonne dame. Reprenons notre exemple de réponse :
HTTP/1.1 200 OK Date: Fri, 16 Jun 2014 23:59:59 UTC Content-Type: text/html <html> <body> <h1>Coucou</h1> </body> </html>
Première ligne, on précise le protocole, puis le code de réponse, qui stipule la nature de votre réponse (tout va bien, une erreur, une redirection, la page n’existe pas, etc).
Ensuite les headers, puis on saute une ligne, et on met le corps de la réponse. Ici, le code HTML qui va donner notre jolie page “Coucou”.
Tout le reste, toutes les fonctionnalités fantastiques du Web (les liens hypertextes, les fichiers statiques JS et CSS inclus, les cookies, les redirections, le cache, la compression…) ont pour point d’entrée un de ces 3 éléments de la requête ou de la réponse. C’est que c’est un protocole bien foutu.
Enfin, disons, presque tout ? :)