python filenames slug sanitize

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



filenames slug (20)

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

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

El nombre de 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 de archivo, y se comparte y se respalda entre 3 máquinas.


¿Cuál es la razón para usar las cadenas como nombres de archivos? 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 Mateo.


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

Esto parece mucho menos trabajo y es válido sin importar qué sistema operativo utilice.


Al igual que respondió S.Lott , puede consultar el marco de Django para ver cómo convierten una cadena en 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 la siguiente:

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

(Consulte https://github.com/django/django/blob/master/django/utils/text.py )


Aunque hay que tener cuidado. No se dice claramente en su introducción, si está buscando solo en latín. Algunas palabras pueden volverse sin sentido u otro significado si las desinfecta solo con caracteres ASCII.

imagina que tienes "forêt poésie" (poesía de bosque), tu sanitización podría dar "fort-posie" (fuerte + algo sin sentido)

Peor aún si tienes que lidiar con los caracteres chinos.

"下 北 沢" su sistema podría terminar haciendo "---", que está condenado a fallar después de un tiempo y no es muy útil. Por lo tanto, si solo se trata de archivos, le recomendaría que los llame a una cadena genérica que usted controla o que mantenga los caracteres como están. Para URIs, aproximadamente lo mismo.


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 el caso de reemplazar barras diagonales, por ejemplo)


Esta es la solución que finalmente utilicé:

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)

La llamada unicodedata.normalize reemplaza los caracteres acentuados con el equivalente sin acento, lo que es mejor que simplemente eliminarlos. Después de eso se eliminan todos los caracteres no permitidos.

Mi solución no incluye 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. Una solución más general tendría que hacerlo.


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 diga 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, trataría de eliminar los espacios en blanco de valid_chars y anteponer una cadena válida conocida en caso de error, cualquier otro enfoque deberá saber qué se permite dónde hacer frente a las limitaciones de nombres de archivos de Windows y, por lo tanto, 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''


Estoy seguro de que esta no es una gran respuesta, ya que modifica la cadena sobre la que se está repitiendo, 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, '''')


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


La mayoría de estas soluciones no funcionan.

''/ hello / world'' -> ''helloworld''

''/ helloworld'' / -> ''helloworld''

Esto no es lo que generalmente desea, digamos que está guardando el html para cada enlace, va a sobrescribir el html para una página web diferente.

Escucho un dictado como:

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

2 representa el número que se debe agregar al siguiente nombre de archivo.

Busco el nombre del archivo cada vez desde el dict. Si no está allí, creo uno nuevo, agregando el número máximo si es necesario.


Me gustó el enfoque Python-Slugify aquí, pero fue quitar los puntos también, lo que no fue deseado. Así que lo optimicé para cargar 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 seguro, funciona con nombres de archivo sin extensión e incluso funciona solo con nombres de archivo de caracteres no seguros (el resultado es none aquí).


No es exactamente lo que estaba pidiendo OP, 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 un punto de vista de administrador de sistemas.


Otro problema que los otros comentarios no han abordado aún 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 eliminar demasiados caracteres.

Con los nombres de archivo de Windows reservados y los problemas con los puntos, la respuesta más segura a la pregunta "¿Cómo puedo normalizar un nombre de archivo válido a partir de una entrada de usuario arbitraria?" Es "ni siquiera se moleste en intentarlo": si puede encontrar otra forma de evitar (por ejemplo, utilizando claves primarias enteras de una base de datos como nombres de archivo), haga eso.

Si es necesario, y realmente necesitas permitir espacios y ''.'' para las 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 sistemas operativos inesperados, por ejemplo, el sistema operativo RISC odia los espacios y los usos ''''. como un separador de directorio.


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

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

Resultaría en un identificador de archivo a /tmp/filename.txt.


Puede utilizar la 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''


Puedes ver el marco de Django para ver cómo crean una "bala" de texto arbitrario. Un slug es compatible con URL y nombre de archivo.

Su template/defaultfilters.py (en torno a la línea 183) define una función, slugify , que probablemente sea el estándar de 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 slugification, sino el escape.


Solo para complicar aún más las cosas, no se garantiza que obtenga 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 inválido. Es posible que desee agregar un manejo especial para los casos donde:

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

  • Usted termina con una cadena 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.anything en realidad) 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

Es probable que pueda solucionar estos problemas añadiendo una cadena a los nombres de archivo que nunca pueden resultar en uno de estos casos y eliminando los caracteres no válidos.


Tenga en cuenta que, de hecho, no hay restricciones para los nombres de archivo en sistemas Unix que no sean

  • No puede contener / 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í, simplemente almacené los códigos de color ANSI en un nombre de archivo y los hice tener efecto.

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


ACTUALIZAR

Todos los enlaces rotos más allá de la reparación en esta respuesta de 6 años.

Además, tampoco lo haría de esta manera, solo codificar en base64 o eliminar 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, por lo que puede recuperar el nombre del archivo original nuevamente.

Sin embargo, dependiendo del caso de uso, es mejor que genere un nombre de archivo aleatorio y almacene los metadatos en un archivo o DB por 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''

RESPUESTA ORIGINAL DE LINKROTTEN :

El proyecto bobcat contiene un módulo de Python que hace justamente esto.

No es completamente robusto, vea este post y esta reply .

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


>>> 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 archivos especiales (''nul'', ''con'', etc.).