Ceci est un post invité de atrament posté sous licence creative common 3.0 unported.
… il n’y a que des scripts en passe de devenir bons. Avis aux débutants, croyez-y, vous ne le resterez pas.
C’est l’histoire d’un des premiers “vrais scripts” que j’ai écrits. Je venais de finir Learning Python et Programming Python de Mark Lutz (à l’époque la seconde édition “couvre python 2 !”).
J’étais même pas un script kiddie, et je ne programmais que pour m’amuser. J’étais fan de Sinfest, un web comic gavé de gros blasphèmes qui tâchent.
Je me suis mis en tête de télécharger tout ces comics d’un coup. Voilà ce que ce script était, et ce qu’il est devenu.
Script originel avec formatage encore moins lisible, sur 0bin.
#~ Script to DL all sinfest webcomics #~ one can edit the START and END parameters to DL a subset STARTDAY = 17 # 17 STARTMONTH = 01 # 01 STARTYEAR = 2011 # 2000 ENDDAY = 31 ENDMONTH = 01 ENDYEAR = 2012 ROOTDIR = 'Sinfest' ROOTURL = "http://www.sinfest.net/comikaze/comics/" IMGEXT = ".gif" #~ Code start - no edition from here unless you know what you're doing #~ -------------------- from distutils.file_util import copy_file from urllib import urlopen import os.path import os #~ ROOTDIR=os.getcwd()+ROOTDIR #~ Check if date is out of range def checkdate(d, m, y, ed, em, ey, sd, sm, sy): # end date is ed, em, ey if y > ey or (y == ey and m > em) or (y == ey and m == em and d > ed): return 0 # date over date range elif y < sy or (y == sy and m < sm) or (y == sy and m == sm and d < sd): return 0 else: return 1 # date below end date and above start date #~ make the file name teh sae format than sinfest server def getfilename(y, m, d, IMGEXT): M =`m` D =`d` Y =`y` if len(M) < 2: M = '0' + M if len(D) < 2: D = '0' + D filename = Y + '-' + M + '-' + D + IMGEXT return filename #~ run the loop if not os.path.isdir(ROOTDIR): os.mkdir(ROOTDIR) for y in range(STARTYEAR, ENDYEAR + 1): for m in range(1, 13): for d in range(1, 32): filename = getfilename(y, m, d, IMGEXT) if checkdate(d, m, y, ENDDAY, ENDMONTH, ENDYEAR, STARTDAY, STARTMONTH, STARTYEAR): if os.path.isfile(ROOTDIR + '/' + filename): print '[' + filename + '] already exists' else: src = urlopen(ROOTURL + filename) if src.info().gettype() == 'image/gif': dst = open(ROOTDIR + '/' + filename, 'wb') dst.write(src.read()) dst.close() print '[' + filename + ']' + ' copied' else: print "MIMETYPE ERROR (probably ERROR 404) ignored" src.close() |
On remarquera l’arrogance du jeune codeur fier de lui dans certains commentaires. Ils étaient à mon intention, en vrai, et montraient surtout que je ne savais pas trop ce que je faisais.
En vrac, parmi les choses qui me font sourire aujourd’hui :
- la fonction
checkdate
qui prend 9 paramètres pour vérifier si une date est entre deux autres, - les grosses constantes au début
- la portabilité nulle du script qui doit être lancé au bon endroit
- la date codée avec 3 boucles pour l’année, le mois, le jour.
- Je ne parle même pas de la pep008, elle n’avait même pas un an, d’abord, et puis visiblement on était loin des questions de style…
Bon, il y a prescription, et puis je suis pas mal autodidacte, c’était pas si mal : ça fonctionnait ! Ça m’a quand même servi, de temps en temps, jusque 2012, pour remettre à jour mes dossiers d’images.
Et puis je l’ai relu et j’ai décidé de le reprendre avec ce que je savais de nouveau :
- le module
datetime
, - la construction
if __name__ == '__main__'
, - les exceptions,
et ça a donné ça :
# -*- coding: utf-8 -*- #! /usr/bin/python # Script to DL all sinfest webcomics # one can edit the START and END parameters to DL a subset #------------------------------------------------------------------------------- # Name: getSinfest # Purpose: # # Author: Atrament # # Created: 12/04/2013 # Copyright: (c) Atrament 2013 # Licence: #------------------------------------------------------------------------------- import datetime from urllib.request import urlopen import os.path import os from urllib.error import HTTPError def main(): debut=datetime.date(2000,1,17) curGif=debut if not os.path.isdir("Sinfest"): os.mkdir("Sinfest") while curGif<=datetime.date.today(): if not os.path.isfile("Sinfest/"+curGif.isoformat()+".gif"): try: src=urlopen("http://www.sinfest.net/comikaze/comics/"+curGif.isoformat()+".gif") dst=open("Sinfest/"+curGif.isoformat()+".gif",'wb') dst.write(src.read()) dst.close() print(" "+curGif.isoformat()+".gif : fetched.") src.close() except HTTPError as e: print("on "+curGif.isoformat()+" error happened. So is life.") else: print(curGif.isoformat()+" : ok") curGif+=datetime.timedelta(days=1) if __name__ == '__main__': main() |
Et honnêtement ça s’améliore (un peu): un shebang (pas tout à fait en haut du fichier, mais j’apprendrai plus tard), bien que je code à l’époque uniquement sous windows avec pyscripter, qui me fournit les commentaires de début de fichier, qui ne servent à rien, mais à l’époque m’éclatent. Ben oui, à ce moment là, je suis à peine adulte, et avec les heures que j’ai passé sur IDLE à tâtonnner, c’est une victoire pour moi.
C’est un peu plus tard encore que j’apprends que les requêtes internet qui prennent des heures, c’est pas obligatoire, parce qu’on peut faire du multi thread, et continuer de tourner pendant qu’on attend qu’une autre tâche s’accomplisse. Voilà de quoi faire rêver un jeune programmeur : la puissance du multi-coeurs à la portée de mon code ! (non, je ne savais pas que c’est faux). En conséquence, mon script évolue, pour devenir… ça. Attention, ça pique les yeux.
# -*- coding: UTF-8 -*- # --------------------------------------------------------------------- # Author: Atrament # Licence: CC-BY-SA https://creativecommons.org/licenses/by-sa/4.0/ # --------------------------------------------------------------------- # All libs are part of standard distribution for python 3.4 import imghdr import os import queue from threading import Thread import datetime from urllib.error import HTTPError, URLError from urllib.request import urlopen import zipfile import sys # Useful functions def make_cbz(directory): for year in range(2000, datetime.date.today().year + 1): with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive: for gif_file in [x for x in os.listdir(directory) if x.split("-")[0] == str(year)]: archive.write(directory + '/' + gif_file, arcname=gif_file) print("Sinfest-{}.cbz has been generated".format(year)) def confirm(prompt): if input(prompt + " (y/n)") in "yY": return True else: return False def file_needs_download(filename): """Checks whether a file exists, is corrupt, so has to be downloaded again also cleans garbage if detected""" if not os.path.isfile(filename): # many comics are *supposed* to be missing, # no need to output for these (uncomment for debug) # print("IS NOT FILE :", filename) return True elif os.path.getsize(filename) == 0: print("WRONG SIZE for", filename) return True elif filename.split(".")[-1] != "gif": print(filename, "IS NOT GIF") return True elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None: # Encoding error... print("WRONG FILE STRUCTURE for", filename) return True else: return False def conditional_download(filename, base_url): if file_needs_download(filename): try: src = urlopen(base_url + filename) dst = open(filename, 'wb') dst.write(src.read()) # gracefully close theses accesses. dst.close() src.close() print("\t" + filename + " : fetched.") except HTTPError: # many days do not have a comic published. # no need to flood the console for this. pass except URLError: pass print("network error on " + filename) finally: # clean garbage on disk, useful if failure occurred. file_needs_download(filename) class ThreadedWorker(): def __init__(self, function=None, number_of_threads=8): self.queue = queue.Queue() def func(): while True: item = self.queue.get() if function: function(item) else: print(item, "is being processed.") self.queue.task_done() self.function = func for i in range(number_of_threads): t = Thread(target=self.function, name="Thread-{:03}".format(i)) t.daemon = True t.start() def put(self, object_to_queue): self.queue.put(object_to_queue) def join(self): self.queue.join() def feed(self, iterator): for task in iterator: self.queue.put(task) def download_sinfest(target_folder): """ Creates a directory and fetches Sinfest comics to populate it in full. """ if not os.path.isdir(target_folder): os.makedirs(target_folder) os.chdir(target_folder) f = lambda filename: conditional_download(filename, "http://www.sinfest.net/btphp/comics/") # Make a worker with this function and run it t = ThreadedWorker(function=f, number_of_threads=20) # structure of comprehended list is a bit complex to generate all file names files = [(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat() + ".gif" for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)] t.feed(files) t.join() if __name__ == "__main__": if any(("y", "Y", "-y", "-Y" in sys.argv)): folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/") print("\nproceeding to download...") download_sinfest(folder) print("\ngenerating comic book files (.cbz)...") os.chdir(folder) os.chdir("..") make_cbz(folder) else: folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/") while not confirm("Target to downloads is {} ?".format(folder)): folder = input("Please enter new folder (N to abort) :") if folder in "nN": exit(0) if confirm("Proceed to download ?"): download_sinfest(folder) if confirm("Do you want to generate cbz (comic books) files ?"): os.chdir(folder) os.chdir("..") make_cbz(folder) input("Finished. Please press Enter") |
C’est une horreur. Si vous êtes de ces perfectionnistes qui lisent le code et font la code review par habitude, je suis désolé, vous avez du pleurer. Le commentaire sur la lib standard “en intro” montre que à ce moment là, j’ai un peu conscience qu’on peut accéder à d’autres modules, mais on est loin de pip pour moi. Je tente maladroitement d’exploiter sys.argv
(avec des erreurs qui pourraient être catastrophiques), et l’input à défaut. Une fonctionnalité neuve est apparue : faire des archives comic book en zip. Je suis fier.
Mais le côté comique de ce code, c’est la classe ThreadedWorker
. C’est ptêt bien ma première ‘vraie’ classe, mais j’implémente moi-même un dispatcheur de jobs sur des threads. Il y a de quoi être fier, mais on n’est pas du tout dans du python propre, là. C’est ballot, la version précédente était pas si mal, niveau clarté.
Et dire que la lib standard en fournit un, de dispacheur de jobs…
Je passe quelques itérations sur des détails, aujourd’hui il en est là, ce script.
#! /usr/bin/env python3.5 import imghdr import os import datetime import zipfile from concurrent.futures import ThreadPoolExecutor import requests import begin # Useful functions def make_cbz(dst_directory, src_directory): for year in range(2000, datetime.date.today().year + 1): with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive: for filename in os.listdir(src_directory): if filename.startswith(str(year)): archive.write(src_directory + "/" + filename, arcname=filename) print("Sinfest-{}.cbz has been generated".format(year)) def file_needs_download(filename): """Checks whether a file exists, is corrupt, so has to be downloaded again also cleans garbage if detected""" if not os.path.isfile(filename): # many comics are *supposed* to be missing, # no need to output for these (uncomment for debug) # print("IS NOT FILE :", filename) return True elif os.path.getsize(filename) == 0: print("WRONG SIZE for", filename) return True elif filename.split(".")[-1] != "gif": print(filename, "IS NOT GIF") return True elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None: # Encoding error... print("WRONG FILE STRUCTURE for", filename) return True else: return False def conditional_download(filename, base_url, caller=None): if file_needs_download(filename): src = requests.get(base_url + filename) # manage failure to download if src.status_code == 404: src.close() return # ignore it, that file is simply missing. if src.status_code != 200: # an error other than 404 occurred # print("Error {} on {}".format(src.status_code, filename)) src.close() if caller: # retry that file later caller.submit(conditional_download, filename, base_url, caller) # actually copy that file dst = open(filename, 'wb') dst.write(src.content) # gracefully close theses accesses. dst.close() src.close() print("\t{} : fetched.".format(filename)) def download_sinfest(target_folder): """ Source function for the process Creates a directory and fetches Sinfest comics to populate it in full. """ if not os.path.isdir(target_folder): os.makedirs(target_folder) os.chdir(target_folder) with ThreadPoolExecutor(max_workers=64) as executor: for file in ("".join([(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat(), ".gif"]) for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)): executor.submit(conditional_download, file, "http://www.sinfest.net/btphp/comics/", executor) @begin.start def run(path: "folder in which the comics must be downloaded" = os.path.expanduser("~/Sinfest/"), makecbz: "Compile CBZ comic book archives" = False): """Download the Sinfest WebComics""" download_sinfest(path) if makecbz: dst = path[:-1] if path.endswith('/') else path dst = "/".join(dst.split("/")[:-1]) make_cbz(dst, path) print("Finished. Goodbye.") exit() |
Enfin je m’autorise à utiliser des modules tierces. Alors, ça marche, même plutôt vite et bien. Mais c’est largement perfectible : si c’est mieux que par le passé, ça ne correspond pas à ce que je code aujourd’hui : il y a beaucoup de code hérité des anciennes versions et la prochaine évolution serait de le réécrire entièrement. Mais je ne lis plus vraiment Sinfest.
Mais à écrire ce texte, j’ai pris les nerfs, et j’ai refait tout ça. J’ai fait un effort, et j’ai tout jeté à github.
Encore un pas de mieux : les fichiers ont du sens, il y a une classe abstraite pour faire une pseudo-API, c’est devenu pratiquement évolutif.
La conclusion à l’attention du débutant : chacun voit et juge son code et celui des autres selon son niveau de compétence à l’instant T. On a tous été fiers comme Artaban de bouts de code laids à nos yeux d’aujourd’hui. Débutant qui m’a lu (merci, t’as bien du courage), garde tes scripts à travers les années, reprends les, tu te rendras compte de tous les progrès que tu fais au fil du temps. Quand un type te dis sur internet que ce que tu fais n’est pas “pythonique”, il a sûrement raison, de son point de vue, et il y a sans doute un autre gars qui lui dira la même chose demain. La courbe d’apprentissage est longue comme la vie.
Quelques typos et autres:
– Avis aux débutants
– gavé de gros blasphèmes qui tâchent
– Et dire que la lib standard (oui je suis tatillon)
– niveau de compétence
J’ai eu le courage de lire l’article, étant débutant, merci. J’ai commencé à faire du Python il y a deux ans maintenant dans un studio de 3D. Je code principalement des petits scripts et outils sur Maya et Nuke pour les artistes de la boîte.
Il y a quelques mois, j’ai jeté un coup d’oeil aux premiers scripts que j’ai fait. J’étais pas fier.
Puis je me suis mis en tête de reprendre un peu tout ça avec les connaissances que j’avais acquise au fil du temps. Ces connaissances elles viennent principalement de ce blog. J’ai pas encore pu mettre en pratique les plans à quatre par contre, ni à trois :(
Résultat, sur un outil qui permet d’exporter des anims, je suis passé de 400 lignes de code à 100 et des brouettes pour un script qui met trois fois moins de temps à s’exécuter… J’étais fier.
Maintenant, il me tarde juste d’être dans deux ans, revoir les scripts qui sont ouverts actuellement sur l’écran à ma droite et éclater de rire (ou de désespoir).
Bref, merci atrament pour cet article qui fait du bien à un débutant. Et merci S&M pour le reste des articles, je vous lis mais ne commente pas beaucoup, mais vos articles m’aident énormément.
Il n’y a pas de mauvais script, il n’y a que des mauvais problèmes ;)
Article sympa ;-)
Après avoir beaucoup bidouillé pleins de tests et micro-projets et avoir appris
grâce au traceback, forums et à S&M je butte sur un probleme:
l’ architecture.
Je fait des schemas UML de projets qui m’ intéressent mais j’ai du mal avec les design pattern. Les étudier en anglais en C++ c’est vraiment pas agréable.
Et quand j’en trouve quelques un en français Et en python, c’est vague, abstrait.
Je cherche des exemples percutants, détaillés sous toutes les coutures, en python et en français. Ouais je sais…
Et quand j’aurai trouvé ça, je me demande quel sera le prochain seuil.
Article assez intéressant. J’ai l’impression que les comparaisons dans les bouts de code passent mal (un problème de html entity escape j’imagine).
C’est cool que que tu aies mis la dernière version sur github, j’ai un projet perso très similaire https://github.com/SylvainDe/ComicBookMaker mais le tien a l’air mieux organisé, je vais pouvoir en prendre de la graine.
@buffalo je crois justement que les gars d’indexerror/sametmax avaient lancé un projet du genre, un repo github avec des exemples de patterns en python, mais hélas j’ai plus le lien, quelqu’un se souvient de ce dont je cause?
@Nattefrost
Oui c’est même @Buffalo qui a posé la question, donc je pense qu’il rechercher autre chose, je suppose que tu vises ce repo (d’IndexErrorCoders), mais ce n’est qu’un vulgaire fork, il faut le traduire en français, et faire des exemples plus user friendly. Si quelqu’un est chaud c’est l’occasion
@DrHaze:
En principe j’ai corrigé pour tenir compte de tes corrections orthographiques.
Merci de ce retour, c’est exactement pour ça que je l’ai écrit. Personne n’est devenu un dieu de la prog du jour au lendemain, pas même Sam ni Max. Personne n’a la sacro-sainte vérité universelle de python.
Mais pourtant, les débutants ont honte de montrer ce qu’ils font. Ils devraient être fiers, mais ils cachent leurs codes, se privant de progresser, et réinventant la roue les uns après les autres. C’est pas innocent, si j’ai choisi ce script de téléchargement de comics, parmi mes scripts que j’ai gardés. On en a tous fait un, en python ou pas. Et on doit à peu près tous être insatisfaits de nos premiers jets.
ET C’EST PAS GRAVE ! Quand un gamin fait ses premiers pas titubants, on le félicite, on ne le gronde pas parce qu’il ne va assez vite ou assez droit.
@boblinux : oui exactement. C’est pas facile de trouver un truc tout cuit, faut souvent s’arracher les cheveux pour comprendre un dixième de ce qu’on veut.
@ atrament : je pense que la honte vient du ratio energie dépensée / resultat obtenu.
Quand un proche te vois bosser de façon assidue, et qu’il te dit “montres moi ce que tu sais faire depuis l’temps…”.
Là tu as honte, parceque même sans intention de te vexer , il peut pas s’ empecher de pointer du doigt tout ce qui n’est pas implémenté, ou va de travers. Sans aucune perception de ce qui a le mérite de déjà fonctionner.
Mais si tu fais une merde avec une jolie interface, t’es la réincarnation de cyber-bouddah. Et si y’a un .exe, t’es un hacker !
Pour un giga-noob comme moi (pareil, j’ai fait trois scripts et demi pour rigger sous Maya), c’est super décourageant. Man, j’voulais tout savoir en deux semaines; et tu me dis que c’est long comme la vie ! En tout cas merci pour les notes c’est vraiment intéressant.
Quand je pense que mon script, pour récupérer des vidéos de certains sites, simplement print des wget +xarg commandes que je copy paste dans une nouvelle fenêtre de terminal, je me sens un peu con. Le vrai problème c’est qu’écrire ces scripts c’est l’éclate et je peux y passer tout mon week-end. :-)
Pourquoi te sentir con quand tu économise des heures de ta vie ?