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

298

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

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

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

Софи Гейдж
источник
17
Разве это не должно быть встроено в модуль os.path?
эндолит
2
Возможно, хотя ее вариант использования потребовал бы единственного безопасного пути для всех платформ, а не только текущей, который os.path не предназначен для обработки.
javawizard
2
Чтобы развернуть вышеупомянутый комментарий: текущий дизайн os.pathфактически загружает другую библиотеку в зависимости от операционной системы (см. Второе примечание в документации ). Поэтому, если в нем реализована функция цитирования, os.pathона может заключать в кавычки только строку для POSIX-безопасности при работе в системе POSIX или для Windows-безопасности при работе в Windows. Полученное имя файла не обязательно будет действительным для обоих окон и POSIX, о чем и спрашивает вопрос.
dshepherd

Ответы:

164

Вы можете посмотреть на структуру Django, как они создают «слаг» из произвольного текста. Слаг - это URL-адрес и имя файла.

Текстовые утилиты 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))
    # ...
    return value

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

С. Лотт
источник
11
Последняя строка должна быть: value = unicode (re.sub ('[- \ s] +', '-', value))
Джозеф Туриан
1
Спасибо - я мог что-то упустить, но я получаю: «normalize () аргумент 2 должен быть в кодировке Unicode, а не str»
Алекс Кук
msgstr "нормализовать () аргумент 2". Значит value. Если значение должно быть Unicode, то вы должны быть уверены, что это на самом деле Unicode. Или. Возможно, вы захотите пропустить нормализацию Юникода, если ваше фактическое значение на самом деле является строкой ASCII.
S.Lott
8
В случае, если кто-то не заметил положительную сторону этого подхода, он не просто удаляет не-буквенные символы, но пытается сначала найти хорошие заменители (посредством нормализации NFKD), поэтому é становится e, верхний индекс 1 становится нормальный 1 и т. д. Спасибо
Майкл Скотт Катберт
48
slugifyФункция была перемещена в Джанго / Utils / text.py , и этот файл также содержит get_valid_filenameфункцию.
Денилсон Са
104

Этот подход белого списка (то есть, разрешающий только символы, присутствующие в 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'
Винко Врсалович
источник
7
valid_chars = frozenset(valid_chars)не повредит. Это в 1,5 раза быстрее, если применяется к allchars.
JFS
2
Предупреждение: это сопоставляет две разные строки в одну и ту же строку >>> строка импорта >>> valid_chars = "- . ()% S% s"% (string.ascii_letters, string.digits) >>> valid_chars '- . () abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '>>> filename = "a.com/hello/world" >>>' '.join (c для c в имени файла, если c в valid_chars)' a.comhelloworld '>>> filename = "a.com/helloworld ">>> '' .join (c для c в имени файла, если c в valid_chars) 'a.comhelloworld' >>>
Роберт
3
Не говоря уже о том, что присвоение имени файлу "CON"в Windows доставит вам неприятности ...
Натан Осман,
2
Небольшая перестановка делает определение замещающего символа простым. Сначала исходная функциональность: '' .join (c, если c в valid_chars else '' для c в имени файла) или с замененным символом или строкой для каждого недопустимого символа: '' .join (c если c в valid_chars else '.' Для c в имени файла)
PeterVermont
101

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

>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
Джон Ми
источник
3
Обратите внимание, что вы можете опустить квадратные скобки. В этом случае выражение генератора передается в join, что экономит шаг создания неиспользуемого списка.
Обен Сонне
31
+1 Очень понравилось это. Небольшая модификация, которую я сделал: "" .join ([x, если x.isalnum () else "_" для x в s]) - даст результат, где недопустимые элементы - это _, как будто они пустые. Может быть, кто-то еще.
Эдди Паркер
12
Это решение отлично! Я сделал небольшую модификацию, хотя:filename = "".join(i for i in s if i not in "\/:*?<>|")
Алекс Крайчек
1
К сожалению, это даже не позволяет пробелы и точки, но мне нравится идея.
Тиктак
9
@tiktak: чтобы (также) разрешить пробелы, точки и подчеркивания, на которые вы можете пойти"".join( x for x in s if (x.isalnum() or x in "._- "))
hardmooth
95

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

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

Игал Сербан
источник
1
Очевидно, что это лучший ответ, если это так.
user32141
60
Предупреждение! Кодировка base64 по умолчанию включает символ "/" в качестве допустимого вывода, который недопустим в именах файлов во многих системах. Вместо этого используйте base64.urlsafe_b64encode (your_string)
Матфея
15
На самом деле удобочитаемость почти всегда является фактором, даже если только для целей отладки.
static_rtti
5
В Python 3 your_stringдолжен быть байтовый массив или результат, encode('ascii')чтобы это работало.
Нумен
4
def url2filename(url): url = url.encode('UTF-8') return base64.urlsafe_b64encode(url).decode('UTF-8') def filename2url(f): return base64.urlsafe_b64decode(f).decode('UTF-8')
ДжеффПрод
40

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

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

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

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

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

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

Брайан
источник
25

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

Установка:

pip install python-slugify

Тогда используйте:

>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
Shoham
источник
2
Мне нравится эта библиотека, но она не так хороша, как я думал. Начальное тестирование в порядке, но оно также конвертирует точки. Так что test.txtполучается test-txtслишком много.
Therealmarv
23

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

cowlinator
источник
4
для ленивых уже на django:django.utils.text import get_valid_filename
анонс
2
Если вы не знакомы с регулярным выражением, re.sub(r'(?u)[^-\w.]', '', s)удаляются все символы, которые не являются буквами, не цифрами (0-9), ни подчеркиванием ('_'), ни тире ('-'), ни точкой ('.'). ). «Буквы» здесь включают все буквы Unicode, такие как 漢語.
cowlinator
3
Вы также можете проверить длину: имена файлов ограничены 255 символами (или, как вы знаете, 32; в зависимости от FS)
Матиас Винкельман
19

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

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

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

Софи Гейдж
источник
Вы должны иметь возможность использовать uuid.uuid4 () для своего уникального префикса
slf
6
дело верблюда .. ааа
сумасшедший ежик
Может ли это быть отредактировано / обновлено для работы с Python 3.6?
Wavesailor
13

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

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

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

$ touch "
> даже многострочный
> хаха
> ^ [[31м красный ^ [[0м
> зло "
$ ls -la 
-rw-r - r-- 0 17 ноября 23:39? даже многострочный? хаха ?? [31м красный? [0м? злой
$ ls -lab
-rw-r - r-- 0 17 ноября 23:39 \ neven \ multiline \ nhaha \ n \ 033 [31m \ red \ \ 033 [0m \ nevil
$ perl -e 'для моего $ i (glob (q {./* even *})) {print $ i; } '
./
даже многострочный
ха-ха
 красный 
злой

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

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

Кент Фредрик
источник
ОП заявляет, что «имя файла должно быть допустимым в нескольких операционных системах»
cowlinator
1
@cowlinator, что разъяснение было добавлено через 10 часов после того, как мой ответ был опубликован :) Проверьте журнал редактирования ОП.
Кент Фредрик
12

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

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

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

mnach
источник
7

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

import re

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

Результатом будет дескриптор файла /tmp/filename.txt.

Gx.
источник
5
Вам нужен тире, чтобы идти первым в групповом совпадении, чтобы он не отображался как диапазон. re.sub ('[^ - a-zA-Z0-9 _. ()] +', '', str)
phord
7
>>> 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' и т. Д.).

JFS
источник
+1 для таблиц перевода, это, безусловно, самый эффективный метод. Для специальных имен файлов / пустых мест будет достаточно простой проверки предусловий, а для посторонних периодов это также простая поправка.
Кристиан Виттс
1
Хотя translate немного более эффективен, чем регулярное выражение, это время, скорее всего, будет меньше, если вы действительно попытаетесь открыть файл, что, без сомнения, вы собираетесь делать. Таким образом, я предпочитаю более удобочитаемое решение регулярных выражений, чем беспорядок выше
nosatalian
Я также беспокоюсь о черном списке. Конечно, это черный список, основанный на белом списке, но все же. Это кажется менее ... безопасным. Откуда вы знаете, что «allchars» на самом деле завершена?
Исаакль
@isaaclw: '.translate ()' принимает строку из 256 символов в качестве таблицы перевода (байт-байтовый перевод). '.maketrans ()' создает такую ​​строку. Все значения покрыты; это чисто белый подход
JFS
Как насчет имени файла. (одна точка). Это не будет работать в Unixes, поскольку нынешний каталог использует это имя.
Финн Оруп Нильсен
6

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

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

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

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

karlcow
источник
6

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

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

Джеймс Андерсон
источник
5
Это действительно имя, хотя? Я имею в виду, если ОС не устраивает, то вам все равно нужно что-то делать, верно?
Джеромей
1
В некоторых случаях OS / Language может молча поменять ваше имя файла в альтернативную форму, но когда вы создадите список каталогов, вы получите другое имя. И это может привести к проблеме «когда я записываю файл там, но когда я ищу файл, это называется чем-то другим». (Я говорю о поведении, о котором я слышал в VAX ...)
Кент Фредрик,
Более того, «Имя файла должно быть действительным в нескольких операционных системах», которое вы не можете обнаружить при osopenработе на одном компьютере.
LarsH
5

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

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

bobince
источник
4

Мне здесь понравился подход, связанный с питоном, но он также удалял точки, что было нежелательно. Поэтому я оптимизировал его для загрузки чистого имени файла в 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здесь).

therealmarv
источник
1
Мне это нравится, не изобретайте колесо, не импортируйте весь фреймворк Django, если он вам не нужен, не вставляйте непосредственно код, если вы не собираетесь его поддерживать в будущем, и сгенерированные попытки строк сопоставлять похожие буквы с безопасными, поэтому новую строку легче читать.
Vicenteherrera
1
Чтобы использовать подчеркивание вместо тире: name = slugify (s, separator = '_')
vicenteherrera
3

Ответ изменен для 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)
Жан-Робин Трембле
источник
Не могли бы вы объяснить свой ответ в деталях?
Спокойствие
Это тот же ответ, принятый Софи Гейдж. Но он был модифицирован для работы на Python 3.6
Жан-Робин Тремблей
2

Я понимаю, что ответов много, но они в основном полагаются на регулярные выражения или внешние модули, поэтому я хотел бы добавить свой собственный ответ. Чистая функция 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.

Tuncay Göncüoğlu
источник
1

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

'/ hello / world' -> 'helloworld'

'/ helloworld' / -> 'helloworld'

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

Я мариную такие слова, как:

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

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

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

Роберт Кинг
источник
обратите внимание, что если вы используете helloworld1, вам также нужно проверить, что helloworld1 не используется и т. д.
Роберт
1

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

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

makeroo
источник
Обертка для этого без пробелов в именах файлов:def safe_filename(filename): return safePath(filename.strip().replace(' ','_'))
SpeedCoder5
1

Если вы не возражаете против установки пакета, это может быть полезно: https://pypi.org/project/pathvalidate/

Из https://pypi.org/project/pathvalidate/#sanitize-a-filename :

from pathvalidate import sanitize_filename

fname = "fi:l*e/p\"a?t>h|.t<xt"
print(f"{fname} -> {sanitize_filename(fname)}\n")
fname = "\0_a*b:c<d>e%f/(g)h+i_0.txt"
print(f"{fname} -> {sanitize_filename(fname)}\n")

Вывод

fi:l*e/p"a?t>h|.t<xt -> filepath.txt
_a*b:c<d>e%f/(g)h+i_0.txt -> _abcde%f(g)h+i_0.txt
Ставрос
источник
0

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

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, '')
TankorSmash
источник
Я нашел это "".join( x for x in s if (x.isalnum() or x in "._- "))в этом посте комментариев
SergioAraujo
0

ОБНОВИТЬ

Все ссылки неработоспособны в этом 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Проект содержит модуль питона , который делает именно это.

Это не совсем надежно, см. Этот пост и этот ответ .

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

провода
источник
Все ссылки мертвые. Человек, сделай что-нибудь.
Мирный кодер