Mes premiers pas sur Python - acte II

Il y a quelques semaines de cela, je réalisais mes premiers scripts en Python, langage que j'ai toujours voulu apprendre mais pour lequel je manquais de use case concrets, ayant plus l'habitude de Powershell, travaillant sur des environnements Microsoft. Cependant, par curiosité et envie d'apprendre, je planche volontiers à l'occasion sur Python et c'est aujourd'hui pour un besoin purement personnel que j'ai écrit ces deux scripts.

Pour replacer le contexte, je participe à des compétitions de speedrunning de jeux rétro. Un score est calculé en fonction du temps réalisé pour compléter le jeu en question. Récemment, je me suis demandé si il n'était pas intéressant de valoriser les temps d'exécution très rapides en doublant chaque point attribué sous les 2 heures plutôt que d'avoir un traitement linéaire comme actuellement : on part d'une base de 36000 points (1 par seconde, jusqu'à 10 heures) et l'on soustrait un point pour chaque seconde.

Par exemple, pour une partie complétée en 1 heure, 50 minutes et 28 secondes, on a donc un score de 36000 - 6628 (1 x 3600 + 50 x 60 + 28) soit 29372. Avec ce calcul, on a donc 29372 ; mais avec la nouvelle formule, 36000 - 6628 + 572 soit 29944. Les 572 points proviennent des 572 secondes qui séparent le chronomètre des 2 heures.

J'ai donc développé deux scripts Python permettant de faire le calcul. Le premier script demande à l'utilisateur de saisir à la suite les valeurs. Les boucles try et if permettent de faire les contrôles de robustesse nécessaires :

hrCount = input("Hours: ")
mnCount = input("Minutes: ")
seCount = input("Seconds: ")
try:
	hrCount = int(hrCount)
	mnCount = int(mnCount)
	seCount = int(seCount)
except:
	print("Please input integer values as hours, minutes and seconds.")
	quit()
if seCount >= 60:
	print("Seconds value can't be higher than 60.")
	quit()
if mnCount >= 60:
	print("Minutes value can't be higher than 60.")
	quit()
totalSec = hrCount*3600+mnCount*60+seCount
if totalSec > 36000:
	print("Timer can't be higher than 10 hours.")
	quit()
base = 36000 - totalSec
if totalSec < 7200:
	bonus = 7200 - totalSec
else:
	bonus = 0
sc = base + bonus
print(base)
print(bonus)
print(sc)

Le script affiche à la fin le score de base calculé linéairement, le bonus puis le total.

Le deuxième script effectue le même traitement mais avec une approche différente. J'ai voulu alléger un peu le code et les contrôles de robustesse en utilisant le module argparse. Ce module permet d'ajouter des paramètres en ligne de commande au script : ainsi c'est Python lui-même qui va vérifier le type d'entrée de mes arguments (ici, des integer) et permettre d'afficher une aide.

import argparse
parser = argparse.ArgumentParser(description='Counts all seconds under a 2-hour timer twice.')
parser.add_argument('hrCount', metavar='h', type=int, help='Number of hours')
parser.add_argument('mnCount', metavar='m', type=int, help='Number of minutes')
parser.add_argument('seCount', metavar='s', type=int, help='Number of seconds')
args = parser.parse_args()

if args.seCount >= 60:
	print("Seconds value can't be higher than 60.")
	quit()
if args.mnCount >= 60:
	print("Minutes value can't be higher than 60.")
	quit()
totalSec = args.hrCount*3600+args.mnCount*60+args.seCount
if totalSec > 36000:
	print("Timer can't be higher than 10 hours.")
	quit()
base = 36000 - totalSec
if totalSec < 7200:
	bonus = 7200 - totalSec
else:
	bonus = 0
sc = base + bonus
print(base)
print(bonus)
print(sc)

Voici ce que cela donne en exécution. Le paramètre -h est directement intégré et n'a pas à être codé dans le script.

Les deux scripts sont disponibles dans des versions commentées dans l'archive en lien ci-dessous. J'ai utilisé la version 3.9 de Python sous macOS pour les écrire et les utiliser.

Header error lors d'un tar x sur une archive splitée

Hier, j'ai transféré plusieurs Go de données sur un serveur distant sous Debian 10, sous forme d'une archive tar splitée en fichiers de 4 Mo. J'avais donc dans mon répertoire de destination un peu plus de 1000 fichiers au format MonArchive.tar.001.

Je tente un merge avec cat de ces fichiers afin d'avoir une seule grosse archive tar que je peux ensuite extraire.

cat MonArchive.tar.* > MonArchive.tar

Puis je tente une extraction :

tar xvf MonArchive.tar

Et je me heurte à une erreur d'header lors de l'extraction, s'arrêtant net après le troisième fichier sur une vingtaine présente dans l'archive. Je retente un cat et un tar mais sans succès. Ayant encore les fichiers d'origine sur ma station Windows, je décide donc de faire un comparatif des checksums respectifs pour m'assurer qu'il n'y ait pas de fichier qui ait été mal transféré.

Sur mon système Debian :

sha256sum MonArchive.tar.* > MonArchiveChecksum

Sous Windows, grâce à Powershell :

Get-FileHash * -Algorithm SHA256

Je récupère donc mes extractions, ouvre un Notepad++ et compare les fichiers : aucune différence de checksum. Je suis donc un peu surpris car cela veut dire que les fichiers sont a priori identiques entre la destination et la source. En conclusion, le problème semble se situer au niveau du merge de mes multiples archives.

Au détour d'un banal ls sur mon système Debian, j'ai compris d'où venait le problème. A l'heure où j'écris cet article (il était 1 heure du matin quand je me suis penché sur le sujet 😅), je n'ai plus les archives sur mon système Debian mais je les ai régénérées sur mon système Windows, et la cause est identique. Voici le résultat d'une commande ls sur Powershell :

Les fichiers étaient nommés MonArchive.tar.001, MonArchive.tar.002 jusque MonArchive.tar.1032. On passe donc une numérotation sur 3 chiffres à 4 en arrivant au millier ; ce qui va fatalement poser problème pour cat puisqu'il fusionne les fichiers par ordre alphabétique. Or, par ordre alphabétique, il y a donc MonArchive.tar.100, MonArchive.tar.1000, MonArchive.tar.1001, MonArchive.tar.1002... avant de finalement continuer sur MonArchive.tar.101, MonArchive.tar.1010. Par conséquent, l'archive est mal constituée puisque pas concaténée dans l'ordre : cela ne serait pas arrivé si toute la numérotation avait été faite sur 4 chiffres dès le départ (7-zip pour Windows est l'outil de compression d'origine).

J'ai donc procédé en plusieurs étapes : isolation des fichiers tar à 4 chiffres, concaténation d'une première archive avec les 999 premiers fichiers tar, puis concaténation d'une deuxième archive avec les derniers fichiers, et enfin fusion des deux archives pour extraction. Ensuite, j'ai pu vérifier l'intégrité apparente de l'archive finale :

tar tvf MonArchiveFinale.tar

Tous les fichiers lisibles de l'archive sont apparus, j'ai donc pu les extraire 😊

Mes premiers pas sur Python - acte I

Cela faisait quelques temps que je m'intéressais à Python par curiosité et envie d'apprendre quelque chose de nouveau, et cela me permettra d'ajouter une corde à mon arc car à la différence de Powershell, Python fonctionne sur un grand nombre de systèmes d'exploitation. Ce qu'il me manquait pour commencer Python, c'était réellement un besoin, une idée, quelque chose que je pouvais faire avec un autre langage mais pour laquelle il n'y avait aucun enjeu à ce que je prenne plus de temps où qu'elle soit faite dans un autre langage. J'ai finalement trouvé l'inspiration pour un premier bout de code en Python grâce à la table périodique des éléments.

J'ai donc développé mes 2 premiers scripts Python sur ce thème. Le premier demande à l'utilisateur de saisir un symbole de cette table pour lui retourner l'élément correspondant ainsi que son numéro atomique :

import csv
eleminput = input("Enter a symbol:")
with open('PubChemElements_all.csv', newline='') as tablecsv:
	pertbl = csv.reader(tablecsv,delimiter=',')
	for row in pertbl:
		elemread = row[0]
		if elemread == eleminput:
			print(elemread + " is " + row[1] + " and is atomic number " + row[2])

La manière d'ouvrir le fichier CSV est un peu déroutante au départ. En Powershell, la commande Import-CSV permet de le faire en une ligne. Ici, il est nécessaire d'appeler le fichier dans une variable puis d'instancier un objet depuis la classe csv. Le reste est plutôt logique et l'on adresse chaque élément de la ligne parcourue ici nommée row comme un tableau dont l'index commence par 0, comme en .NET.

Le deuxième est un peu plus complexe car il s'agit d'un "jeu" : Python choisit au hasard un élément parmi la liste et expose le nom de cet élément à l'utilisateur qui doit en deviner le symbole :

import csv
import random
randelem = random.randint(1, 118)
with open('PubChemElements_all.csv', newline='') as tablecsv:
	pertbl = csv.reader(tablecsv,delimiter=',')
	for row in pertbl:
		if pertbl.line_num == randelem:
			elemsym = row[0]
			elemname = row[1]
eleminput = input("Please input the symbol for element " + elemname + " : ")
if eleminput == elemsym:
	print("Well done! " + elemsym + " is the symbol for " + elemname)
else:
	print("Sorry! " + elemname + " has " + elemsym + " as symbol")

Voici ce que cela donne :

J'ai donc importé le module random pour générer de manière aléatoire un nombre. Etant donné que mon fichier CSV comporte 118 éléments, je vais chercher un nombre allant de 1 à 118 - il est important de garder à l'esprit que les index commencent à zéro en Python mais ici cela n'a pas d'importance car je cherche à faire concorder cette valeur avec un nombre de lignes parcourues, et ce nombre ne peut pas être égal à zéro - et je parcours ensuite mon fichier CSV en comptant le nombre de lignes jusqu'à arriver au nombre choisi aléatoirement en début de code. Une fois que je tombe sur celui-ci, je stocke dans deux variables le symbole et le nom de l'élément, qui me permettront de faire la vérification de la réponse de l'utilisateur ainsi que l'affichage de l'élément à trouver.

J'ai joué ces scripts sur un environnement macOS Catalina avec Python 3.8. J'ai regroupé les deux scripts ainsi que le fichier CSV d'entrée dans un fichier au format zip téléchargeable directement depuis le miroir du blog.

Manipulation du pagefile avec Powershell et scripting diskpart

Récemment, j'ai modifié mon template de machine virtuelle Windows Server porté par VMware pour y intégrer un deuxième disque virtuel afin d'y placer le fichier d'échange de Windows. Seulement, après génération de mon image Windows avec sysprep, mon deuxième disque n'est plus monté sur le système, et par conséquent ma configuration de fichier d'échange est invalide. J'ai donc dû traiter ce problème en deux temps : la première étape a été de faire en sorte de faire un batch pour diskpart afin de monter mon deuxième disque et l'initialiser correctement, et la deuxième étape a été de configurer le pagefile via Powshell afin de pouvoir intégrer le tout dans mon script de post-installation.

Tout d'abord, j'ai appris qu'il était possible de faire avaler à diskpart un fichier texte contenant les instructions qu'il doit exécuter à la suite. Par exemple, si je veux initialiser mon deuxième disque et affecter la lettre E à ma partition, je vais créer un fichier texte nommé "diskinit.txt" contenant ceci :

select disk 1
online disk
select partition 1
assign letter=E

Je conseille tout de même de tester une par une les commandes à la main pour s'assurer que cela fonctionne avant de les placer à la suite dans le fichier. Une fois que le fichier existe, il est possible d'appeler diskpart pour lui demander de traiter celui-ci, grâce au paramètre /s :

diskpart.exe /s diskinit.txt

Puisque cela fonctionne, j'ai dû m'atteler à la gestion du fichier d'échange sous Powershell. Il n'y a pas de commandlet natif sous Windows Server 2016 pour gérer le fichier d'échange comme un paramètre système standard. Il faut passer par WMI. Cette commande permet d'afficher les fichiers d'échange configurés sur la machine, uniquement si le fichier d'échange n'est pas gérée de manière automatique pour tous les disques du système :

Get-CimInstance -ClassName Win32_PagefileSetting

Maintenant que j'ai bien mon disque E:, je peux rajouter mon fichier d'échange sur cette partition et retirer celui présent sur le disque système.

New-CimInstance -ClassName Win32_PagefileSetting -Property @{Name "E:\pagefile.sys"}
Get-CimInstance -ClassName Win32_PagefileSetting | Where-Object { $_.Name -like "C:\pagefile.sys" } | Remove-CimInstance

A noter que cette instruction créée un fichier d'échange dont la taille est gérée par le système. Il est possible de spécifier une taille minimale et/ou maximale, en procédant par exemple comme ceci pour une taille mini de 1024 Mo et une taille maxi de 2048 Mo :

Get-CimInstance -ClassName Win32_PagefileSetting | Where-Object { $_.Name -like "E:\pagefile.sys" } | Set-CimInstance -Property @{InitialSize = 1024; MaximumSize = 2048}

Alternativement, ce qui est passé dans le dernier pipe peut être passé en pipe de l'instruction New-CimInstance exécutée en premier, afin d'avoir le bon réglage dès la création.

Vous pouvez trouver avoir bien plus de détails sur cette classe WMI et un script complet de gestion du pagefile via Powershell en suivant ce lien qui m'a aidé à comprendre et à scripter cette opération. 🙂