Site original : Sam & Max: Python, Django, Git et du cul
Quand on a à traiter des choses bloquantes, avec des dépendances, des flux complexes ou des actions répétitives, créer des files d’attente peut se révéler très judicieux.
Par exemple lancer la génération d’un gros zip sur le clic d’un utilisateur, télécharger plein fichiers en parallèle pour son site de cul, lancer des calculs sur plusieurs machines et récupérer le résultat, encoder des videos en arrière plan, etc.
Le problème, c’est que fabriquer des files d’attente à la main, ça mène généralement à une grosse galère. La première boîte dans laquelle j’ai travaillé avait tout un système de queues à base de PHP + SQL fait à la main qui tapait dans du MySQL, c’était pas marrant du tout
Je fais une pause, et je note que le potentiel de jeux de mots sur cet article est fortement élevé. Mais je resterai fort.
Locking, priorité, dépendance, asynchronicité, concurrence, sérialisation, encoding, stockage, accessibilité, load balancing… Toutes ces problématiques sont bien vicieuses et chronophages. Il vaut mieux utiliser une lib solide et éprouvée.
Je resterai fort.
Kombu est une telle lib, mais elle est lourde et complexe à utiliser. J’avais fais le choix de la prendre pour un gros projet avec Max, je le regrette sur le long terme : c’est dur à maintenir et à faire évoluer. Le code est vraiment pas sympa.
Heureusement il existe une bibliothèque qui se met au dessus de Kombu pour et nous expose juste les fonctionnalités que l’on souhaite : celery.
Fort.
Celery est simple pour démarrer, mais très puissant si on rentre dans le détail, et croyez moi, le détail, on peut y rentrer très très profondément.
F…
Qui dit file d’attente, dit stockage. Il faut bien mettre les tâches quelque part et communiquer avec ce quelque part. En base de données ? Dans un gestionnaire de messages ? En mémoire ? Dans un cache ?
Celery résout le problème en proposant la même interface, quelque soit le support. Actuellement, on peut utiliser :
Dans notre exemple, nous allons le faire avec Redis car :
Pour ceux qui ont pas redis, c’est généralement dans les dépôts. Par exemple sur Ubuntu :
sudo apt-get install redis-server
Il n’y a rien à faire de plus, ça tourne, c’est configuré avec des valeurs par défaut qui sont saines. Je vous l’ai dis, redis, c’est fantastiquement bien foutu.
Ensuite on install celery et la lib d’accès à redis en Python qui porte un nom très original :
pip install celery redis
Ca devrait compiler un peu, et comme hab avec les extensions en C, assurez vous d’avoir un compilateur et les headers en place comme indiqué dans l’article sur pip.
Ensuite on peut créer ses tâches. Créez un module, par exemple tasks.py :
import urllib2 from collections import Counter from celery import Celery # Configuration de celery. Ceci peut aussi se faire dans un fichier de config. # Ici on dit à celery que pour le module 'tasks', on va utiliser redis # comme broker (passeur de massage) et comme result backend (stockage du # resultat des tâches). celery = Celery('tasks', broker='redis://localhost', backend='redis://localhost') # Et voici notre première tâche. C'est une fonction Python normale, décorée # avec un decorateur de celery. Elle prend une URL, et calcule le nombre # de lettre "e" qu'il y a dans la page. @celery.task def ecount(url): return Counter(urllib2.urlopen(url).read())['e']
On lance ensuite le processus celery dans un terminal (en production, mettez ça dans supervisord ou systemd pour que ça démarre automatiquement) :
[test] sam ~/Bureau/celery_test $ celery -A tasks worker -B --loglevel=info -------------- celery@sam v3.0.21 (Chiastic Slide) ---- **** ----- --- * *** * -- Linux-3.2.0-48-generic-x86_64-with-Ubuntu-12.04-precise -- * - **** --- - ** ---------- [config] - ** ---------- .> broker: redis://localhost:6379// - ** ---------- .> app: tasks:0x2a2fa50 - ** ---------- .> concurrency: 4 (processes) - *** --- * --- .> events: OFF (enable -E to monitor this worker) -- ******* ---- --- ***** ----- [queues] -------------- .> celery: exchange:celery(direct) binding:celery [Tasks] . tasks.ecount [2013-07-26 13:22:21,631: INFO/Beat] Celerybeat: Starting.. [2013-07-26 13:04:51,274: WARNING/MainProcess] celery@sam ready. [2013-07-26 13:04:51,280: INFO/MainProcess] consumer: Connected to redis://localhost:6379//.
-A
précise le module à importer, -B
démarre le beat (on verra ça plus tard), worker
dit à celery que démarrer des processus de consommation de files d’attente (par défaut 4 qui travaillent en parallèle), et --loglevel=info
va nous permettre d’avoir un affichage verbeux pour comprendre ce qui se passe.
Votre file d’attente est prête, et frétille d’impatience.
A partir de là, vous pouvez envoyer des tâches dans la file d’attente, depuis n’importe où :
Plusieurs programmes peuvent envoyer plein de tâches, en même temps, et elles vont se loger dans la file d’attente, sans bloquer le programme qui les a envoyé.
Par exemple, depuis le shell :
>>> from tasks import ecount >>> res = ecount.delay('http://danstonchat.com')
Ceci ne bloque pas mon shell, la ligne s’exécute immédiatement. La fonction ecount
n’est pas appelée depuis le shell, elle est dans la file d’attente et sera appelée par un des processus (les fameux ‘worker’) qui consomment la queue. Du côté de la file, on peut voir dans le log :
[2013-07-26 14:18:08,609: INFO/MainProcess] Got task from broker: tasks.ecount[599a52ea-ef6b-4499-981d-cd17fab592df] [2013-07-26 14:18:09,070: INFO/MainProcess] Task tasks.ecount[599a52ea-ef6b-4499-981d-cd17fab592df] succeeded in 0.446974039078s: 1242
On a donc notre tâche qui a bien été traitée.
On peut récupérer le résultat dans le shell :
>>> res.state 'PENDING'
Ah… La tâche n’est pas encore terminée. Et un peu plus tard :
>>> res.state 'SUCCESS' >>> res.result 1242
Lancer une tâche est bien entendu peu intéressant, les listes d’attente sont vraiment sympa quand on a plein de tâches à lancer, par plein de processus différents :
results = [ecount.delay(url) for url in ('http://google.com', 'http://sametmax.com', 'http://sebsauvage.com', 'http://multiboards.com', 'http://0bin.net', 'http://danstonchat.com')]
[2013-07-26 14:25:46,646: INFO/MainProcess] Got task from broker: tasks.ecount[5d072a7b-29f8-4ea6-8d92-6a4c1740d724] [2013-07-26 14:25:46,649: INFO/MainProcess] Got task from broker: tasks.ecount[402f6a4f-6b35-4f62-a786-9a5ba27707d2] [2013-07-26 14:25:46,650: INFO/MainProcess] Got task from broker: tasks.ecount[bbe46b1b-4719-4c42-bd2f-21e4d72e613e] [2013-07-26 14:25:46,652: INFO/MainProcess] Got task from broker: tasks.ecount[8fb35186-66e2-4eae-a40c-fc42e500ab9d] [2013-07-26 14:25:46,653: INFO/MainProcess] Got task from broker: tasks.ecount[fc63f5db-8ade-4383-b719-c3d6390ca246] [2013-07-26 14:25:46,654: INFO/MainProcess] Got task from broker: tasks.ecount[8434e21d-79ea-4559-a90e-92e2bc2b9dc7] [2013-07-26 14:25:47,144: INFO/MainProcess] Task tasks.ecount[bbe46b1b-4719-4c42-bd2f-21e4d72e613e] succeeded in 0.479865789413s: 27 [2013-07-26 14:25:47,242: INFO/MainProcess] Task tasks.ecount[5d072a7b-29f8-4ea6-8d92-6a4c1740d724] succeeded in 0.578661203384s: 609 [2013-07-26 14:25:47,501: INFO/MainProcess] Task tasks.ecount[fc63f5db-8ade-4383-b719-c3d6390ca246] succeeded in 0.35736989975s: 263 [2013-07-26 14:25:47,645: INFO/MainProcess] Task tasks.ecount[8434e21d-79ea-4559-a90e-92e2bc2b9dc7] succeeded in 0.403187036514s: 1270 [2013-07-26 14:25:47,815: INFO/MainProcess] Task tasks.ecount[8fb35186-66e2-4eae-a40c-fc42e500ab9d] succeeded in 1.14100408554s: 23 [2013-07-26 14:25:49,010: INFO/MainProcess] Task tasks.ecount[402f6a4f-6b35-4f62-a786-9a5ba27707d2] succeeded in 2.34633708s: 3158
Car du coup on sait que ces multiples tâches ne vont pas bloquer le processus en cour, mais qu’en plus la charge sera répartie sur le nombre de workers qu’on a décidé au départ, ni plus (surcharge du serveur), ni moins (traitement trop lent).
On peut attendre que la tâche soit terminée :
>>> print res.wait() 9999
Mais ce n’est pas vraiment le but. On cherche avant tout à ce que les tâches soient non bloquantes, et exécutées dans un processus à part voir potentiellement distribuées sur plusieurs serveurs.
Par ailleurs, Celery n’est pas un remplacement d’un système de traitement asynchrone comme Tornado ou NodeJS, il n’est pas fait pour envoyer des réponses asynchrones à l’utilisateur. Il est fait pour faire des tâches en background, répartir la charge et ordonner le traitement. Bien entendu, on peut faire communiquer un système asynchrone avec celery comme ici ou ici, mais c’est une autre histoire.
Concentrons nous sur les tâches.
La question de “Comment je sais quand une tâche est terminée ?” est souvent traduisible par “comment je réagis à une tâche pour lancer du code quand elle s’est terminée sans erreur ?”.
Et là, il y une solution toute simple :
res = tache1.s(arg1, arg2) | tache2.s() | tache3.s(arg1)
Ceci va créer une chaîne de tâches. Quand la première se termine, la deuxième se lance en recevant le résultat de la première en argument.
s()
fabrique une sous-tâche, c’est à dire une tâche à envoyer dans la file plus tard avec des arguments pré-enregistrés. Dans notre exemple, celery va lancer tache1
avec deux arguments, puis si ça marche, va appeler tache2
en lui passant le résultat de tache1
comme argument, puis si ça marche, va appeler tache3
avec le résultat de tache2
en premier argument et arg1
en second argument.
En fait, celery vient avec tout un tas d’outils pour exécuter des tâches dépendantes les unes des autres : par groupes, par chaînes, par morceaux, etc. Mais de toute façon, vous pouvez appeler une tâche… à l’intérieur d’une autre tâche. Donc parti de là vous pouvez faire pas mal de choses.
C’est là qu’intervient le “beat” dont j’ai parlé tout à l’heure. Avec cette option, celery va vérifier toutes les secondes si il n’y a pas une tâche répétitive à lancer, et la mettre dans une file d’attente, à la manière d’un cron.
Il suffit de définir une tâche comme periodic_task
pour qu’elle soit lancée régulièrement.
import smtplib from celery.schedules import crontab from celery.decorators import periodic_task # va executer la tâche à 5h30, 13h30 et 23h30 tous les lundi # run_every accepte aussi un timedelta, pour par exemple dire "toutes les 10m" @periodic_task(run_every=crontab(hour='5,13,23', minute=30, day_of_week='monday') def is_alive(): """ Vérifie que le blog est toujours en ligne, et si ce n'est pas le cas, envoie un mail en panique. """ if urllib2.urlopen('http://sametmax.com').code != 200: mail = 'lesametlemax__AT__gmail.com'.replace('__AT__', '@') server = smtplib.SMTP('smtp.gmail.com:587') server.starttls() server.login('root', 'admin123') server.sendmail(mail, mail, msg) server.quit()
Il y a de bons exemples sur la syntaxe sur crontab()
dans la doc.
D’une manière générale, la doc de Celery est très très riche, donc plongez vous dedans si cet article ne répond pas à vos besoins, car si ça peut être mis dans une file, ça peut être fait par Celery.
Celery n’autoreload pas le code, donc redémarrez les workers à chaque fois que vous modifiez vos tasks.
Attention aussi aux tâches récurrentes, la suivante peut se lancer avant que la précédente soit terminée. C’est à vous de faire des tâches idempotentes, ou alors de mettre en place un système de locking.
Ceci est un post invité de MrAaaah posté sous licence creative common 3.0 unported.
Salut à toutes et à tous c’est MrAaaah, je suis celui qui réussi à résoudre le plus rapidement les énigmes du coffre secret de Sam et Max, on ma demandé de vous faire un "petit" article expliquant un peu ma démarche et mes méthodes.
Avant de me lancer dans la résolution des énigmes je tiens à signaler que je n’ai qu’un an de python dans les pattes, de ce fait le code peut paraitre cradoc et peut-être un poil bourrin vu que je ne connais pas encore très bien python, ses librairies et ses subtilités (ça a également été codé très vite). Je n’ai pas retouché le fonctionnement de mes scripts, j’ai juste renommé quelques variables et mis des commentaires, donc c’est du brut, y’a pas de vérif, ça plante à la fin, etc.
Première énigme http://game.sametmax.com/ : un message, un champ d’entrée et une image. Comme beaucoup je suppose, je commence par rentrer diverses conneries dans le formulaire, toujours le même résultat : le message “I don’t GET it.”.
Je parcours le code source de la page à la recherche d’un éventuel script où autre indice dans le code, nada.
Si on regarde l’url on peut voir que le code est envoyé via la méthode GET : http://game.sametmax.com/?code=monnezsurtescouilles. En relisant le message de ci-dessus on peut deviner qu’il faut balancer une requête de type POST. Pour faire ça rien de plus simple, on sort Firebug (ou équivalent), on édite le code source en changeant l’attribut method du formulaire et on renvoi avec un code bidon.
Là on obtient un nouveau message : “Error log : areyouhuman”. Bon je retente des codes bidon (genre “yes”, etc.). Je cherche sur le net “areyouhuman”, mais rien de concluant.
Au final rien de bien complexe, mais y’a moyen de tourner un peu en rond, il suffit «juste» d’allez sur l’URL http://game.sametmax.com/areyouhuman
À partir de là des connaissances en Python (basiques) vont être nécessaires. (j’utilise ici Python 2.7)
La page nous renvoie ce qui ressemble à une définition de classe NextUrlContainer avec un attribut next suivi d’une bouillie de caractères en commentaire.
NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) # Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4=
Avec un peu de recherche on découvre que la soi-disant bouillie n’est autre que du texte encodé en base64. Une fois décodé (il y a des utilitaires en ligne qui font ça très bien), on obtient quelque chose qui nous parle un peu plus (quoique).
ccopy_reg _reconstructor p0 (c__main__ NextUrlContainer p1 c__builtin__ object p2 Ntp3 Rp4 (dp5 S'next' p6 I2097351 sb.
Encore un peu de recherche et je découvre que c’est un fameux pickle, on sort notre Python préféré.
# -*-coding:Utf-8 -* import base64, pickle NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) b64 = "Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4=" # on décode et on dépickle-ise obj = pickle.loads(base64.urlsafe_b64decode(b64)) print obj.next
Et on obtient un objet de type NextUrlContainer contenant l’id de la prochaine URL : 2097351.
En ce rendant sur http://game.sametmax.com/areyouhuman/2097351 on se retrouve avec de nouveau un pickle encodé en base64. Après en avoir fait deux URL à la main, on ressort Python pour automatiser tout ça.
Notre script doit :
Récupérer le contenu de la page http://game.sametmax.com/areyouhuman/:id. (j’utilise ici la librairie httplib)
Décoder le contenu. (base64)
Dépickle-iser (je ne connais pas le terme “officiel”). (pickle)
Récupérer le prochain id.
Recommencer à la première étape jusqu’à… la fin.
Voilà le script utilisé. Il affiche chaque nouvel id et plante quand on arrive au bout.
# -*-coding:Utf-8 -* import httplib import base64, pickle # la classe donnée sur la page http://game.sametmax.com/areyouhuman NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) # initialisation de la connection connection = httplib.HTTPConnection("game.sametmax.com:80") # Fait une requête sur l'URL http://game.sametmax.com/areyouhuman/:id # Et retourne le contenu de la page def make_request(id): url = "/areyouhuman/%i" % id connection.request("GET", url) response = connection.getresponse() return response.read() # id de départ id = 2097351 # tant que ça plante pas ça suit les liens while 1: # récupération du pickle en base64 contenant l'id de la prochain URL response_crypt = make_request(id) # on décode et on dépickle-ise obj = pickle.loads(base64.urlsafe_b64decode(response_crypt)) # on remplace l'id courant par le prochain afin de pouvoir recommencer id = obj.next print id
On le lance et ça tourne un petit bout de temps avant d’en arriver au bout. (le serveur doit adorer)
3245669 2993679 1050294 ....... 9683898 8147905 9664723 wololo.zip Traceback (most recent call last): File "1_requests.py", line 27, inresponse_crypt = make_request(id) File "1_requests.py", line 14, in make_request URL = "/areyouhuman/%i" % id TypeError: %d format: a number is required, not str
On a donc notre prochaine destination : http://game.sametmax.com/wololo.zip
Pour pas me prendre la tête j’ai télécharger le .zip dans le même répertoire que mes scripts.
On commence par dézipper wololo.zip, on obtient one_more_time_1.zip que l’on dézippe, on obtient one_more_time_2.zip que l’on dézippe, on obtient one_more_time_3.zip que l’on dézippe, etc.
Bon par besoin de chercher très loin pour savoir quoi faire, l’algo est simple :
On dézippe wololo.zip (zipfile)
Tant que ça marche
On dézippe le fichier venant d’être extrait
Ce qui se traduit en python par :
# -*-coding:Utf-8 -* import zipfile # archive de départ archive_name = "wololo.zip" # tant qu’il y a quelque chose à dézipper, ça tourne while 1: # ouverture de l'archive archive = zipfile.ZipFile(archive_name, 'r') # récupération du nom du fichier contenu dans l'archive file_to_extract = archive.namelist()[0] # extraction archive.extract(file_to_extract) print file_to_extract # le fichier tout fraichement extrait sera le prochain à être dézippé # (ça plantera quand y'aura plus de .zip) archive_name = file_to_extract
On exécute
one_more_time_1.zip one_more_time_2.zip one_more_time_3.zip ................... one_more_time_998.zip one_more_time_999.zip one_more_time_1000.zip youdiditjonhy.txt Traceback (most recent call last): File "2_zips.py", line 10, inarchive = zipfile.ZipFile(archive_name, 'r') File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 712, in __init__ self._GetContents() File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 746, in _GetContents self._RealGetContents() File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 761, in _RealGetContents raise BadZipfile, "File is not a zip file" zipfile.BadZipfile: File is not a zip file
On se retrouve donc avec un nouveau petit fichier youdiditjonhy.txt contenant le texte
api.json
ainsi que 1000 fichiers zip … un petit rm one_more_time* dans son terminal (sur Windows débrouillez-vous) pour cleaner tout ça et on repart pour l’énigme suivante.
On se rend donc sur http://game.sametmax.com/api.json…
Et on se prend ça dans la tronche :
{"\f": ["."], " ": ["."], "$": [".", "."], "(": [".", ".", "."], ",": [".", "."], "0": [".", "."], "4": ["."], "8": [".", ".", "."], "<": ["."], "@": [".", ".", "."], "D": [".", ".", "."], "H": [".", "."], "L": [".", "."], "P": ["."], "T": [".", "."], "X": [".", ".", "."], "\\": [".", "."], "`": ["."], "d": ["."], "h": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "l": [".", ".", "."], "p": [".", ".", "."], "t": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "x": [".", ".", "."], "|": [".", ".", "."], "\u000b": [".", ".", "."], "#": [".", ".", "."], "'": [".", ".", "."], "+": [".", "."], "/": [".", ".", "."], "3": [".", "."], "7": [".", ".", "."], ";": [".", "."], "?": ["."], "C": [".", ".", "."], "G": ["."], "K": [".", "."], "O": ["."], "S": [".", ".", "."], "W": ["."], "[": ["."], "_": [".", "."], "c": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "g": [".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "k": ["."], "o": [".", ".", "."], "s": [".", "."], "w": ["."], "{": [".", "."], "\n": [".", "."], "\"": ["."], "&": [".", "."], "*": ["."], ".": [".", ".", "."], "2": ["."], "6": [".", "."], ":": ["."], ">": [".", "."], "B": ["."], "F": [".", "."], "J": ["."], "N": ["."], "R": [".", "."], "V": [".", "."], "Z": [".", ".", "."], "^": [".", "."], "b": [".", ".", "."], "f": [".", ".", "."], "j": [".", "."], "n": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "r": [".", "."], "v": [".", "."], "z": [".", ".", "."], "~": [".", "."], "\t": ["."], "\r": [".", ".", "."], "!": ["."], "%": [".", "."], ")": ["."], "-": [".", "."], "1": [".", ".", "."], "5": [".", "."], "9": [".", ".", "."], "=": [".", "."], "A": ["."], "E": [".", ".", "."], "I": [".", "."], "M": ["."], "Q": [".", ".", "."], "U": ["."], "Y": [".", ".", "."], "]": ["."], "a": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "e": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "i": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "m": [".", ".", "."], "q": [".", "."], "u": [".", "."], "y": [".", ".", "."], "}": [".", ".", "."]}
Bon déjà on sait que c’est du JSON, on se fait un nouveau script pour voir ce que donne ce json en Python :
# -*-coding:Utf-8 -* import httplib import json # on récupère le json connection = httplib.HTTPConnection("game.sametmax.com:80") url = "/api.json" connection.request("GET", url) response = connection.getresponse() # on transorme en python decoded = json.loads(response.read()) print decode
Bon c’est pas beaucoup mieux, on à affaire à un dictionnaire avec pour clé un caractère Unicode avec une liste de ‘.’ plus où moins longue associé. En mettant un peu en forme ça donne un truc du genre :
Y : . . . Z : . . . [ : . \ : . . ] : . ^ : . . _ : . . ` : . a : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . b : . . .
Soit pas grand-chose d’exploitable, je modifie le script pour obtenir le nombre de points associé à chaque caractère :
# -*-coding:Utf-8 -* import httplib import json # on récupère le json connection = httplib.HTTPConnection("game.sametmax.com:80") url = "/api.json" connection.request("GET", url) response = connection.getresponse() # on met ça en python decoded = json.loads(response.read()) # on passe le dico en liste pour pouvoir le trier items = decoded.items() items.sort() for i in items: print "%s : %s" % (i[0], len(i[1]))
Z : 3 [ : 1 \ : 2 ] : 1 ^ : 2 _ : 2 ` : 1 a : 50 b : 3
À partir de là je tente de convertir des mots en remplaçant les lettres par le nombre de ‘.’ associé, par exemple “Max” => 1503. J’arrête vite mes conneries et me rend compte qu’une grosse partie des caractères n’est associé qu’a 1,2 ou 3 points et quelques autres sont à 50, 30, 60, étrange…
Je garde juste les plus grosses valeurs, les tris dans l’ordre croissant, soit : 10 20 … 80. En reprenant chaque clé associée, on obtient le mot gnitaehc, soit cheating à l’envers. Peu de chance que ce soit là par hasard.
On se rend sur http://game.sametmax.com/cheating, on arrive à la fin.
Retour sur la page d’accueil avec cette fois-ci le Konami code en message (↑↑↓↓←→←→BA). Pas de temps à perdre, on va sur http://game.sametmax.com/konami.
Et c’est là que ce termine la série d’énigmes ! Au final la difficulté était plutôt bien dosée, pas trop hard pour un débutant tout en offrant du challenge et du cassage de tête.
J’espère avoir été clair dans mes explications, si y’a des questions sur certains point allez-y, même si ce n’est pas moi je pense qu’il y’aura toujours quelqu’un pour vous éclairer.
Si vous avez utilisé d’autres techniques ou s’il y a des remarques sur mon code, allez-y. Dans les commentaires de l’article original, il y’a une solution “collaborative” similaire à la mienne (bon leurs codes est quand même un peu plus propre), y’a également Recher qui nous propose d’utiliser un éditeur hexadécimal pour l’énigme du zip.
Je suis assez amateur de ce genre de puzzle/challenge, si ça interesse des gens il y a quelques sites bien sympa pour se prendre la tête :
http://www.pythonchallenge.com/ : qui est un grand classique, très orienté python, niveau énigme c’est vraiment dans la même veine que cette ouverture de coffre, pour ma part j’avais testé il y’a quelque temps et je m’étais assez vite retrouvé coincé… (par contre il ne faut pas s’arrêter à l’aspect graphique qui fait un peu saigner les yeux…)
http://www.newbiecontest.org/ : ce site est beaucoup plus général en proposant des épreuves de crypto, hacking, prog, logique, crackme, etc. Et y’a largement de quoi péter des plombs. La partie programation est assez interessante pour se faire un peu de python. (par contre il est necessaire de s’inscrire)
Y’en a plein d’autre je suppose, mais je n’ai testé que ces deux là, si vous avez quelques bonnes adresses n’hésitez pas à partager.
Sur ce, bravo à ceux qui ont tenté le jeu, bon voyage à Max et encore merci à nos deux taulier !
Mraaaah vient d’avoir un compte sur le blog, et peut être aura-t-il le temps de publier les sources de ses solutions. En attendant, voici le code source du jeu.
En général on documente un peu le truc, on met un README, des commentaires, etc. Mais là je vais invoquer le fait que je veux vous montrer ce que ça donne un code à la vite pour justifier ma grosse flemme. Ce code source est intéressant car il montre plusieurs choses :
Vous trouverez dans le répertoire scripts les soluces des énigmes que j’ai utilisé pour vérifier que ça marchait. Vous verrez que là aussi, c’est du vite fait, avec parfois des trucs qui plantent tout simplement par fainéantise.
Pour démarrer le jeu : python site.py
avec du 2.7.
Mraaaah a publié ses solutions.
J’avais dosé une difficulté sans trop savoir jusqu’où aller car le niveau de prog sur le blog est plutôt disparate, et afin de laisser sa chance à tout le monde. Mais ce mail me fait assez plaisir quant au résultat.
Le gagnant a ouvert le coffre et nous a contacté à 2 heures du math :
Salut les gars !
Bon visiblement j’ai réussi à pénétrer le petit coffre rouge en premier [censored].
Alors tout d’abord un gros merci pour le taff que vous faites sur votre blog. Je vous ai découvert y’a un peu plus d’un an en cherchant des infos sur Django et Python que je ne connaissait pas du tout à l’époque. Et depuis je ne vous lâche plus et je me régale de vos articles (bien que je fasse parti de la masse discrète en ne commentant jamais …). Un an plus tard me voilà à appliquer des techniques apprises (en partie) sur votre blog (json, pickle, etc.).
Première fois que je résout un challenge de ce genre, c’était bien sympathique (un peu prise de tête par moment mais quel bonheur à chaque pas réaliser), bravo pour la réalisation en tout cas !
On voit les acharnés !
Comme pour le cadeau de la dernière fois, on va lui demander d’envoyer une petite photo de ce qui l’a reçu et on la postera pour que vous découvriez tous ensemble ce qu’il y avait a gagner. Ça restera donc une surprise jusqu’au déballage (mais moi je suis jaloux parce que j’en voulais un).
Si notre gagnant à le courage, ce serait marrant qu’il fasse un article pour expliquer comment il a passé les énigmes, et poster son code. Tu nous le dis en comment si tu es chaud ?
Dans tous les cas je laisse le jeu en ligne quelques jours pour le fun, et on le retirera, puis on mettera le code source sur github.