Вопрос по sanitize, python, filenames, slug – Превратить строку в правильное имя файла?

248

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

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

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

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

Ваш Ответ

21   ответ
13

Имейте в виду, на самом деле нет никаких ограничений на имена файлов в системах 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 в имя каталога и посмотрите, как весело, когда вы вставляете в него CD;)

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

Вы можете посмотреть наРамки Джанго за то, как они создают «слизняк» из произвольного текста. Слаг - это 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))

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

Я нашел полезным также заменить пробелы на "-" Claudiu
slugify функция была перемещена вДжанго / Utils / text.pyи этот файл также содержитget_valid_filename функция. Denilson Sá Maia
Должен был быть принят ответ swdev
Последняя строка должна быть: value = unicode (re.sub ('[- \ s] +', '-', value)) Joseph Turian
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' и т. Д.).

+1 для таблиц перевода, это, безусловно, самый эффективный метод. Для специальных имен файлов / пустых мест будет достаточно простой проверки предварительных условий, а для посторонних периодов это также будет простым исправлением. Christian Witts
Хотя translate немного более эффективен, чем регулярное выражение, это время, скорее всего, будет меньше, если вы действительно попытаетесь открыть файл, что, без сомнения, вы собираетесь делать. Таким образом, я предпочитаю более удобочитаемое решение регулярных выражений, чем беспорядок выше nosatalian
Я также беспокоюсь о черном списке. Конечно, это черный список, основанный на белом списке, но все же. Это кажется менее ... безопасным. Откуда вы знаете, что «allchars» на самом деле завершена? isaaclw
Как насчет имени файла. (одна точка). Это не будет работать в Unixes, поскольку нынешний каталог использует это имя. Finn Årup Nielsen
@isaaclw: '.translate ()' принимает строку из 256 символов в качестве таблицы перевода (межбайтовый перевод). '.maketrans ()' создает такую ​​строку. Все значения покрыты; это чисто белый подход jfs
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 = se,t(map(lambda x: ord(x), '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-_ .'))

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

Обертка для этого без пробелов в именах файлов:def safe_filename(filename): return safePath(filename.strip().replace(' ','_')) SpeedCoder5
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, '')
Я нашел это"".join( x for x in s if (x.isalnum() or x in "._- ")) на этомсообщение Комментарии SergioAraujo
2

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

'/ hello / world' -> 'helloworld'

'/ helloworld' / -> 'helloworld'

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

Я задираю такие слова, как:

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

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

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

обратите внимание, если вы используете helloworld1, вам также нужно проверить, что helloworld1 не используется и так далее .. robert king
6

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

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

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

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

6

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

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

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

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

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

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

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.

Вам нужен тире, чтобы идти первым в групповом совпадении, чтобы он не отображался как диапазон. re.sub ('[^ - a-zA-Z0-9 _. ()] +', '', str) phord
«(? i)» здесь не нужно. jfs
Правда, извините, привычки. gx.
88

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

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

В Python 3your_string должен быть байтовым массивом или результатомencode('ascii') чтобы это работало. Noumenon
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') JeffProd
На самом деле удобочитаемость почти всегда является фактором, даже если только для целей отладки. static_rtti
Предупреждение! Кодировка base64 по умолчанию включает символ "/" в качестве допустимого вывода, который недопустим в именах файлов во многих системах. Вместо этого используйте base64.urlsafe_b64encode (your_string) Matthew
21

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

Установка:

pip install python-slugify

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

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

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

89

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

>>> s
'foo-bar#[email protected]/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
x.isalnum () делает то же самое schlamar
К сожалению, это даже не позволяет пробелы и точки, но мне нравится идея. tiktak
Хотя это не включает несколько дополнительных символов, которые он хотел "_-. ()". Тем не менее, мое любимое решение, хотя;) matt burns
Это прекрасно работает, и является самым простым из всех. Torbjørn Kristoffersen
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 Проект содержит модуль Python, который делает именно это.

Это не совсем надежно, смотрите этосообщение и этоОтветить.

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

Документыhttps://svn.origo.ethz.ch/bobcat/src-doc/safefilename-module.htmlИсточникhttps://svn.origo.ethz.ch/bobcat/trunk/src/bobcatlib/safefilename.py
Все ссылки мертвые. Человек, сделай что-нибудь. The Peaceful Coder
Ха, я должен удалить или обновить этот ответ. wires
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 Вот).

96

Этот подход белого списка (то есть, разрешающий только символы, присутствующие в 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%
>>> 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'
amp;$ .txt" >>> ''.join(c for c in filename if c in valid_chars) 'This Is a (valid) - filename .txt'
Не говоря уже о том, что имя файла"CON" на Windows вы попадете в беду ... Nathan Osman
valid_chars = frozenset(valid_chars) не повредит. Это в 1,5 раза быстрее, если применяется к allchars. jfs
Предупреждение: это отображает две разные строки в одну строку >>> строка импорта >>> 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' >>> robert king
Небольшая перестановка делает определение замещающего символа простым. Сначала исходная функциональность: '' .join (c, если c в valid_chars else '' для c в имени файла) или с замененным символом или строкой для каждого недопустимого символа: '' .join (c если c в valid_chars else '.' Для c в имени файла) PeterVermont
5

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

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

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

import re
badchars= re.compile(r'[^A-Za-z0-9_. ]+|^\.|\.$|^ | $|^

Даже это не может быть гарантировано, особенно в неожиданных ОС - например, ОС RISC ненавидит пробелы и использует «.» В качестве разделителя каталогов.

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

18

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

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
дело верблюда .. ааа demented hedgehog
Может ли это быть отредактировано / обновлено для работы с Python 3.6? Wavesailor
слишком многословно ааа Claudiu
34

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

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

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

На окнах,определенные имена устройств зарезервированы Например, вы не можете создать файл с именем «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

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

где код? SpeedCoder5
15

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

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

Похожие вопросы