Превратить строку в допустимое имя файла?

У меня есть строка, которую я хочу использовать в качестве имени файла, поэтому я хочу удалить все символы, которые не будут разрешены в именах файлов, используя Python.

Я бы предпочел быть строгим, чем иначе, поэтому предположим, что я хочу сохранить только буквы, цифры и небольшой набор других символов, таких как "_-.() ". Какое самое элегантное решение?

имя файла должно быть действительным в нескольких операционных системах (Windows, Linux и Mac OS) - это MP3-файл в моей библиотеке с песней название как имя файла, и совместно используется и резервное копирование между 3 машинами.

20 ответов


вы можете посмотреть на Django framework как они создают "Слизень" из произвольного текста. Слизняк является URL - и имя файла дружественных.

их template/defaultfilters.py (около строки 183) определяет функцию,slugify, Это, наверное, золотой стандарт для такого рода вещей. По сути, их код следующий.

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

есть еще, но я оставил его, так как он не касается слизняков, а убегает.


этот подход белого списка (т. е., позволяющий только символы, присутствующие в valid_chars) будет работать, если нет ограничений на форматирование файлов или комбинацию допустимых символов, которые являются незаконными (например,".."), например, то, что вы говорите, позволит имя файла с именем " . txt", который, я думаю, недействителен в Windows. Поскольку это самый простой подход, я бы попытался удалить пробелы из valid_chars и добавить известную допустимую строку в случае ошибки, любой другой подход должен знать о том, что разрешено где справляться с ограничения имен файлов Windows и, таким образом, намного сложнее.

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

в чем причина использования строк в качестве имен файлов? Если человеческая читаемость не является фактором, я бы пошел с модулем base64, который может создавать безопасные строки файловой системы. Это не будет читаемым, но вам не придется иметь дело с столкновениями, и это обратимо.

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

обновление: изменено на основе комментария Мэтью.


вы можете использовать понимание списка вместе со строковыми методами.

>>> s
'foo-bar#baz?qux@127/\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'

чтобы еще больше усложнить ситуацию, вы не гарантированно получите действительное имя файла, просто удалив недопустимые символы. Поскольку разрешенные символы различаются в разных именах файлов, консервативный подход может в конечном итоге превратить допустимое имя в недопустимое. Вы можете добавить специальную обработку для случаев, когда:

  • строка содержит все недопустимые символы (оставляя вас с пустой строкой)

  • вы в конечном итоге со строкой с особое значение, например "." или." ."

  • на windows,определенные имена устройств зарезервированы. Например, вы не можете создать файл с именем "nul", " nul.txt "(или nul.ничего на самом деле) зарезервированные имена:

    кон, ПРН, ОКС, нуль, СОМ1, СОМ2, СОМ3, порт com4, COM5, СОМ6, резолюцию com7, COM8, com9, то порт lpt1, lpt2 в, подключен к порту lpt3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9 и

вы, вероятно, можете обойти эти проблемы, добавив некоторую строку в имена, которые никогда не могут привести к одному из этих случаев и зачистки недопустимых символов.


есть хороший проект на GitHub под названием python-slugify:

установка:

pip install python-slugify

затем использовать:

>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'

это решение, которое я в конечном итоге использовал:

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)

в unicodedata.normalize call заменяет акцентированные символы на неучтенный эквивалент, что лучше, чем просто удалить их. После этого все запрещенные символы удаляются.

мое решение не добавляет известную строку, чтобы избежать возможных запрещенных имен файлов, потому что я знаю, что они не могут произойти, учитывая мой конкретный формат файла. Для этого потребуется более общее решение.


имейте в виду, что на самом деле нет никаких ограничений на имена файлов в системах Unix, кроме

  • он не может содержать \0
  • он может не содержать /

все остальное-честная игра.

$ 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\n3[31m\ red\ 3[0m\nevil
$ perl -e 'for my $i ( glob(q{./*even*}) ){ print $i; } '
./
even multiline
haha
 red 
evil

Да, я просто сохранил цветовые коды ANSI в имени файла и они вступили в силу.

для развлечения, поместите символ BEL в имя каталога и смотреть удовольствие, которое следует, когда вы CD в него;)


как С. Лотт ответил, Вы можете посмотреть на Django Framework для того, как они преобразуют строку в допустимое имя файла.

самая последняя и обновленная версия находится в utils/text.py, и определяет "get_valid_filename", который выглядит следующим образом:

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

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


вы можете использовать re.sub () метод для замены чего-либо не "filelike". Но на самом деле каждый символ может быть действительным; поэтому нет встроенных функций (я считаю), чтобы это сделать.

import re

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

приведет к файлу в /tmp/filename.формат txt.


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

Он не обрабатывает пустые строки, специальные имена файлов ('nul',' con ' и т. д.).


Почему бы просто не обернуть "osopen" с помощью try/except и позволить базовой ОС разобраться, действителен ли файл?

Это кажется гораздо меньше работы и действительно независимо от того, какую ОС вы используете.


в одну строку:

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

вы также можете поместить символ"_", чтобы сделать его более читаемым (в случае замены косых черт, например)


хотя вы должны быть осторожны. Это не ясно сказано в вашем вступлении, если вы смотрите только на латинский язык. Некоторые слова могут стать бессмысленными или другим значением, если вы очистите их только символами ascii.

представьте, что у вас есть "forêt poésie "(Лесная поэзия), ваша дезинфекция может дать" fort-posie " (сильный + что-то бессмысленное)

хуже, если вам придется иметь дело с китайскими иероглифами.

" 下北沢 " ваша система может в конечном итоге сделать "---" который обречен на провал через некоторое время и не очень полезно. Поэтому, если вы имеете дело только с файлами, я бы рекомендовал либо назвать их общей цепочкой, которую вы контролируете, либо сохранить символы как есть. Для Уриса примерно то же самое.


другой проблемой, которую другие комментарии еще не рассмотрели, является пустая строка, которая, очевидно, не является допустимым именем файла. Вы также можете получить пустую строку от удаления слишком большого количества символов.

что с зарезервированными именами файлов Windows и проблемами с точками, самый безопасный ответ на вопрос "Как нормализовать допустимое имя файла из произвольного ввода пользователя?"это" даже не пытайтесь": если вы можете найти другой способ избежать этого (например. использование целочисленных первичных ключей из a база данных как имена файлов), сделайте это.

Если нужно, и вам действительно нужно разрешить пробелы и ‘. для расширения файлов как часть имени, попробуйте что-то вроде:

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

даже это не может быть гарантировано, особенно на неожиданных ОС - например, RISC OS ненавидит пробелы и использует".- как разделитель каталогов.


большинство из этих решений не работают.

'/ hello / world' - > 'helloworld'

' / helloworld' / - > 'helloworld'

это не то, что вы хотите обычно, скажем, вы сохраняете html для каждой ссылки, вы собираетесь перезаписать html для другой веб-страницы.

Я мариновать дикт, такие как:

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

2 представляет число, которое должно быть добавлено к следующему имени файла.

Я каждый раз ищу имя файла из словарь. Если его нет, я создаю новый, добавляя при необходимости максимальное число.


мне понравился подход python-slugify здесь, но он также удалял точки, что было нежелательно. Поэтому я оптимизировал его для загрузки чистого имени файла в s3 следующим образом:

pip install python-slugify

пример кода:

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

выход:

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

это так безотказно, он работает с именами файлов без расширения, и он даже работает только для небезопасных символов имен файлов (результат none здесь).


не совсем то, что OP просил, но это то, что я использую, потому что мне нужны уникальные и обратимые преобразования:

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

результат" несколько " читаем, по крайней мере, с точки зрения системного администратора.


Я уверен, что это не отличный ответ, так как он изменяет строку, которую он зацикливает, но, похоже, работает нормально:

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

обновление

все ссылки сломаны без ремонта в этом 6-летнем ответе.

кроме того, я бы тоже так больше не делал, просто base64 кодировать или удалять небезопасные символы. В Python 3 Пример:

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

С base64 вы можете кодировать и декодировать, так что вы можете снова получить исходное имя.

но в зависимости от варианта использования вам может быть лучше генерировать случайное имя файла и хранить метаданные в отдельном файле или ДЕЦИБЕЛ.

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'

ОРИГИНАЛЬНЫЙ ОТВЕТ LINKROTTEN:

на bobcat проект содержит модуль python, который делает именно это.

это не совсем надежный, увидеть это в должности и ответ.

так, как указано: base64 кодирование-это, вероятно, лучшая идея, если читаемость не имеет значения.