Ouverture d'un fichier comprenant des caractères « très spéciaux »

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous !

J’ai réalisé un petit programme sur Python 3 qui parcourt les images PNG d’un dossier et les compresse avec OptiPNG :

# coding: utf-8
import os

files = os.listdir("C:\\Users\\Green\\Images")
for file in files:
    if file.lower().endswith('.png'):
        os.system('optipng "' + file + '"')

# Facultatif
os.system('pause')

Le souci

Le programme fonctionne même si le nom des fichiers images comporte des chiffres, des espaces et même des caractères accentués.
En revanche, il ne supporte pas les caractères un peu « trop spéciaux » comme ♘ :


Que faire ?

Je me rappelle avoir lancé une discussion similaire, mais le cas présent me semble toutefois assez différent vu qu’ici, les caractères accentués ne posent pas de problème. Cela ne semble pas être à cause UTF-8, donc. J’utilise Python 3 sur Windows 8. Auriez-vous des idées pour résoudre le souci ?

Merci d’avance ! :)

L’utilisation de la méthode os.system() te fait dépendre des règles d’interprétation du shell sous-jacent, ce qui peut induire des problèmes avec les caractères, et ce qui t’oblige à gérer des guillemets comme tu le fais. Je ne connais pas assez bien Windows pour en être sûr, mais peut-être que le problème vient de cela.

Il est conseillé d’invoquer directement le programme voulu (optipng) sans détour grâce au module standard subprocess comme cela :

import subprocess

p = subprocess.Popen(['optipng', file])
p.wait()

# ou bien (au choix)

subprocess.run(['optipng', file])

Tu remarqueras qu’on n’a plus besoin de gérer les guillemets car la sémantique est quelque peu différente : on ne demande pas au shell d’interpréter optipng "[file]", mais à la place on lance le programme optipng avec comme paramètre la valeur de file.

+0 -0

Merci pour les explications !
Malheureusement, le code ne fonctionne pas et renvoie la même erreur :

# coding: utf-8
import os, subprocess


files = os.listdir("C:\\Users\\Green\\Images")

for file in files:
    if file.lower().endswith('.png'):
        subprocess.run(['optipng', file])


# Facultatif
os.system('pause')

Salut,

Tu devrais essayer en utilisant une bytestring plutôt qu’une chaîne de caractères comme nom de fichier : b'Green\xe2\x99\x98.png' (en considérant que ton nom de fichier est encodé en UTF-8).

La lib standard les accepte donc ça devrait aller, mais il n’est pas improbable que ton problème vienne d'optipng et non de Python qui gère correctement les chaînes unicode.

question bête : cela fonctionne si tu le fais "manuellement" en ligne de commande ?

Angelo

Malheureusement non, même en copiant-collant le caractère spécial :

C:\Users\Green>cd Images

C:\Users\Green\Images>optipng Green?.png
** Processing: Green?.png
Error: Can't open the input file

** Status report
1 file(s) have been processed.
1 error(s) have been encountered.

C:\Users\Green\Images>

La console ne reconnaît pas le caractère ♘.

@entwanne Je vais tester pour voir !

Ou essayer du globbing (je ne sais pas quelles possibilités tu as depuis Windows) en tapant optipng Green*.png dans ta console.

Mais je n’avais pas vu que dès le premier screen tu avais ce « ** Processing: Green?.png », c’est donc bien optipng qui te renvoie ça et ne gère pas ton nom de fichier.

Salut,

Plutôt que d’appeler un programme externe, tu pourrais utiliser pillow avec l’option optimize de la méthode save pour compresser au maximum sans perte le PNG obtenu.

Par ailleurs, le module os en Python devrait être réservé à des cas particuliers de programmation système lorsque tu n’as pas le choix (comprendre, tu as peu de chances d’en avoir besoin). En l’occurrence, subprocess.run est la façon standard d’appeler un programme externe, pathlib est le module à utiliser pour manipuler des chemins et explorer des dossiers, et input() remplace de façon portable l’appel system à pause.

+2 -0

Re,

il faut jouer avec l’auto complétion voir ce que ça donne…

Angelo

L’autocomplétion renvoie le même résultat dans la console.
Quand j’appuie sur TAB pour anticiper, il m’écrit toujours Green?.png.

Ou essayer du globbing (je ne sais pas quelles possibilités tu as depuis Windows) en tapant optipng Green*.png dans ta console.

Mais je n’avais pas vu que dès le premier screen tu avais ce « ** Processing: Green?.png », c’est donc bien optipng qui te renvoie ça et ne gère pas ton nom de fichier.

entwanne

Idem avec le globbing.
La console interprète littéralement le * sans essayer de trouver le fichier correpondant.

Si c’est bien optipng le problème, et que je ne trouve aucune autre solution, je compte essayer la solution d'@adri1 et utiliser pillow.

Par ailleurs, le module os en Python devrait être réservé à des cas particuliers de programmation système lorsque tu n’as pas le choix (comprendre, tu as peu de chances d’en avoir besoin). En l’occurrence, subprocess.run est la façon standard d’appeler un programme externe, pathlib est le module à utiliser pour manipuler des chemins et explorer des dossiers, et input() remplace de façon portable l’appel system à pause.

adri1

J’ai pris en compte tes remarques, malheureusement cela ne semble pas régler le souci. J’ai testé en faisant une copie que j’ai nommé Green.png afin de comparer le comportement avec Green♘.png :

# coding: utf-8

import subprocess
from pathlib import Path


# Recherche des fichiers PNG
files  = Path('.')
images = list(files.glob('*.png'))

# Affiche les images trouvées
print(str(images[0])) # Green.png
print(str(images[1])) # Green♘.png

# Compression
subprocess.run(['optipng', str(images[0])]) # Compression réussie !
subprocess.run(['optipng', str(images[1])]) # Compression ratée !


# Pause
input()

Demain, j’essaierai une autre solution :

  • Renommer temporairement le fichier en temp.png ;
  • le compresser avec OptiPNG ;
  • puis lui redonner son nom initial.

Si j’ai bien compris l’article de @SpaceFox, il me suffit d’encoder mes entrées en utf-8 et cela devrait bien fonctionner.

En dernier des derniers recours, j’ouvre l’image en python et je passe directement l’image (sans son nom !) à OptiPNG. Sinon, j’utiliserai pillow.

Bref, bonne nuit ! :)

J’ai pris en compte tes remarques, malheureusement cela ne semble pas régler le souci.

C’est parfaitement normal, les deux codes font essentiellement la même chose. Sauf que dans un cas tu manipules des abstractions bien faites et portables, et dans l’autre tu t’embêtes pour rien.

Si j’ai bien compris l’article de @SpaceFox, il me suffit d’encoder mes entrées en utf-8 et cela devrait bien fonctionner.

Ça n’a pas grand chose à voir avec le problème. Visiblement, les chemins que tu récupères sont corrects vu que le print(images[1]) te sort le bon nom de fichier. C’est optipng qui galère. C’est donc plutôt ta sortie que tu dois contrôler. Tu peux tenter de lui passer str(images[1]).encode('cp1252') pour utiliser l’encodage typique de windows ou str(images[1]).encode('utf8') pour forcer un encodage en utf8 (l’un de ces deux est ce qui est fait par défaut, mais je ne sais pas lequel sous Windows…).

PS : le lien de Spacefox met beaucoup trop en avant l’idée de tout passer en utf8, il y a bien des nuances par-ci par-là mais c’est vite fait de passer à côté. Il faut surtout utiliser un encodage compris par ce qui va lire la donnée. Si optipng est pas foutu de lire de l’utf8 et que ton nom de fichier n’est pas descriptible dans un encodage qu’il comprend, t’es foutu.

PPS :

Demain, j’essaierai une autre solution :

  • Renommer temporairement le fichier en temp.png ;
  • le compresser avec OptiPNG ;
  • puis lui redonner son nom initial.

Tu fais quoi si temp.png existe déjà ? Utilise tempfile.NamedTemporaryFile.

+1 -0

J’ai essayé d’utiliser les méthodes .encode() mais elles ne fonctionnent pas.
Du coup, j’ai opté pour le renommage des fichiers et le code fonctionne enfin ! :ange:

Même les noms d’images très longs ou complexes sont correctement gérés à présent :

# coding: utf-8
from pathlib import Path
from subprocess import run

images = list(Path('.').glob('*.png'))
tmp_file = Path('tmp86fb9e7cb0d7e.png')

for img in images:
    img.rename(tmp_file)
    run(['optipng', str(tmp_file)])
    tmp_file.rename(img)

input("Fin du programme !")

Sujet résolu ! :)

Tu fais quoi si tmp86fb9e7cb0d7e.png existe déjà ?

Si j’ai un peu de temps, je gérerai ce cas dans une prochaine version du programme.

Note que tu n’as pas besoin de fabriquer la liste images à partir de l’appel à glob. Cette dernière renvoie un générateur que tu peux itérer directement et qui prendre très peu de place en mémoire. De manière générale, construire des listes à partir de générateurs si tu n’as pas besoin d’itérer plusieurs fois ou de manipuler la collection en entier est une mauvais idée, tu vas juste consommer beaucoup plus de mémoire et itérer 2 fois à travers la liste de fichiers pour rien. Si tu as 3 fichiers, ça va. Si tu en a quelques milliers ça commence à être gênant. Tu peux directement écrire

current_dir = Path()

for img in current_dir.glob('*.png'):
    # do stuff

Tant qu’on est dans les conseils Python généraux, tu n’as pas besoin du coding: utf-8 en entête, c’est un reliquat de Python 2 qui n’est plus nécessaire en Python 3 puisque c’est déjà l’encodage par défaut.

Enfin, tu pourrais facilement abstraire ton code et le rendre beaucoup plus flexible en définissant une fonction qui prend un chemin quelconque et traite les png à l’intérieur. Tu peux ensuite mettre l’appel dans une garde if __name__ == '__main__' et rendre ton module importable sans effet de bord. Ta fonction devient alors accessible depuis d’autres scripts et le comportement de ton script ne change pas (mais est plus facilement généralisable si tu veux passer des chemins en arguments par exemple).

+3 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte