Cours CMI / M1 Données - séance 2
Résumé de la séance 2
On s'entraine ici à produire des scripts un peu plus conséquents et à utiliser Perl et Python pour "enrober" des commandes qui traitent des listes de fichiers. On apprend aussi à exécuter des scripts en tache de fond avec nohup.
1. Scripts et traitement de fichiers par lots
1.1 Traitements bioinformatiques et scripts
La bioinformatique regorge de scripts et de programmes à exécuter, avec des paramètres plus ou moins compliqués, avec des durées plus ou moins longues. Pour les utiliser efficacement, une fois installés, il faut souvent les évaluer, les tester avant de passer à leur exécution en masse ou en routine. Pour cela, il est bon de savoir réaliser un certain nombre d'actions classiques comme
afficher l'aide sur les paramètres avec des exemples ;
tester la présence d'un fichier ;
gérer des durées ;
traiter des fichiers fournis par une spécification ambigue ou par une liste de fichiers ;
exécuter des programmes qui durent plusieurs heures, plusieurs jours.
Aucune de ces taches n'est difficile en soi si on sait programmer et si on a beaucoup de rigueur. Toutefois écrire un script de plusieurs dizaines de lignes est un travail ingrat et long quand on débute. C'est pourquoi nous fournissons ici des modèles de scripts prêts à l'emploi ou, plus exactement prêts à être complétés en fonction des traitements bioinformatiques à réaliser.
Pour éviter de perdre du temps à écrire des fichiers, chacun des scripts est téléchargeable, par exemple avec wget. Nous fournissons aussi une archive dataCmi.zip qui contient des exemples de fichiers de données à traiter.
1.2 Quelques actions classiques pour les scripts
Afficher l'aide si -h ou --help
Fichier aide
# # (gH) -_- aide ; TimeStamp (unix) : 23 Février 2019 vers 22:50 if [[ -n $1 ]] ; then if [[ $1 = "-h" ]] || [[ $1 = "--help" ]] then echo "Voici l'aide sur le script..." exit 1 fi # fin si le paramètre est -h ou --help fi # fin si il y a des paramètres echo " Exécution normale du programme..."Fichier aide.pl
# # (gH) -_- aide.pl ; TimeStamp (unix) : 23 Février 2019 vers 22:13 use strict ; use warnings FATAL => 'all'; if ($#ARGV > -1) { if (($ARGV[0] eq "-h") or ($ARGV[0] eq "--help")) { print "Voici l'aide sur le script...\n" ; exit(1) ; } ; # fin si le paramètre est -h ou --help } ; # fin si il y a des paramètres print " Exécution normale du programme...\n" ;Fichier aide.py
# -*- coding:latin1 -*- # # (gH) -_- aide.py ; TimeStamp (unix) : 05 Mars 2019 vers 15:35 import sys if (len(sys.argv)>1) : if (sys.argv[1]=="-h") or (sys.argv[1]=="--help") : aide = ''' Voici l'aide sur le script... ''' print(aide) sys.exit(1) # fin de si le paramètre est -h ou --help # fin de si il y a des paramètres print(" Exécution normale du programme...")Tester les paramètres comme la présence d'un fichier avant d'exécuter une commande
Fichier testefic
# (gH) -_- testefic ; TimeStamp (unix) : 23 Février 2019 vers 23:15 # si pas de paramètre, rappel de la syntaxe et sortie if [ -z $1 ] ; then echo syntaxe : testefic NOM_DE_FICHIER exit 1 fi # fin si pas de paramètre # si le paramètre est présent, on teste que c'est un fichier qui existe if [ ! -r $1 ] ; then echo fichier $1 non vu. exit 2 fi # finsi echo Exécution normale du programme...Fichier testefic.pl
# # (gH) -_- testefic.pl ; TimeStamp (unix) : 23 Février 2019 vers 23:12 use strict ; use warnings FATAL => 'all'; # si pas de paramètre, rappel de la syntaxe et sortie if ($#ARGV == -1) { print "syntaxe : perl testefic.pl NOM_DE_FICHIER\n" ; exit(1) ; } ; # fin si pas de paramètre # si le paramètre est présent, on teste que c'est un fichier qui existe my $nomFic = $ARGV[0] ; if (! -e $nomFic) { print "fichier $nomFic non vu.\n" ; exit(2) ; } ; # finsi print " Exécution normale du programme...\n" ;Fichier testefic.py
# -*- coding:latin1 -*- # # (gH) -_- testefic.py ; TimeStamp (unix) : 23 Février 2019 vers 22:27 import sys import os # si pas de paramètre, rappel de la syntaxe et sortie if (len(sys.argv)==1) : print("syntaxe : python3 testefic.py NOM_DE_FICHIER") sys.exit(1) # fin de si pas de paramètre # si le paramètre est présent, on teste que c'est un fichier qui existe nomFic = sys.argv[1] if (not os.path.isfile(nomFic)) : print("fichier "+ nomFic + " non vu. STOP.") sys.exit(2) # fin de si fichier non vu print(" Exécution normale du programme...")1.3 Comparaison des scripts Bash, Perl et Python
Les quelques exemples précédents montrent que les actions standards sont toutes réalisables avec les trois langages de scripts. Les scripts les plus lisibles sont sans doute ceux écrits en Python, les plus efficaces ceux écrits en Bash si on ne fait que d'exécuter des commandes systèmes, les plus efficaces pour le traitement de données textuelles (dont les séquences Fasta) ceux écrits en Perl.
Au final, il n'y a sans doute aucune raison de privilégier l'un de ces trois langages de script. Python et Perl ont toutefois l'avantage d'offrir des instructions de haut niveau et de nombreuses fonctions de calcul, conversion, que ce soit numérique ou textuel. Les bibliothèques Python fournissent aussi de nombreuses solutions "clés en main" pour de nombreux problèmes classiques.
Nous vous encourageons donc à vous entrainer à maitriser l'écriture de petits scripts dans ces trois langages avant d'approfondir celui qui vous plait le plus.
Par contre, dès que l'on veut effectuer des calculs, de la mise en forme "propre", les langages Python et Perl sont beaucoup plus agréables et faciles à utiliser.
2. Scripts Perl pour traiter des longues listes de fichiers
On suppose ici qu'on a une liste de fichiers à traiter, comme par exemple listeFic1.txt. Pour l'exemple ce fichier ne contient que peu de lignes, mais le script reste valable pour plusieurs centaines ou milliers de lignes. Les fichiers à utiliser sont vides et presque tous présents, ce qui suffira pour la démonstration.
Vous pouvez les récupérer dans l'archive dataCmi.zip.
Comme vu à la séance 1, une liste de fichiers peut être obtenue via la commande ls et la redirection des sorties, comme par exemple avec la commande
ls fichier*txt > listeFic1.txtPour le premier script, nous supposons que la liste des fichiers est contenue dans listeFic1.txt. Le script Perl fournit un affichage minimal et incomplet (pourquoi ?) :
# # (gH) -_- traiteListe1.pl ; TimeStamp (unix) : 04 Mars 2019 vers 13:54 use strict ; use warnings FATAL => 'all'; my $listeFic = "listeFic1.txt" ; # liste des fichiers à traiter open(LISTEFIC ,"<$listeFic") || die "\n impossible to read from the file $listeFic \n\n" ; my $nbFic = 0 ; while (my $nomFichier = <LISTEFIC>) { chop($nomFichier) ; $nbFic++ ; print "Traitement du fichier $nbFic : $nomFichier\n" ; } ; # fin tant que close(LISTEFIC) ;$gh> perl traiteListe1.pl Traitement du fichier 1 : fichier01.txt Traitement du fichier 2 : fichier02.txt Traitement du fichier 3 : fichier03.txt Traitement du fichier 4 : fichier05.txtPour le deuxième script, la liste des fichiers est fournie en paramètre. et nous fournissons un meilleur affichage :
# # (gH) -_- traiteListe2.pl ; TimeStamp (unix) : 04 Mars 2019 vers 13:59 use strict ; use warnings FATAL => 'all'; # si pas de paramètre, rappel de la syntaxe et sortie if ($#ARGV == -1) { print "syntaxe : perl traiteListe2.pl NOM_DE_FICHIER\n" ; exit(1) ; } ; # fin si pas de paramètre # si le paramètre est présent, on teste que c'est un fichier qui existe my $listeFic = $ARGV[0] ; if (! -e $listeFic) { print "fichier $listeFic non vu.\n" ; exit(2) ; } ; # finsi # exécution normale du programme open(LISTEFIC ,"<$listeFic") ; my $nbFic = 0 ; while (my $nomFichier = <LISTEFIC>) { chop($nomFichier) ; $nbFic++ ; if (!-e $nomFichier) { print " ==> Fichier numéro $nbFic : $nomFichier non vu \n" ; } else { print "Traitement du fichier ".sprintf("%4d",$nbFic)." : $nomFichier \n" ; } ; # fin si } ; # fin tant que close(LISTEFIC) ;$gh> perl traiteListe2.pl syntaxe : perl traiteListe2.pl NOM_DE_FICHIER $gh> perl traiteListe2.pl listeFic.txt fichier listeFic.txt non vu. $gh> perl traiteListe2.pl listeFic1.txt Traitement du fichier 1 : fichier01.txt Traitement du fichier 2 : fichier02.txt Traitement du fichier 3 : fichier03.txt Traitement du fichier 4 : fichier05.txt $gh> perl traiteListe2.pl listeFic2.txt Traitement du fichier 1 : fichier01.txt Traitement du fichier 2 : fichier02.txt Traitement du fichier 3 : fichier03.txt ==> Fichier numéro 4 : fichier04.txt non vu Traitement du fichier 5 : fichier05.txtPour le troisième script, nous générons la liste à la volée et nous ajoutons la date et l'heure. Le programme nommé quidure est un script Bash qui dure de 2 à 8 secondes.
# # (gH) -_- traiteListe3.pl ; TimeStamp (unix) : 04 Mars 2019 vers 14:06 use strict ; use warnings FATAL => 'all'; # construction à la volée de la liste des fichiers my @list = split(/\n/,`ls fichier*txt`) ; # comptage explicite du nombre de fichiers my $nbf = 0 ; foreach my $file (@list) { $nbf++ ; } ; # fin pour chaque fichier # boucle de traitement my $idf = 0 ; foreach my $file (@list) { $idf++ ; print "Traitement du fichier ".sprintf("%3d",$idf)." / $nbf : $file ".`date`."\n" ; system("quidure") ; } ; # fin pour chaque fichier$gh> perl traiteListe3.pl Traitement du fichier 1 / 4 : fichier01.txt lundi 4 mars 2019, 14:22:16 (UTC+0100) Traitement du fichier 2 / 4 : fichier02.txt lundi 4 mars 2019, 14:22:18 (UTC+0100) Traitement du fichier 3 / 4 : fichier03.txt lundi 4 mars 2019, 14:22:20 (UTC+0100) Traitement du fichier 4 / 4 : fichier05.txt lundi 4 mars 2019, 14:22:22 (UTC+0100)Enfin, pour le quatrième script, nous calculons la durée totale du script.
# # (gH) -_- traiteListe3.pl ; TimeStamp (unix) : 04 Mars 2019 vers 14:06 use strict ; use warnings FATAL => 'all'; use POSIX ; ############################################################################################################### # # sous-programmes # ############################################################################################################### sub dateEtHeure { ################################################################################# my ($sec,$min,$hour,$mday,$mmon,$year)=localtime(); $mmon = $mmon + 1 ; $year = $year + 1900 ; #if (length($sec)<2) { $sec = "0$sec" } ; #if (length($mday)<2) { $mday = "0$mday" } ; #if (length($mmon)<2) { $mmon = "0$mmon" } ; $sec = sprintf("%02d",$sec) ; $min = sprintf("%02d",$min) ; $mday = sprintf("%02d",$mday) ; $mmon = sprintf("%02d",$mmon) ; $hour = sprintf("%02d",$hour) ; my $now = $mday."/".$mmon."/".$year; $now .= " ".$hour.":".$min ; return $now ; } ; # fin sub dateEtHeure ################################################################################# sub duration { ################################################################################# my $temps1 = $_[0] ; my $temps2 = $_[1] ; my $option = $_[2] ; my $durationInSeconds = $temps2 - $temps1 ; my $retStr = "" ; if ($option eq "long") { $retStr .= "Elapsed time:" ; } ; my $years = floor($durationInSeconds / 31536000) ; $durationInSeconds -= $years * 31536000 ; my $days = floor($durationInSeconds / 86400); $durationInSeconds -= $days * 86400; my $hours = floor($durationInSeconds / 3600); $durationInSeconds -= $hours * 3600; my $minutes = floor($durationInSeconds / 60); my $seconds = $durationInSeconds - $minutes * 60; if($years > 0) { $retStr .= $years . ' an'; if ($years>1) { $retStr .= 's' ; } ; } ; # fin si if($days > 0) { $retStr .= ' '.$days . ' jour'; if ($days>1) { $retStr .= 's' ; } ; } ; # fin si if($hours > 0) { $retStr .= ' ' . $hours . ' h'; } ; # fin si if($minutes > 0) { $retStr .= ' ' . sprintf("%02d",$minutes) . ' min'; } ; # fin si if($seconds > 0) { $retStr .= ' ' . sprintf("%02d",$seconds) . ' sec'; } ; # fin si if (($retStr eq "") or ($retStr eq "Elapsed time:") ) { $retStr .= " 0 seconds" ; } ; return( $retStr ) ; } ; # fin sub duration ############################################################################################################### # message de début et initialisation print "\n" ; my $timeDeb = time() ; print &dateEtHeure().". DEBUT DE TRAITEMENT\n" ; print "\n" ; # construction à la volée de la liste des fichiers my @list = split(/\n/,`ls fichier*txt`) ; # comptage explicite du nombre de fichiers my $nbf = 0 ; foreach my $file (@list) { $nbf++ ; } ; # fin pour chaque fichier # boucle de traitement my $idf = 0 ; foreach my $file (@list) { $idf++ ; print "Traitement du fichier ".sprintf("%3d",$idf)." / $nbf : $file ".`date` ; # pas besoin de ."\n" ; system("quidure") ; } ; # fin pour chaque fichier print "\n" ; my $timeFin = time() ; my $dure = &duration($timeDeb,$timeFin,"long") ; print &dateEtHeure().". FIN DE TRAITEMENT. $dure.\n\n" ;$gh> perl traiteListe4.pl 04/03/2019 14:22. DEBUT DE TRAITEMENT Traitement du fichier 1 / 4 : fichier01.txt lundi 4 mars 2019, 14:22:33 (UTC+0100) Traitement du fichier 2 / 4 : fichier02.txt lundi 4 mars 2019, 14:22:35 (UTC+0100) Traitement du fichier 3 / 4 : fichier03.txt lundi 4 mars 2019, 14:22:37 (UTC+0100) Traitement du fichier 4 / 4 : fichier05.txt lundi 4 mars 2019, 14:22:39 (UTC+0100) 04/03/2019 14:22. FIn DE TRAITEMENT. Elapsed time: 08 sec.3. Scripts Python pour traiter des longues listes de fichiers
Fort des expériences précédentes avec les script Perl, nous pouvons écrire la même progression dans le traitement de listes de fichiers :
Script 1 : traiteListe1.py
# -*- coding:latin1 -*- # # (gH) -_- traiteListe1.py ; TimeStamp (unix) : 04 Mars 2019 vers 15:28 import sys import os listeFic = "listeFic1.txt" # liste des fichiers à traiter # on sort si le fichier n'existe pas if (not os.path.isfile(listeFic)) : print("fichier "+ listeFic + " non vu. STOP.") sys.exit(2) # fin de si fichier non vu # parcours des fichiers de la liste nbFic = 0 with open(listeFic) as LISTEFIC : for nomFichier in LISTEFIC : nomFichier = nomFichier.strip() nbFic += 1 print("Traitement du fichier " + str(nbFic)+ " : " + nomFichier) # fin pour LISTEFIC.close() # fin d'ouverture de fichierScript 2 : traiteListe2.py
# -*- coding:latin1 -*- # # (gH) -_- traiteListe2.py ; TimeStamp (unix) : 04 Mars 2019 vers 15:44 import sys import os # si pas de paramètre, rappel de la syntaxe et sortie if (len(sys.argv)==1) : aide = ''' syntaxe : python3 traiteListe2.py NOM_DE_FICHIER ''' print(aide) sys.exit() # fin de si # on sort si le fichier n'existe pas listeFic = sys.argv[1] if (not os.path.isfile(listeFic)) : print("fichier "+ listeFic + " non vu. STOP.") sys.exit(2) # fin de si fichier non vu # parcours des fichiers de la liste nbFic = 0 with open(listeFic) as LISTEFIC : for nomFichier in LISTEFIC : nomFichier = nomFichier.strip() nbFic += 1 if (not os.path.isfile(nomFichier)) : print(" ==> Fichier numéro " + str(nbFic)+ " : " + nomFichier + " non vu ") else : print("Traitement du fichier " + ('%4d' % nbFic) + " : " + nomFichier)Script 3 : traiteListe3.py
# -*- coding:latin1 -*- # # (gH) -_- traiteListe3.py ; TimeStamp (unix) : 04 Mars 2019 vers 15:53 import pathlib import subprocess # lecture à la volée de la liste des fichiers currentDirectory = pathlib.Path('.') currentPattern = "fichier*.txt" # comptage explicite du nombre de fichiers nbf = 0 for file in currentDirectory.glob(currentPattern): nbf += 1 idf = 0 for file in currentDirectory.glob(currentPattern): idf += 1 dateEtHeure = subprocess.run("date", stdout=subprocess.PIPE) print("Traitement du fichier " + ('%4d' % idf) + " / " + str(nbf) + " : " + str(file ) + dateEtHeure.stdout.decode(),end="") #system("quidure") ;Script 4 : traiteListe4.py
# -*- coding:latin1 -*- # # (gH) -_- traiteListe4.py ; TimeStamp (unix) : 04 Mars 2019 vers 16:03 import pathlib import subprocess import datetime from os import system # lecture à la volée de la liste des fichiers currentDirectory = pathlib.Path('.') currentPattern = "fichier*.txt" # comptage explicite du nombre de fichiers nbf = 0 for file in currentDirectory.glob(currentPattern): nbf += 1 dateEtHeure = subprocess.run("date", stdout=subprocess.PIPE) print(dateEtHeure.stdout.decode().strip() + "DEBUT DE TRAITEMENT" ) date1 = datetime.datetime.now() idf = 0 for file in currentDirectory.glob(currentPattern): idf += 1 dateEtHeure = subprocess.run("date", stdout=subprocess.PIPE) print("Traitement du fichier " + ('%4d' % idf) + " / " + str(nbf) + " : " + str(file ) + dateEtHeure.stdout.decode(),end="") system("./quidureLongtemps") ; dateEtHeure = subprocess.run("date", stdout=subprocess.PIPE) date2 = datetime.datetime.now() print(dateEtHeure.stdout.decode().strip() + "FIN DE TRAITEMENT. Duration ", str(date2 - date1))4. Commandes pour gérer des traitements longs
Les scripts présentés ci-dessus sont prévus pour s'exécuter en mode terminal dans un temps "raisonnable". Si on ferme le terminal, si on ferme la fenêtre associée ou si on éteint l'ordinateur, alors le script s'arrête et tout est perdu. Comment faire alors pour exécuter des traitements longs ou très longs ?
Tout d'abord, il faut soit laisser l'ordinateur allumé, soit disposer d'un serveur qui, par définition, fonctionne en permanence. Ensuite, il ne faut pas lancer le script directement, mais le lancer en tâche de fin via la commande nohup. Lorsque nohup est lancé, on peut fermer le terminal, le script continue de s'exécuter. On peut ensuite consulter dans le fichier nohup.out ce qui est affiché par le script -- si on bien pensé à lui faire écrire ce qu'il fait !
En cours d'exécution, il est possible consulter en temps réel ce qu'écrit le script via la commande tail -f nohup.out.
Voici un exemple d'utilisation :
------- commandes du lundi $gh> # lundi, 9 h $gh> rm nohup.out # mais pourquoi ? $gh> nohup py monScriptTresLong.py & $gh> # lundi, 14 h $gh> tail -f nohup.out fichier 1589 / 3677411 fichier 1590 / 3677411 fichier 1591 / 3677411 ... # ici on tape ^C pour arreter le tail -f $gh> exit ------- commandes du mardi $gh> # mardi, 14 h $gh> tail -n 3 nohup.out fichier 35678 / 3677411 fichier 35679 / 3677411 fichier 35680 / 3677411 ------- une semaine plus tard $gh> tail -n 3 nohup.out fichier 3677411 / 3677411 (gH) : fin du script très long.5. Quelques exemples de scripts Python et Perl sophistiqués
Vous trouverez ci-dessous quelques exemples de script Perl et Python nettement plus sophistiqués. Certains ont des pages Web d'explications, d'autres sont disponibles tels quels. Lire les scripts des autres est souvent très riche d'enseignement...
6. Questions/réponses pour tester vos connaissances
Question 6.1
Pourquoi faut-il noter les fichiers fic001.txt plutôt que fic1.txt ?
Question 6.2
A quoi sert la partie de code sprintf("%4d",$nbFic) dans le fichier traiteListe2.pl ?
Quel est son équivalent dans le fichier traiteListe2.py ?
Question 6.3
Traduire le script Bash nommé regnes en un script Python ou Perl, au choix.
Serait-ce beaucoup plus difficile de compter combien de fois chaque règne est vu dans le fichier ?
Voici ce qu'on doit trouver :
774 lignes vues dans lea.tsv 1 : Alveolata 1 2 : Bacteria 38 3 : Euryarchaeota 1 4 : Fungi 11 5 : Metazoa 23 6 : Parabasalidea 1 7 : Viridiplantae 698 Total : 773 pour 7 règnesSolutions : afficher les solutions
Réponse à la question 6.1
Parce que l'ordre des noms de fichiers suit l'ordre du dictionnaire. on trouve donc fic11.txt juste après fic1.txt et malheureusement avant fic2.txt.
Réponse à la question 6.2
Cela permet de justifier à droite sur 4 caractères, de façon à avoir un bel affichage cadré. L'équivalent en Python est '%4d' % nbFic.
Réponse à la question 6.3
Solution volontairement non communiquée pour l'instant.
Cliquer ici pour revenir à la page de départ des cours CMI / M1.
Retour à la page principale de (gH)