Превратить строку в допустимое имя файла?
У меня есть строка, которую я хочу использовать в качестве имени файла, поэтому я хочу удалить все символы, которые не будут разрешены в именах файлов, используя 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))есть еще, но я оставил его, так как он не обращается к slugification, но убегает.
этот подход к белому списку (т. е. разрешающий только символы, присутствующие в 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 в имя каталога и смотреть удовольствие, которое следует, когда вы компакт-диск в него ;)
как S. Lott ответил, Вы можете посмотреть на 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 )
вы можете использовать огонь.суб() метод, чтобы заменить что-нибудь не "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 и проблемами с точками, самый безопасный ответ на вопрос "Как нормализовать допустимое имя файла из произвольного пользовательского ввода?"это "даже не пытайтесь": если вы можете найти какой-либо другой способ избежать этого (например. используя первичный ключ целое число от база данных как имена файлов), сделайте это.
Если вы должны, и вам действительно нужно, чтобы разрешить пробелы и ‘. для расширения файлов как часть имени, попробуйте что-то вроде:
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здесь).
не совсем то, что ОП просил, но это то, что я использую, потому что мне нужны уникальные и обратимые преобразования:
# 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:
The
bobcatпроект содержит модуль python, который делает именно это.это не совсем надежно, смотрите это post и ответ.
Итак, как было отмечено:
base64кодирование, вероятно, лучше, если читаемость не имеет значения.
Comments