从任何(不安全)字符串创建(健全/安全)文件名

时间:2011-09-13 17:40:52

标签: python cocoa filenames pyobjc

我想从一些随机的Unicode字符串(mich可能只包含任何内容)中创建一个理智/安全的文件名(即有些可读,没有“奇怪”字符等)。

(对我来说无关紧要,函数是Cocoa,ObjC,Python等。)


当然,可能会有无数多个字符可能很奇怪。因此,拥有黑名单并在一段时间内向该列表添加越来越多的内容并不是真正的解决方案。

我可以有一个白名单。但是,我真的不知道如何定义它。 [a-zA-Z0-9 .]是一个开始,但我也想接受可以正常方式显示的unicode字符。

11 个答案:

答案 0 :(得分:60)

的Python:

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

这会接受Unicode字符,但会删除换行符等。

示例:

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

给出:adblaç

修改 str.isalnum()在一步上做了字母数字。 - 来自下面的queueoverflow的评论。 danodonovan暗示要保持一个点。

    keepcharacters = (' ','.','_')
    "".join(c for c in filename if c.isalnum() or c in keepcharacters).rstrip()

答案 1 :(得分:10)

我的要求是保守的(生成的文件名需要在多个操作系统上有效,包括一些古老的移动操作系统)。我最终得到了:

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

该白色列出了字母数字字符(a-z,A-Z,0-9)和下划线。如果要匹配很多字符串,则可以编译和缓存正则表达式以提高效率。对于我的情况,它不会产生任何显着差异。

答案 2 :(得分:5)

这里有一些合理的答案,但在我的情况下,我想采取一些可能有空格和标点符号的字符串,而不是仅删除它们,我宁愿用下划线替换它。即使空间是大多数操作系统中允许的文件名字符,它们也是有问题的。另外,在我的情况下,如果原始字符串包含一段时间我不希望它传递到文件名中,或者它会生成我可能不想要的“额外扩展”(我自己附加扩展名)

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 ______死___。GIF

答案 3 :(得分:3)

此处没有解决方案,只有您必须考虑的问题:

  • 您的最小文件名长度是多少? (例如,DOS仅支持8-11个字符;大多数操作系统不支持> 256个字符)

  • 在某些情况下禁止使用哪些文件名? (Windows仍然不支持将文件保存为CON.TXT - 请参阅https://blogs.msdn.microsoft.com/oldnewthing/20031022-00/?p=42073

  • 请记住...具有特定含义(当前/父目录),因此不安全。

  • 是否存在文件名冲突的风险 - 由于删除了字符或多次使用相同的文件名?

考虑对数据进行哈希处理并使用hexdump作为文件名吗?

答案 4 :(得分:3)

或多或少在这里用regexp提及,但相反(替换未列出的任何内容):

>>> import re
>>> filename = u"ad\nbla'{-+\)(ç1?"
>>> re.sub(r'[^\w\d-]','_',filename)
u'ad_bla__-_____1_'

答案 5 :(得分:2)

的Python:

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

(只是您要删除的字符示例) 字符串前面的r确保字符串以原始格式解释,允许您删除反斜杠\

编辑: Python中的正则表达式解决方案:

import re
re.sub(r'[]/\;,><&*:%=+@!#^()|?^', '', filename)

答案 6 :(得分:1)

如果您不介意导入其他软件包,那么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/

答案 7 :(得分:0)

这是我受到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")

答案 8 :(得分:0)

我承认,关于DIY与依存关系有两种流派。但是我来自一个坚定的思想流派,他不希望重塑轮子,而不会看到像这样的简单任务的规范方法。我是pathvalidate库的粉丝

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

其中包含函数sanitize_filename(),该函数可以满足您的需求。

我将优先选择众多家庭烘焙解决方案中的任何一种。理想情况下,我想在os.path中看到一个消毒程序,它对文件系统差异敏感,并且不会进行不必要的消毒。我想象pathvalidate采用保守的方法并产生可以舒适地跨越至少NTFS和ext4的有效文件名,但是很难想象它甚至会受到旧DOS约束的困扰。

答案 9 :(得分:0)

另一种方法是为任何不需要的符号指定替换。这样文件名看起来更可读。

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

答案 10 :(得分:0)

此处许多解决方案的问题在于仅涵盖字符替换,而没有涵盖其他问题。

这是一个全面的通用解决方案,应涵盖所有基础。它可以为您处理所有类型的问题,包括(但不限于)字符替换。

可在Windows,* nix和几乎所有其他文件系统中使用。仅允许可打印字符。

import re

def txt2filename(txt, chr_set='normal'):
    """Converts txt to a valid Windows/*nix filename with printable characters only.

    args:
        txt: The str to convert.
        chr_set: 'normal', 'universal', or 'inclusive'.
            'universal':    ' -.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
            'normal':       Every printable character exept those disallowed on Windows/*nix.
            'extended':     All 'normal' characters plus the extended character ASCII codes 128-255
    """

    FILLER = '-'

    # Step 1: Remove excluded characters.
    if chr_set == 'universal':
        # Lookups in a set are O(n) vs O(n * x) for a str.
        printables = set(' -.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
    else:
        if chr_set == 'normal':
            max_chr = 127
        elif chr_set == 'extended':
            max_chr = 256
        else:
            raise ValueError(f'The chr_set argument may be normal, extended or universal; not {chr_set=}')
        EXCLUDED_CHRS = set(r'<>:"/\|?*')               # Illegal characters in Windows filenames.
        EXCLUDED_CHRS.update(chr(127))                  # DEL (non-printable).
        printables = set(chr(x)
                         for x in range(32, max_chr)
                         if chr(x) not in EXCLUDED_CHRS)
    result = ''.join(x if x in printables else FILLER   # Allow printable characters only.
                     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: Maximum length of filename is 255 bytes in Windows and Linux (other *nix flavors may allow longer names).
    result = result[:255]

    # 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

此解决方案不需要外部库。它也替代了不可打印的文件名,因为它们并不总是很容易处理。