¿Convertir una cadena en un nombre de archivo válido?


Tengo una cadena que quiero usar como nombre de archivo, por lo que quiero eliminar todos los caracteres que no se permitirían en los nombres de archivo, utilizando Python.

Prefiero ser estricto que de otra manera, así que digamos que quiero retener solo letras, dígitos y un pequeño conjunto de otros caracteres como "_-.() ". ¿Cuál es la solución más elegante?

El nombre del archivo debe ser válido en varios sistemas operativos (Windows, Linux y Mac OS): es un archivo MP3 en mi biblioteca con el título de la canción como nombre del archivo, y se comparte y respalda entre 3 máquinas.

Author: martineau, 2008-11-17

20 answers

Puedes mirar el framework Django para ver cómo crean un "slug" a partir de texto arbitrario. Un slug es amigable con la URL y el nombre de archivo.

Su template/defaultfilters.py (alrededor de la línea 183) define una función, slugify, que es probablemente el patrón oro para este tipo de cosas. Esencialmente, su código es el siguiente.

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))

Hay más, pero lo dejé fuera, ya que no aborda la slugificación, sino el escape.

 126
Author: S.Lott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-29 15:26:33

Este enfoque de lista blanca (es decir, permitir solo los caracteres presentes en valid_chars) funcionará si no hay límites en el formato de los archivos o la combinación de caracteres válidos que son ilegales (como ".."), por ejemplo, lo que dices permitiría un nombre de archivo llamado " . txt " que creo que no es válido en Windows. Como este es el enfoque más simple, intentaría eliminar los espacios en blanco de los valid_chars y anteponer una cadena válida conocida en caso de error, cualquier otro enfoque tendrá que saber qué es permitido dónde hacer frente a limitaciones de nombres de archivos de Windows y por lo tanto ser mucho más complejo.

>>> 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'
 92
Author: Vinko Vrsalovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:33:24

¿Cuál es la razón para usar las cadenas como nombres de archivo? Si la legibilidad humana no es un factor, iría con el módulo base64 que puede producir cadenas seguras para el sistema de archivos. No será legible, pero no tendrá que lidiar con colisiones y es reversible.

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

Actualización : Cambiado basado en el comentario de Matthew.

 87
Author: Igal Serban,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-04-13 16:48:36

Puede usar comprensión de lista junto con los métodos de cadena.

>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
 81
Author: John Mee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-29 09:59:05

Solo para complicar aún más las cosas, no se garantiza que obtendrá un nombre de archivo válido simplemente eliminando caracteres no válidos. Dado que los caracteres permitidos difieren en diferentes nombres de archivo, un enfoque conservador podría terminar convirtiendo un nombre válido en uno no válido. Es posible que desee agregar un manejo especial para los casos en los que:

  • La cadena es todos los caracteres no válidos (dejándote con una cadena vacía)

  • Terminas con una cuerda con un significado especial, por ejemplo"." o ".."

  • En Windows, ciertos nombres de dispositivos están reservados. Por ejemplo, no puede crear un archivo llamado "nul", " nul.txt " (o nul.cualquier cosa de hecho) Los nombres reservados son:

    CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8 y LPT9

Probablemente pueda solucionar estos problemas anteponiendo alguna cadena a los nombres de archivo que nunca puede dar lugar a uno de estos casos, y eliminación de caracteres no válidos.

 33
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-17 09:57:40

Hay un buen proyecto en Github llamado python-slugify :

Instalar:

pip install python-slugify

Luego use:

>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
 20
Author: Shoham,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-29 11:19:47

Esta es la solución que finalmente usé:

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)

El unicodedata.normalize call reemplaza los caracteres acentuados con el equivalente no acentuado, que es mejor que simplemente eliminarlos. Después de eso, se eliminan todos los caracteres no permitidos.

Mi solución no antepone una cadena conocida para evitar posibles nombres de archivo no permitidos, porque sé que no pueden ocurrir dado mi formato de nombre de archivo particular. Para ello se necesitaría una solución más general.

 17
Author: Sophie Gage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-30 19:40:17

Tenga en cuenta que en realidad no hay restricciones sobre los nombres de archivo en sistemas Unix que no sean

  • Puede que no contenga \0
  • no puede contener /

Todo lo demás es juego limpio.

$ touch "
> even multiline
> haha
> ^[[31m red ^[[0m
> evil"
$ ls -la 
-rw-r--r--       0 Nov 17 23:39 ?even multiline?haha??[31m red ?[0m?evil
$ ls -lab
-rw-r--r--       0 Nov 17 23:39 \neven\ multiline\nhaha\n\033[31m\ red\ \033[0m\nevil
$ perl -e 'for my $i ( glob(q{./*even*}) ){ print $i; } '
./
even multiline
haha
 red 
evil

Sí, acabo de almacenar códigos de color ANSI en un nombre de archivo y los hice entrar en vigor.

Para entretenimiento, ponga un carácter BEL en un nombre de directorio y observe la diversión que se produce cuando se graba en él;)

 13
Author: Kent Fredric,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-17 10:45:54

Al igual que S. Lott respondió, puede mirar el Django Framework para ver cómo convierten una cadena a un nombre de archivo válido.

La versión más reciente y actualizada se encuentra en utils/text.py, y define "get_valid_filename", que es como sigue:

def get_valid_filename(s):
    s = str(s).strip().replace(' ', '_')
    return re.sub(r'(?u)[^-\w.]', '', s)

(Véase https://github.com/django/django/blob/master/django/utils/text.py )

 10
Author: cowlinator,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-18 00:24:44

Podrías usar el re.método sub () para reemplazar cualquier cosa que no sea "filelike". Pero en efecto, cada carácter podría ser válido; por lo que no hay funciones predefinidas (creo), para hacerlo.

import re

str = "File!name?.txt"
f = open(os.path.join("/tmp", re.sub('[^-a-zA-Z0-9_.() ]+', '', str))

Daría como resultado un manejador de archivos a /tmp/filename.txt.

 7
Author: gx.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-01 09:35:10
>>> 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'

No maneja cadenas vacías, nombres de archivo especiales ('nul', 'con', etc.).

 7
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-22 11:26:50

¿Por qué no simplemente envolver el "osopen" con un try/except y dejar que el sistema operativo subyacente resuelva si el archivo es válido?

Esto parece mucho menos trabajo y es válido sin importar el sistema operativo que use.

 6
Author: James Anderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-30 01:46:53

En una línea:

valid_file_name = re.sub('[^\w_.)( -]', '', any_string)

También puede poner el carácter ' _ ' para que sea más legible (en caso de reemplazar barras, por ejemplo)

 6
Author: mnach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-04 11:29:03

Aunque hay que tener cuidado. No se dice claramente en su introducción, si solo está mirando el lenguaje latino. Algunas palabras pueden perder sentido u otro significado si las desinfectas solo con caracteres ascii.

Imagine que tiene " forêt poésie "(poesía del bosque), su desinfección podría dar" fort-posie " (fuerte + algo sin sentido)

Peor si tienes que lidiar con caracteres chinos.

"your" su sistema podría terminar haciendo "- - - " que está condenado al fracaso después de un tiempo y no muy útil. Por lo tanto, si se trata solo de archivos, le animaría a llamarlos una cadena genérica que controle o a mantener los caracteres tal como están. Para los URIs, más o menos lo mismo.

 5
Author: karlcow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-11 10:44:46

Otro problema que los otros comentarios aún no han abordado es la cadena vacía, que obviamente no es un nombre de archivo válido. También puede terminar con una cadena vacía de quitar demasiados caracteres.

Con los nombres de archivo reservados de Windows y los problemas con los puntos, la respuesta más segura a la pregunta "¿cómo normalizo un nombre de archivo válido a partir de una entrada arbitraria del usuario?"es" ni siquiera te molestes en intentarlo": si puedes encontrar cualquier otra manera de evitarlo (por ejemplo. uso de claves primarias enteras de una base de datos como nombres de archivo), hacer eso.

Si debe, y realmente necesita permitir espacios y '.'para extensiones de archivo como parte del nombre, intente algo como:

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

Incluso esto no se puede garantizar correctamente, especialmente en OSS inesperados - por ejemplo, RISC OS odia los espacios y los usos '.'como separador de directorios.

 4
Author: bobince,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-17 13:24:19

La mayoría de estas soluciones no funcionan.

'/hello / world '- > 'helloworld'

'/ helloworld ' / - > 'helloworld'

Esto no es lo que quieres en general, digamos que estás guardando el html para cada enlace, vas a sobrescribir el html para una página web diferente.

Escurro un dictado como:

{'helloworld': 
    (
    {'/hello/world': 'helloworld', '/helloworld/': 'helloworld1'},
    2)
    }

2 representa el número que debe añadirse al siguiente nombre de archivo.

Busco el nombre del archivo cada vez desde el diccionario. Si no está ahí, creo uno nuevo, añadiendo el número máximo si es necesario.

 2
Author: robert king,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-16 01:04:34

Me gustó el enfoque python-slugify aquí, pero también estaba eliminando puntos, lo que no era deseado. Así que lo optimicé para subir un nombre de archivo limpio a s3 de esta manera:

pip install python-slugify

Código de ejemplo:

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

Salida:

>>> clean_filename
'very-unsafe-file-name-haha.txt'

Esto es tan a prueba de fallos, funciona con nombres de archivo sin extensión e incluso funciona solo para nombres de archivo de caracteres inseguros (el resultado es none aquí).

 2
Author: therealmarv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-05 16:51:07

No es exactamente lo que OP estaba pidiendo, pero esto es lo que uso porque necesito conversiones únicas y reversibles:

# 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+-_ .'))

El resultado es "algo" legible, al menos desde el punto de vista de sysadmin.

 1
Author: makeroo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-12 12:19:39

Estoy seguro de que esta no es una gran respuesta, ya que modifica la cadena sobre la que está en bucle, pero parece funcionar bien:

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, '')
 0
Author: TankorSmash,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-05 03:56:00

UPDATE

Todos los enlaces rotos sin posibilidad de reparación en esta respuesta de 6 años.

Además, tampoco lo haría de esta manera más, solo base64 codificar o soltar caracteres inseguros. Ejemplo de Python 3:

import re
t = re.compile("[a-zA-Z0-9.,_-]")
unsafe = "abc∂éåß®∆˚˙©¬ñ√ƒµ©∆∫ø"
safe = [ch for ch in unsafe if t.match(ch)]
# => 'abc'

Con base64 puede codificar y decodificar, para que pueda recuperar el nombre de archivo original de nuevo.

Pero dependiendo del caso de uso, podría ser mejor generar un nombre de archivo aleatorio y almacenar los metadatos en un archivo o BD separado.

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'

ORIGINAL LINKROTTEN RESPUESTA :

El proyecto bobcat contiene un módulo python que hace precisamente esto.

No es completamente robusto, ver este post y este reply.

Así que, como se señaló: base64 la codificación es probablemente una mejor idea si la legibilidad no importa.

 0
Author: wires,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-28 14:30:16