Создать (нормальное / безопасное) имя файла из любой (небезопасной) строки

Я хочу создать разумное / безопасное имя файла (т.е. несколько удобочитаемое, без «странных» символов и т. Д.) Из некоторой случайной строки Unicode (mich может содержать что угодно).

(Для меня не имеет значения, является ли функция Cocoa, ObjC, Python и т. Д.)


Конечно, персонажей может быть бесконечное множество, что может показаться странным. Таким образом, иметь черный список и со временем добавлять в него все больше и больше - не лучшее решение.

Я мог бы иметь белый список. Однако я действительно не знаю, как это определить. [a-zA-Z0-9 .] - это начало, но я также хочу принимать символы Unicode, которые могут отображаться обычным образом.


person Albert    schedule 13.09.2011    source источник
comment
Правильно ли я понимаю, что вы хотите, чтобы это было интернационализировано?   -  person N_A    schedule 13.09.2011
comment
@mydogisbox: Нет, только одно имя файла (unicode) из входных данных.   -  person Albert    schedule 13.09.2011
comment
«Никаких странных символов ... но я также хочу принимать символы Unicode, которые могут отображаться как обычно». Проблема в том, что между этими наборами есть пересечение. Например, если пользователь пишет статью о Феликс Дзержинский, то это «р» латинское? «р» или кириллица «р»? (Да, это действительно два разных символа. Вставьте в UnicodeChecker, чтобы увидеть.)   -  person Peter Hosey    schedule 13.09.2011
comment
… Что касается того, почему это «странный» персонаж, несколько лет назад появилось множество новостей и аналитических отчетов о том, как фишинговые мошенники начали использовать подобные символы для создания поддельных, но реально выглядящих доменных имен («paypal.com» , для примера, придуманного только что). По этой причине браузеры, такие как Safari, теперь отображают такие домены как «Punycode» (что-то вроде half-base64 half-ASCII). Итак, этот персонаж и многие ему подобные могут быть использованы во благо или во зло - и в этом проблема.   -  person Peter Hosey    schedule 13.09.2011
comment
Поскольку это не взаимно-однозначное сопоставление символов, похоже, что вам также необходимо проверить повторяющиеся имена файлов.   -  person octern    schedule 21.05.2012
comment
Дубликат: stackoverflow.com/questions/295135/   -  person jmetz    schedule 12.03.2014
comment
-1. Я не думаю, что этот вопрос вообще четко сформулирован. Вменяемое и странное ничего не значат. Либо примите все, что фактически принимает файловая система (в этом случае этот вопрос является дубликатом), либо примите четко определенное подмножество ascii (в этом случае этот вопрос тривиален).   -  person Clément    schedule 12.12.2015
comment
@ Клеман: Офк, это не совсем точно. Вопрос также был в том смысле, что, возможно, есть какой-то прямой ответ, поэтому ваш комментарий является своего рода ответом нет, его нет, но я этого не знаю. Может быть, Unicode определяет что-то вроде невидимых (странных) символов или канонических символов или что-то в этом роде. Я не знаю. В любом случае, принятый ответ довольно прост, и теперь я доволен им. И это не те два случая, которые вы описываете, это намного лучше.   -  person Albert    schedule 12.12.2015


Ответы (11)


Python:

"".join([c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()

это принимает символы Unicode, но удаляет разрывы строк и т. д.

пример:

filename = u"ad\nbla'{-+\)(ç?"

дает: adblaç

edit str.isalnum () выполняет буквенно-цифровую обработку за один шаг. - комментарий из queueoverflow ниже. данодонован намекнул, что стоит оставить точку.

    keepcharacters = (' ','.','_')
    "".join(c for c in filename if c.isalnum() or c in keepcharacters).rstrip()
person Remi    schedule 13.09.2011
comment
О, круто, да, я не знал, что str.isalpha() также работает с такими символами Unicode. - person Albert; 13.09.2011
comment
Разве здесь не пропущены пробелы? - person Peter Hosey; 13.09.2011
comment
На самом деле ... Это проблема для @Albert? В противном случае просто добавьте or x==' '. Накладные расходы небольшие, потому что это будет последнее, на что нужно обращать внимание. - person Remi; 13.09.2011
comment
@Peter: Да, но из этого ответа было достаточно легко сделать мою собственную функцию точно соответствующей моим потребностям. c.isalpha() достаточно близко к тому, что я искал. Конечно, это все еще не идеально (и вы привели хороший пример в своем комментарии к вопросу о разных ps). - person Albert; 13.09.2011
comment
Это небезопасно в Windows. Во-первых, вам необходимо защититься от устаревших имен файлов устройств, таких как CON и NUL. Во-вторых, как насчет чувствительности к регистру? Вы можете случайно перезаписать другой файл. В-третьих, имена файлов с пробелами в конце неправильно обрабатываются Python в Windows. Это как минимум три способа избавиться от него с головы. - person Antimony; 01.10.2012
comment
для вашего 3-го замечания я добавил rstrip(). Что касается CON, NUL и т. Д., Возможно, желаемый файл можно проверить, чтобы он закончился только одним из фиксированного списка разрешенных расширений файлов? Что касается чувствительности к регистру и перезаписи файла: имя файла, по крайней мере, является допустимым именем, следующим шагом должна быть проверка, если файл еще не существует, прежде чем вы перезаписываете (например, используйте os.path.exists()) - person Remi; 03.10.2012
comment
Есть даже str.isalnum(), который делает буквенно-цифровые символы за один шаг. - person Martin Ueding; 09.12.2012
comment
Чтобы не убирать точку (точка) . попробуйте `.join (c вместо c в имени файла, если c.isalnum () или c в ['', '.']). Rstrip () ` - person danodonovan; 12.04.2013
comment
Символы Unicode могут вызывать проблемы в некоторых старых файловых системах - вероятно, лучше всего использовать код unidecode или аналогичный для преобразования символов в безопасные символы ASCII. Кроме того, может быть хорошей идеей удалить пробелы. - person naught101; 29.04.2014
comment
Крошечное подмножество потенциально опасных имен файлов, через которые оно может пройти: имя файла длиной 5 гигабайт, ......., nul, dir.exe, пустая строка. - person Bob Stein; 25.11.2015

Мои требования были консервативными (сгенерированные имена файлов должны были быть действительными в нескольких операционных системах, включая некоторые старые мобильные ОС). В итоге я получил:

    "".join([c for c in text if re.match(r'\w', c)])

В этом белом списке перечислены буквенно-цифровые символы (a-z, A-Z, 0-9) и подчеркивание. Регулярное выражение можно скомпилировать и кэшировать для повышения эффективности, если нужно сопоставить много строк. В моем случае это не имело бы особого значения.

person Ngure Nyaga    schedule 21.05.2012

Более или менее то, что было упомянуто здесь с регулярным выражением, но в обратном порядке (замените все НЕ перечисленные):

>>> import re
>>> filename = u"ad\nbla'{-+\)(ç1?"
>>> re.sub(r'[^\w\d-]','_',filename)
u'ad_bla__-_____1_'
person Filipe Pina    schedule 12.07.2018
comment
Просто используйте \W, который соответствует чему-либо, кроме буквы, цифры или символа подчеркивания. Эквивалент [^a-zA-Z0-9_]. - person Escape0707; 25.01.2021

Здесь есть несколько разумных ответов, но в моем случае я хочу взять что-то, что представляет собой строку, которая может иметь пробелы и знаки препинания, и вместо того, чтобы просто удалять их, я бы предпочел заменить ее подчеркиванием. Несмотря на то, что пробелы являются допустимым символом имени файла в большинстве ОС, они проблематичны. Кроме того, в моем случае, если исходная строка содержала точку, я не хотел, чтобы она передавалась в имя файла, или она генерировала «дополнительные расширения», которые мне могли не понадобиться (я добавляю расширение сам)

def make_safe_filename(s):
    def safe_char(c):
        if c.isalnum():
            return c
        else:
            return "_"
    return "".join(safe_char(c) for c in s).rstrip("_")

print(make_safe_filename( "hello you crazy $#^#& 2579 people!!! : die!!!" ) + ".gif")

печатает:

hello_you_crazy _______ 2579_people ______ die ___. gif

person uglycoyote    schedule 21.04.2017
comment
Я думаю, что эта функция могла бы быть лучше, если бы повторяющиеся подчеркивания были заменены одним подчеркиванием. re.sub('_{2,}', '_', 'hello_you_crazy_______2579_people______die___.gif') `>> 'hello_you_crazy_2579_people_die_.gif' - person Xevion; 18.03.2020
comment
@Xevion Но это еще больше увеличивает вероятность того, что разные строки отображаются на одно и то же имя файла. - person BlackJack; 26.02.2021

Здесь нет решений, только проблемы, которые вы должны учитывать:

  • какова ваша минимальная максимальная длина имени файла? (например, DOS поддерживает только 8-11 символов; большинство ОС не поддерживают> 256 символов)

  • какие имена файлов запрещены в каком-то контексте? (Windows по-прежнему не поддерживает сохранение файла как CON.TXT - см. https://blogs.msdn.microsoft.com/oldnewthing/20031022-00/?p=42073)

  • помните, что . и .. имеют определенное значение (текущий / родительский каталог) и поэтому небезопасны.

  • существует ли риск совпадения имен файлов - либо из-за удаления символов, либо из-за того, что одно и то же имя файла используется несколько раз?

Подумайте только о хешировании данных и использовании их шестнадцатеричного дампа в качестве имени файла?

person Dragon    schedule 21.11.2017

Если вы не против импортировать другие пакеты, то у werkzeug есть метод очистки строк:

from werkzeug.utils import secure_filename

secure_filename("hello.exe")
'hello.exe'
secure_filename("/../../.ssh")
'ssh'
secure_filename("DROP TABLE")
'DROP_TABLE'

#fork bomb on Linux
secure_filename(": () {: |: &} ;:")
''

#delete all system files on Windows
secure_filename("del*.*")
'del'

https://pypi.org/project/Werkzeug/

person Anders_K    schedule 05.03.2020

Я признаю, что есть две точки зрения относительно DIY и зависимостей. Но я пришел из твердой школы мысли, которая предпочитает не изобретать колеса и видеть канонические подходы к таким простым задачам, как эта. То есть я фанат библиотеки pathvalidate

https://pypi.org/project/pathvalidate/

Что включает в себя функцию sanitize_filename(), которая делает то, что вам нужно.

Я бы предпочел это любому из множества решений домашней выпечки. В идеале я бы хотел видеть в os.path дезинфицирующее средство, чувствительное к различиям файловых систем и не выполняющее ненужную дезинфекцию. Я предполагаю, что pathvalidate использует консервативный подход и создает допустимые имена файлов, которые могут удобно охватывать как минимум NTFS и ext4, но трудно представить, что он вообще беспокоит старые ограничения DOS.

person Bernd Wechner    schedule 18.12.2019

Проблема со многими другими ответами заключается в том, что они имеют дело только с заменой символов; не другие вопросы.

Вот комплексное универсальное решение. Он решает все типы проблем, включая (но не ограничиваясь) замену символов. Он должен охватывать все основы.

Работает в Windows, * nix и почти во всех других файловых системах.

def txt2filename(txt, chr_set='printable'):
    """Converts txt to a valid filename.

    Args:
        txt: The str to convert.
        chr_set:
            'printable':    Any printable character except those disallowed on Windows/*nix.
            'extended':     'printable' + extended ASCII character codes 128-255
            'universal':    For almost *any* file system. '-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    """

    ext = '' if '.' not in txt else txt[txt.rfind('.'):]

    FILLER = '-'
    MAX_LEN = 255  # Maximum length of filename is 255 bytes in Windows and some *nix flavors.

    # Step 1: Remove excluded characters.
    BLACK_LIST = set(chr(127) + r'<>:"/\|?*')                           # 127 is unprintable, the rest are illegal in Windows.
    white_lists = {
        'universal': {'-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'},
        'printable': {chr(x) for x in range(32, 127)} - BLACK_LIST,     # 0-32, 127 are unprintable,
        'extended' : {chr(x) for x in range(32, 256)} - BLACK_LIST,
    }
    white_list = white_lists[chr_set]
    result = ''.join(x
                     if x in white_list else FILLER
                     for x in txt)

    # Step 2: Device names, '.', and '..' are invalid filenames in Windows.
    DEVICE_NAMES = 'CON,PRN,AUX,NUL,COM1,COM2,COM3,COM4,' \
                   'COM5,COM6,COM7,COM8,COM9,LPT1,LPT2,' \
                   'LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,' \
                   'CONIN$,CONOUT$,..,.'.split()  # This list is an O(n) operation.
    if result in DEVICE_NAMES:
        result = f'-{result}-'

    # Step 3: Truncate long files while preserving the file extension.
    result = result[:MAX_LEN - len(ext)] + ext

    # Step 4: Windows does not allow filenames to end with '.' or ' ' or begin with ' '.
    result = re.sub(r'^[. ]', FILLER, result)
    result = re.sub(r' $', FILLER, result)

return result

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

Никаких внешних библиотек не требуется.

person ChaimG    schedule 18.12.2020

Python:

for c in r'[]/\;,><&*:%=+@!#^()|?^':
    filename = filename.replace(c,'')

(просто пример символов, которые вы хотите удалить) r перед строкой гарантирует, что строка интерпретируется в необработанном формате, что позволяет вам также удалить обратную косую черту \

Изменить: решение регулярного выражения в Python:

import re
re.sub(r'[]/\;,><&*:%=+@!#^()|?^', '', filename)
person Remi    schedule 13.09.2011
comment
Может быть бесконечное количество символов, что может показаться странным. На самом деле это не решение, чтобы со временем добавлять к этому списку все больше и больше. - person Albert; 13.09.2011
comment
Я понимаю; известны РАЗРЕШЕННЫЕ символы? - person Remi; 13.09.2011
comment
Я действительно не знаю, как определить разрешенные символы. В основном я имею в виду все символы, которые могут отображаться и не имеют странного поведения (в том смысле, что они имеют отрицательную ширину или добавляют новую строку или около того). Вот что я имею в виду под словом «нормальный». Это, по сути, весь вопрос, иначе это было бы тривиально. - person Albert; 13.09.2011
comment
Я думаю, вы скорее хотите] [[] захватить и [и]. Хотя я не уверен - person ealfonso; 23.09.2013
comment
@Albert: Unicode не бесконечен, и как пользователь, если я собираюсь ввести имя файла, я действительно не хочу, чтобы странная логика программы решала, что я могу или не могу вставить туда. Удалите ровно столько, сколько нужно для обеспечения безопасности (например, разделители каталогов и маркеры относительного пути, такие как . и ..), это нормально, но удалить больше? Я не уверен. - person Clément; 12.12.2015
comment
Совершенно уверен, что это регулярное выражение неверно. [- это специальный символ в регулярном выражении. - person Carson Ip; 03.01.2020
comment
-1. Решение с регулярным выражением явно не проверено. Как указывает @CarsonIp, он использует символы, зарезервированные для регулярных выражений, не только [, но и ]*+^?|. Из-за этого регулярное выражение не компилируется. Кроме того, этот подход просто не работает в целом, потому что, как указывает OP, черный список символов просто плохо масштабируется, поэтому белый список, вероятно, предпочтительнее. - person Graham; 31.03.2020

Вот что я пришел, вдохновленный uglycoyote:

import time

def make_safe_filename(s):
    def safe_char(c):
        if c.isalnum() or c=='.':
            return c
        else:
            return "_"

    safe = ""
    last_safe=False
    for c in s:
      if len(safe) > 200:
        return safe + "_" + str(time.time_ns() // 1000000)

      safe_c = safe_char(c)
      curr_safe = c != safe_c
      if not last_safe or not curr_safe:
        safe += safe_c
      last_safe=curr_safe
    return safe

И для проверки:

print(make_safe_filename( "hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!hello you crazy $#^#& 2579 people!!! : hi!!!" ) + ".gif")
person Martin Kunc    schedule 24.09.2019

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

>>> substitute_chars = {'/':'-', ' ':''}
>>> filename = 'Cedric_Kelly_12/10/2020 7:56 am_317168.pdf'
>>> "".join(substitute_chars.get(c, c) for c in filename)
'Cedric_Kelly_12-10-20207:56am_317168.pdf'
person Dmitry    schedule 10.12.2020