Перетворити рядок у дійсне ім'я файлу?


298

У мене є рядок, який я хочу використовувати як ім'я файлу, тому я хочу видалити всі символи, які не були дозволені у іменах, використовуючи Python.

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

Ім’я файлу має бути дійсним у кількох операційних системах (Windows, Linux та Mac OS) - це MP3-файл у моїй бібліотеці із назвою пісні як ім'я файлу, і його можна спільно використовувати та створити резервну копію між 3-ма машинами.


17
Чи не слід це вбудовувати в модуль os.path?
ендоліт

2
Можливо, хоча для її використання потрібен буде єдиний безпечний шлях для всіх платформ, а не лише поточний, який os.path не призначений для обробки.
javawizard

2
Для розширення вищезазначеного коментаря: поточна конструкція os.pathфактично завантажує іншу бібліотеку залежно від ОС (див. Другу примітку в документації ). Отже, якщо в ньому була реалізована функція котирування, os.pathвона могла б цитувати лише рядок для POSIX-безпеки під час роботи в системі POSIX або для безпеки Windows під час роботи на Windows. Отримане ім'я файлу не обов'язково було б дійсним як для Windows, так і для POSIX, саме про це і задається питання.
дшеферд

Відповіді:


164

Ви можете подивитися на рамку Джанго, як вони створюють "кулі" з довільного тексту. Слизька є 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] +', '-', значення))
Джозеф Туріан

1
Дякую - я міг би чогось пропустити, але я отримую: "нормалізація () аргумент 2 повинен бути unicode, а не str"
Alex Cook

"нормалізувати () аргумент 2". Є в виду value. Якщо значення повинно бути Unicode, то ви повинні бути впевнені, що це насправді Unicode. Або Ви можете залишити нормалізацію Unicode, якщо фактичне значення - це фактично рядок ASCII.
С.Лотт

8
Якщо хтось не помітив позитивної сторони цього підходу, це те, що він не просто видаляє символи, що не містять альфа, але намагається спочатку знайти хороші замінники (через нормалізацію NFKD), так що це стає e, а надпис 1 стає а нормальний 1 і т. д. Спасибі
Майкл Скотт Катберт

48
slugifyФункція була переміщена в Джанго / Utils / text.py , і цей файл також містить get_valid_filenameфункцію.
Denilson Sá Maia

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 рази швидше, якщо застосовувати його до всіхчасів.
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.comhelhelorld' >>/ ">>>" ".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
Зауважте, що квадратні дужки можна опустити. У цьому випадку вираз генератора передається для приєднання, що зберігає крок створення списку, що не використовується в іншому випадку.
Обен Сонне

31
+1 Полюбив це. Незначна модифікація, яку я зробив: "" .join ([x, якщо x.isalnum () else "_" для x in 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
Очевидно, що це найкраща відповідь, якщо це так.
користувач32141

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

40

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

  • Рядок - це всі недійсні символи (залишаючи порожній рядок)

  • Ви закінчуєте рядок зі спеціальним значенням, наприклад "." або ".."

  • У Windows певні назви пристроїв зарезервовані. Наприклад, ви не можете створити файл з назвою "nul", "nul.txt" (або nul. Усе, що насправді) Зарезервовані імена:

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

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


24

Є хороший проект на Github під назвою python-slugify :

Встановити:

pip install python-slugify

Потім використовуйте:

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

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 )


4
для ледачих вже на джанго:django.utils.text import get_valid_filename
диктор

2
Якщо ви не знайомі з регулярним виразом, re.sub(r'(?u)[^-\w.]', '', s)видаляє всі символи, які не букви, не цифри (0-9), не підкреслення ('_'), не тире ('-'), а не період ('). ). "Літери" сюди містять усі унікодні букви, такі як 漢語.
ковбасник

3
Ви можете також перевірити довжину: Імена файлів обмежені 255 символами (або, знаєте, 32; залежно від FS)
Matthias Winkelmann

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 -лаб
-rw-r - r-- 0 17 листопада 23:39 \ neven \ multiline \ nhaha \ n \ 033 [31m \ red \ \ 033 [0m \ nevil
$ perl -e 'для мого $ i (glob (q {./* навіть *})) {print $ i; } '
./
навіть багаторядковий
ха-ха
 червоний 
зло

Так, я просто зберігав ANSI кольорові коди у назві файлу, і вони набули чинності.

Для розваги введіть символ BEL у назву каталогу та спостерігайте за задоволенням, що настає, коли ви на них CD;)


В ОП зазначається, що "Ім'я файлу має бути дійсним для декількох операційних систем"
cowlinator

1
@cowlinator, що пояснення було додано через 10 годин після опублікування моєї відповіді :) Перевірте журнал редагування ОП.
Кент Фредрік

12

В одному рядку:

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.


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' тощо).


+1 для таблиць перекладу, це, безумовно, найбільш ефективний метод. Для спеціальних імен файлів / порожніх місць буде достатня проста перевірка попередньої умови, а для сторонніх періодів це також проста корекція.
Крістіан Віттс

1
Хоча переклад є дещо ефективнішим, ніж регулярний вираз, цей час, швидше за все, буде карликовим, якщо ви насправді спробуєте відкрити файл, що, без сумніву, ви маєте намір зробити. Таким чином, я віддаю перевагу більш читабельному ріджекс-рішенню, ніж безлад вище
ноталійський

Я також переживаю за чорний список. Звичайно, це чорний список, заснований на списку, але все ж. Здається менш ... безпечно. Звідки ти знаєш, що "всехарактеристики" насправді завершені?
isaaclw

@isaaclw: '.translate ()' приймає рядок 256 знаків як таблицю перекладу (переклад байтів у байт). '.maketrans ()' створює такий рядок. Усі значення охоплені; це чистий білий підхід
jfs

Як щодо імені файлу "." (одна крапка). Це не буде працювати на Unixes, оскільки в цьому каталозі використовується це ім'я.
Фінн Еруп Нільсен

6

Хоча треба бути обережним. Це не чітко сказано у вашому вступі, якщо ви дивитесь лише на латинську мову. Деякі слова можуть набути безглуздого або іншого значення, якщо ви їх санітуєте лише символами ascii.

уявіть, що у вас є "forêt poésie" (лісова поезія), ваша санітарія може дати "fort-posie" (сильний + щось безглузде)

Гірше, якщо вам доведеться мати справу з китайськими символами.

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


6

Чому б просто не обернути "osopen" спробуйте / за винятком і дозволити базовій ОС розібратися, чи дійсний файл?

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


5
Чи дійсно це ім'я? Я маю на увазі, якщо ОС не задоволена, то вам все одно потрібно щось робити, правда?
jeromej

1
У деяких випадках ОС / Мова може мовчки з’єднати ваше ім'я файлу в альтернативній формі, але коли ви перелічите каталог, ви отримаєте інше ім’я. І це може призвести до проблеми "коли я записую його файл там, але коли я шукаю файл, який називається чимось іншим". (Я кажу про поведінку, про яку я чув про 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 OS ненавидить місця та використання '.' як роздільник каталогів.


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тут).


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)

Чи можете ви детально пояснити свою відповідь?
Serenity

Цю ж відповідь прийняла Софі Гейдж. Але він був модифікований для роботи на пітоні 3.6
Жан-Робін Тремблей

2

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

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.


1

Більшість цих рішень не працюють.

'/ hello / world' -> 'helloworld'

'/ helloworld' / -> 'helloworld'

Це, як правило, не те, що ви хочете, скажімо, ви зберігаєте HTML для кожного посилання, ви збираєтеся замінити HTML на іншу веб-сторінку.

Я підбираю диктант, такий як:

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

2 представляє число, яке слід додати до наступного імені файлу.

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


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

1

Не зовсім те, що просила ОП, але це те, що я використовую, тому що мені потрібні унікальні та оборотні перетворення:

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

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


Обгортка для цього без пробілів у назвах файлів: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, '')

Я знайшов це "".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'

ОРИГІНАЛЬНА ВІДПОВІДЬ :

bobcatПроект містить модуль пітона , який робить саме це.

Це не зовсім надійно, дивіться цю публікацію та цю відповідь .

Отже, як зазначалося: base64кодування, мабуть, краща ідея, якщо читабельність не має значення.


Усі посилання мертві. Людина, роби щось.
Мирний кодер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.