J'ai une chaîne que je veux utiliser comme nom de fichier, donc je veux supprimer tous les caractères qui ne seraient pas autorisés dans les noms de fichiers, en utilisant Python.
Je préférerais être strict plutôt qu'autre chose, alors supposons que je ne conserve que des lettres, des chiffres et un petit ensemble de caractères tels que "_-.() "
. Quelle est la solution la plus élégante?
Le nom de fichier doit être valide sur plusieurs systèmes d'exploitation (Windows, Linux et Mac OS) - il s'agit d'un fichier MP3 de ma bibliothèque avec le titre de la chanson comme nom de fichier, et il est partagé et sauvegardé sur 3 ordinateurs.
C'est la solution que j'ai finalement utilisée:
import unicodedata
validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits)
def removeDisallowedFilenameChars(filename):
cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore')
return ''.join(c for c in cleanedFilename if c in validFilenameChars)
L'appel unicodedata.normalize remplace les caractères accentués par l'équivalent non accentué, ce qui est mieux que de simplement les supprimer. Après cela, tous les caractères non autorisés sont supprimés.
Ma solution n'inclut pas une chaîne connue pour éviter les noms de fichiers non autorisés, car je sais qu'ils ne peuvent pas apparaître compte tenu de mon format de nom de fichier particulier. Une solution plus générale devrait le faire.
Vous pouvez consulter Django Framework pour savoir comment ils créent un "slug" à partir de texte arbitraire. Un slug est convivial pour les URL et les noms de fichiers.
Les utilitaires de texte Django définissent une fonction, slugify()
, qui est probablement la règle d'or pour ce genre de chose. Essentiellement, leur code est le suivant.
def slugify(value):
"""
Normalizes string, converts to lowercase, removes non-alpha characters,
and converts spaces to hyphens.
"""
import unicodedata
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
value = unicode(re.sub('[-\s]+', '-', value))
Il y a plus, mais je l'ai laissé de côté, car il ne traite pas de slugification, mais échapper.
Cette approche de liste blanche (c'est-à-dire, n'autorisant que les caractères présents dans valid_chars) fonctionnera s'il n'y a pas de limites au formatage des fichiers ou à une combinaison de caractères valides qui sont illégaux (comme ".."), par exemple, ce que vous dites autoriserait un nom de fichier nommé ".txt" qui, à mon avis, n’est pas valide sous Windows. Comme il s’agit de l’approche la plus simple, j’essayerais de supprimer les espaces blancs de valid_chars et d’ajouter une chaîne valide connue en cas d’erreur, toute autre approche devra savoir ce qui est autorisé à gérer où nom de fichier Windows). limitations et donc être beaucoup plus complexe.
>>> import string
>>> valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
>>> valid_chars
'-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
>>> filename = "This Is a (valid) - filename%$&$ .txt"
>>> ''.join(c for c in filename if c in valid_chars)
'This Is a (valid) - filename .txt'
Vous pouvez utiliser la compréhension de liste avec les méthodes de chaîne.
>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
Quelle est la raison d'utiliser les chaînes en tant que noms de fichiers? Si la lisibilité humaine n’est pas un facteur, j’irais avec le module base64 qui peut produire des chaînes sûres pour le système de fichiers. Il ne sera pas lisible mais vous n’aurez pas à faire face à des collisions et il est réversible.
import base64
file_name_string = base64.urlsafe_b64encode(your_string)
Mise à jour: Modifié en fonction du commentaire de Matthew.
Juste pour compliquer encore les choses, vous n'êtes pas assuré d'obtenir un nom de fichier valide simplement en supprimant les caractères non valides. Étant donné que les caractères autorisés diffèrent en fonction du nom de fichier, une approche prudente pourrait aboutir à transformer un nom valide en un nom invalide. Vous voudrez peut-être ajouter un traitement spécial dans les cas où:
La chaîne contient tous des caractères non valides (vous laissant une chaîne vide)
Vous vous retrouvez avec une chaîne avec une signification spéciale, par exemple "." ou ".."
Sous Windows, certains noms de périphérique sont réservés. Par exemple, vous ne pouvez pas créer un fichier nommé "nul", "nul.txt" (ou nul.anything en fait). Les noms réservés sont:
CON, PRN, AUX, NUL, COM1, COM2, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT6, LPT7, LPT7, LPT8 et LPT9
Vous pouvez probablement contourner ces problèmes en ajoutant une chaîne aux noms de fichiers qui ne peuvent jamais aboutir à l'un de ces cas et en supprimant les caractères non valides.
Il y a un projet Nice sur Github appelé python-slugify :
Installer:
pip install python-slugify
Alors utilisez:
>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
Tout comme S.Lott , vous pouvez regarder Django Framework pour savoir comment ils convertissent une chaîne en un nom de fichier valide.
La version la plus récente et mise à jour se trouve dans utils/text.py, et définit "get_valid_filename", qui est la suivante:
def get_valid_filename(s):
s = str(s).strip().replace(' ', '_')
return re.sub(r'(?u)[^-\w.]', '', s)
(Voir https://github.com/Django/django/blob/master/Django/utils/text.py )
Rappelez-vous qu’il n’existe aucune restriction sur les noms de fichiers sur les systèmes Unix autres que
Tout le reste est un jeu juste.
$ touch " > même multiligne > haha > ^ [[31m rouge ^ [[0m > maléfique"]. $ ls -la - rw-r - r-- 0 nov. 17 23h39 - même multiligne? haha ?? [31m rouge? [0m? diable $ ls -lab - rw-r - r-- 0 nov 17 23:39\neven\multiline\nhaha\n\033 [31m\red\\ 033 [0m\nevil $ Perl -e ' pour mon $ i (glob (q {./* même *})) {print $ i; } ' ./ même multiligne haha rouge mal
Oui, je viens de stocker les codes de couleur ANSI dans un nom de fichier et de les faire prendre en compte.
Pour le divertissement, placez un caractère BEL dans un nom de répertoire et observez le plaisir que procure le fait de créer un CD;)
En une ligne:
valid_file_name = re.sub('[^\w_.)( -]', '', any_string)
vous pouvez également mettre le caractère '_' pour le rendre plus lisible (par exemple, en cas de remplacement des barres obliques)
>>> import string
>>> safechars = bytearray(('_-.()' + string.digits + string.ascii_letters).encode())
>>> allchars = bytearray(range(0x100))
>>> deletechars = bytearray(set(allchars) - set(safechars))
>>> filename = u'#ab\xa0c.$%.txt'
>>> safe_filename = filename.encode('ascii', 'ignore').translate(None, deletechars).decode()
>>> safe_filename
'abc..txt'
Il ne gère pas les chaînes vides, les noms de fichiers spéciaux ('nul', 'con', etc.).
Vous pouvez utiliser la méthode re.sub () pour remplacer tout ce qui n’est pas "filelike". Mais en réalité, chaque caractère pourrait être valide; il n'y a donc pas de fonctions prédéfinies (je crois) pour le faire.
import re
str = "File!name?.txt"
f = open(os.path.join("/tmp", re.sub('[^-a-zA-Z0-9_.() ]+', '', str))
Cela entraînerait un descripteur de fichier vers /tmp/nom_fichier.txt.
Bien que vous deviez faire attention. Ce n'est pas clairement dit dans votre intro, si vous ne regardez que la langue latine. Certains mots peuvent perdre leur signification ou une autre signification si vous les désinfectez avec des caractères ascii uniquement.
imaginez que vous avez "forêt poésie", votre désinfection pourrait donner "fort-posie" (fort + quelque chose de sens)
Pire encore si vous devez composer avec des caractères chinois.
"北 沢" votre système pourrait finir par faire "---" qui est voué à échouer après un certain temps et qui n'est pas très utile. Donc, si vous ne traitez que des fichiers, je vous encourage à les appeler soit une chaîne générique que vous contrôlez, soit à conserver les caractères tels quels. Pour les URI, à peu près les mêmes.
Pourquoi ne pas simplement envelopper le "osopen" avec un try/except et laisser le système d'exploitation sous-jacent déterminer si le fichier est valide?
Cela semble beaucoup moins de travail et est valable quel que soit le système d'exploitation que vous utilisez.
Un autre problème que les autres commentaires n'ont pas encore abordé est la chaîne vide, qui n'est évidemment pas un nom de fichier valide. Vous pouvez également vous retrouver avec une chaîne vide en supprimant trop de caractères.
Qu'en est-il des noms de fichiers réservés de Windows et des problèmes de points, la réponse la plus sûre à la question "Comment normaliser un nom de fichier valide à partir d'une entrée d'utilisateur arbitraire?" (par exemple, en utilisant des clés primaires entières d’une base de données comme noms de fichiers), faites-le.
Si vous devez, et que vous avez vraiment besoin d'autoriser les espaces et ‘.’ Pour les extensions de fichiers dans le nom, essayez quelque chose comme:
import re
badchars= re.compile(r'[^A-Za-z0-9_. ]+|^\.|\.$|^ | $|^$')
badnames= re.compile(r'(aux|com[1-9]|con|lpt[1-9]|prn)(\.|$)')
def makeName(s):
name= badchars.sub('_', s)
if badnames.match(name):
name= '_'+name
return name
Même cela ne peut pas être garanti, en particulier sur des systèmes d’exploitation inattendus - par exemple, RISC OS déteste les espaces et utilise ‘.’ Comme séparateur de répertoire.
J'appréciais l'approche python-slugify ici, mais c'était en train de faire disparaître des points, ce qui n'était pas souhaitable. Je l'ai donc optimisé pour le téléchargement d'un nom de fichier propre sur s3 de cette façon:
pip install python-slugify
Exemple de code:
s = 'Very / Unsafe / file\nname hähä \n\r .txt'
clean_basename = slugify(os.path.splitext(s)[0])
clean_extension = slugify(os.path.splitext(s)[1][1:])
if clean_extension:
clean_filename = '{}.{}'.format(clean_basename, clean_extension)
Elif clean_basename:
clean_filename = clean_basename
else:
clean_filename = 'none' # only unclean characters
Sortie:
>>> clean_filename
'very-unsafe-file-name-haha.txt'
C’est tellement sûr que cela fonctionne avec les noms de fichiers sans extension et même avec les noms de fichiers contenant des caractères non sécurisés (le résultat est none
ici).
Ce n'est pas exactement ce que OP demandait, mais voici ce que j'utilise parce que j'ai besoin de conversions uniques et réversibles:
# p3 code
def safePath (url):
return ''.join(map(lambda ch: chr(ch) if ch in safePath.chars else '%%%02x' % ch, url.encode('utf-8')))
safePath.chars = set(map(lambda x: ord(x), '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-_ .'))
Le résultat est "assez" lisible, du moins du point de vue du système.
La plupart de ces solutions ne fonctionnent pas.
'/ hello/world' -> 'helloworld'
'/ helloworld'/-> 'helloworld'
Ce n'est pas ce que vous voulez en général, disons que vous enregistrez le code HTML pour chaque lien, vous allez écraser le code HTML pour une page Web différente.
Je pickle un dict comme:
{'helloworld':
(
{'/hello/world': 'helloworld', '/helloworld/': 'helloworld1'},
2)
}
2 représente le numéro à ajouter au prochain nom de fichier.
Je cherche le nom du fichier à chaque fois dans le dict. Si ce n'est pas là, je crée un nouveau, en ajoutant le nombre maximum si nécessaire.
UPDATE
Tous les liens sont irréparables dans cette réponse de 6 ans.
En outre, je ne le ferais plus de cette façon plus, juste base64
encoder ou supprimer des caractères non sécurisés. Python 3 exemple:
import re
t = re.compile("[a-zA-Z0-9.,_-]")
unsafe = "abc∂éåß®∆˚˙©¬ñ√ƒµ©∆∫ø"
safe = [ch for ch in unsafe if t.match(ch)]
# => 'abc'
Avec base64
, vous pouvez encoder et décoder afin de pouvoir récupérer à nouveau le nom de fichier d'origine.
Mais selon le cas d'utilisation, il peut être préférable de générer un nom de fichier aléatoire et de stocker les métadonnées dans un fichier ou une base de données distinct.
from random import choice
from string import ascii_lowercase, ascii_uppercase, digits
allowed_chr = ascii_lowercase + ascii_uppercase + digits
safe = ''.join([choice(allowed_chr) for _ in range(16)])
# => 'CYQ4JDKE9JfcRzAZ'
RÉPONSE LINKROTTEN ORIGINALE :
Le projet bobcat
contient un module python qui ne fait que cela.
Ce n'est pas complètement robuste, voyez ceci post et ceci réponse .
Donc, comme indiqué: base64
le codage est probablement une meilleure idée si la lisibilité n'a pas d'importance.
Je me rends compte qu'il y a beaucoup de réponses, mais qu'elles reposent principalement sur des expressions régulières ou des modules externes, j'aimerais donc apporter ma propre réponse. Fonction pure python, aucun module externe requis, aucune expression régulière utilisée. Mon approche n’est pas de nettoyer les caractères non valides, mais de n’autoriser que les caractères valides.
def normalizefilename(fn):
validchars = "-_.() "
out = ""
for c in fn:
if str.isalpha(c) or str.isdigit(c) or (c in validchars):
out += c
else:
out += "_"
return out
si vous le souhaitez, vous pouvez ajouter au début vos propres caractères valides à la variable validchars
, tels que les lettres nationales qui n'existent pas dans l'alphabet anglais. C’est quelque chose que vous ne voulez peut-être pas: certains systèmes de fichiers qui ne s’exécutent pas sur UTF-8 peuvent tout de même avoir des problèmes avec des caractères non-ASCII.
Cette fonction permet de tester la validité d'un nom de fichier unique. Elle remplacera donc les séparateurs de chemin par _ les considérant comme des caractères non valides. Si vous voulez ajouter cela, il est facile de modifier le if
pour inclure le séparateur de chemin os.
Je suis sûr que ce n'est pas une bonne réponse, car cela modifie la chaîne sur laquelle elle est bouclée, mais cela semble fonctionner correctement:
import string
for chr in your_string:
if chr == ' ':
your_string = your_string.replace(' ', '_')
Elif chr not in string.ascii_letters or chr not in string.digits:
your_string = your_string.replace(chr, '')
Réponse modifiée pour python 3.6
validFilenameChars = "-_.() %s%s" % (string.ascii_letters, string.digits)
def removeDisallowedFilenameChars(filename):
cleanedFilename = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore')
return ''.join(chr(c) for c in cleanedFilename if chr(c) in validFilenameChars)