Включить строку в действительное имя файла?

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

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

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

Ответ 1

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

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 вместо акцентированных символов заменяется несогласованный эквивалент, который лучше, чем просто их удаление. После этого все запрещенные символы удаляются.

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

Ответ 2

Вы можете посмотреть на Django Framework, чтобы узнать, как они создают "слаг" из произвольного текста. Слизняк URL- и filename- дружелюбен.

Текстовые утилиты Django определяют функцию 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))

Есть еще кое-что, но я не упомянул об этом, так как это не касается слизистых, а спасения.

Ответ 3

Этот подход в белом списке (т.е. разрешая только символы, присутствующие в 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'

Ответ 4

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

>>> s
'foo-bar#[email protected]/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'

Ответ 5

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

Обновление: изменено на основе комментария Matthew.

Ответ 6

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

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

  • В итоге вы получите строку со специальным значением, например "." или ".."

  • В окнах определенные имена устройств зарезервированы. Например, вы не можете создать файл с именем "nul", "nul.txt" (или вообще nul.anything). Зарезервированными именами являются:

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

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

Ответ 7

В Github есть хороший проект, называемый python-slugify:

Установка:

pip install python-slugify

Затем используйте:

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

Ответ 8

Как и ответил С.Лотт, вы можете посмотреть на 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).

Ответ 9

Имейте в виду, на самом деле нет ограничений на имена файлов в системах 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\n\033[31m\ red\ \033[0m\nevil
$ perl -e 'for my $i ( glob(q{./*even*}) ){ print $i; } '
./
even multiline
haha
 red 
evil

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

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

Ответ 10

В одной строке:

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

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

Ответ 11

Вы можете использовать метод 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.

Ответ 12

>>> 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' и т.д.).

Ответ 13

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

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

Ответ 14

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

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

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

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

Ответ 15

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

Что с зарезервированными именами файлов 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 ненавидит пробелы и использует ". как разделитель каталогов.

Ответ 16

Мне понравился подход 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).

Ответ 17

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

'/hello/world' → 'helloworld'

'/helloworld'/- > 'helloworld'

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

Я расчекаю диктофон, например:

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

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

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

Ответ 18

Не совсем то, о чем просил 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+-_ .'))

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

Ответ 19

UPDATE

В этом 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, который выполняет только это.

Это не полностью надежный, см. этот post и этот ответ.

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

Ответ 20

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

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

Ответ 21

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

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    

если хотите, вы можете добавить свои собственные действительные символы в переменную validchars в начале, например, ваши национальные буквы, которых нет в английском алфавите. Это то, что вы можете или не можете хотеть: некоторые файловые системы, которые не работают на UTF-8, все еще могут иметь проблемы с не-ASCII-символами.

Эта функция предназначена для проверки правильности одного имени файла, поэтому она заменит разделители пути на _, считая их недопустимыми символами. Если вы хотите добавить это, тривиально изменить if чтобы включить разделитель пути os.

Ответ 22

Ответ изменен для Python 3.6

import string
import unicodedata

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)