将字符串转换为有效的文件名?

时间:2008-11-17 09:02:08

标签: python filenames slug sanitize

我有一个字符串,我想用作文件名,所以我想删除文件名中不允许使用Python的所有字符。

我宁愿比其他方面更严格,所以我要说我只想保留字母,数字和一小部分其他字符,例如"_-.() "。什么是最优雅的解决方案?

文件名需要在多个操作系统(Windows,Linux和Mac OS)上有效 - 它是我的库中的MP3文件,歌曲标题为文件名,并在3台机器之间共享和备份。

25 个答案:

答案 0 :(得分:138)

您可以查看Django framework如何从任意文本创建“slug”。 slug是URL和文件名友好的。

Django text utils定义了一个函数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))

还有更多,但我把它排除在外,因为它不会解决挫败问题,而是逃避。

答案 1 :(得分:97)

如果对文件的格式或非法的有效字符组合(例如“..”)没有限制,则此白名单方法(即仅允许valid_chars中存在的字符)将起作用,例如,您所说的将允许名为“.txt”的文件名,我认为该文件在Windows上无效。由于这是最简单的方法,我尝试从valid_chars中删除空格并在出现错误时添加已知的有效字符串,任何其他方法都必须知道允许在何处处理Windows file naming limitations因此要复杂得多。

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

答案 2 :(得分:93)

您可以将列表理解与字符串方法结合使用。

>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'

答案 3 :(得分:88)

将字符串用作文件名的原因是什么?如果人类可读性不是一个因素,我会使用base64模块,它可以生成文件系统安全字符串。它不可读,但你不必处理碰撞,它是可逆的。

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

更新:根据Matthew评论进行了更改。

答案 4 :(得分:37)

只是为了使事情进一步复杂化,您无法保证仅通过删除无效字符就能获得有效的文件名。由于允许的字符在不同的文件名上有所不同,因此保守的方法最终可能会将有效名称转换为无效的名称。您可能希望为以下情况添加特殊处理:

  • 字符串是所有无效字符(留下空字符串)

  • 您最终得到一个具有特殊含义的字符串,例如“。”或“..”

  • 在Windows上,保留certain device names。例如,您无法创建名为“nul”,“nul.txt”(或实际上为nul.anything)的文件。保留名称为:

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

您可以解决这些问题,方法是将一些字符串添加到永远不会导致其中一种情况的文件名,并删除无效字符。

答案 5 :(得分:22)

Github上有一个名为python-slugify的好项目:

安装:

pip install python-slugify

然后使用:

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

答案 6 :(得分: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调用用非重音等效替换重音字符,这比简单地剥离它们要好。之后,所有不允许的字符都被删除。

我的解决方案不会添加已知字符串以避免可能的不允许的文件名,因为我知道在给定我的特定文件名格式时它们不会发生。更通用的解决方案需要这样做。

答案 7 :(得分:17)

就像S.Lott回答一样,您可以查看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

答案 8 :(得分: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刻录后的乐趣;)

答案 9 :(得分:8)

在一行中:

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

你也可以添加'_'字符以使其更具可读性(例如,在替换斜杠时)

答案 10 :(得分:7)

您可以使用re.sub()方法替换不是“filelike”的任何内容。但实际上,每个角色都是有效的;所以没有预先构建的功能(我相信),以完成它。

import re

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

会导致文件句柄为/tmp/filename.txt。

答案 11 :(得分: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'等)。

答案 12 :(得分:6)

虽然你必须要小心。如果您只关注拉丁语言,那么在您的介绍中并没有明确说明。如果只使用ascii字符对它们进行消毒,有些单词可能会变得毫无意义或其他含义。

想象你有“forêtpoésie”(森林诗歌),你的消毒可能会给“堡垒”(强烈+无意义的东西)

如果你不得不处理中文字符,那就更糟了。

“下北沢”你的系统可能最终会做“---”,注定会在一段时间后失败并且不是很有帮助。因此,如果您只处理文件,我会鼓励将它们称为您控制的通用链或保持字符不变。对于URI,大致相同。

答案 13 :(得分:6)

为什么不用try / except包装“osopen”并让底层操作系统判断文件是否有效?

无论您使用哪种操作系统,这似乎都要少得多,并且有效。

答案 14 :(得分: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讨厌空格并使用'。'作为目录分隔符。

答案 15 :(得分:3)

如果您不介意安装软件包,这将非常有用: 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

答案 16 :(得分:2)

大多数这些解决方案都不起作用。

'/ hello / world' - &gt; '的HelloWorld'

'/ helloworld'/ - &gt; '的HelloWorld'

这通常不是你想要的,比如你要保存每个链接的html,你要覆盖不同网页的html。

我挑了一个如:

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

2表示应附加到下一个文件名的数字。

我每次都从字典中查找文件名。如果它不在那里,我会创建一个新的,如果需要附加最大数字。

答案 17 :(得分: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)。

答案 18 :(得分:1)

遇到同样的问题时,我使用了 python-slugify。

Shoham 也建议使用,但正如 therealmarv 指出的那样,默认情况下 python-slugify 也会转换点。

可以通过在 regex_pattern 参数中包含点来否决此行为。

> filename = "This is a väryì' Strange File-Nömé.jpeg"
> pattern = re.compile(r'[^-a-zA-Z0-9.]+')
> slugify(filename,regex_pattern=pattern) 
'this-is-a-varyi-strange-file-nome.jpeg'

请注意,正则表达式模式是从

ALLOWED_CHARS_PATTERN_WITH_UPPERCASE

python-slugify 包的 slugify.py 文件中的全局变量,并以“.”进行扩展

请记住,像 .() 这样的特殊字符必须用 \ 转义。

如果您想保留大写字母,请使用 lowercase=False 参数。

> filename = "This is a väryì' Strange File-Nömé.jpeg"
> pattern = re.compile(r'[^-a-zA-Z0-9.]+')
> slugify(filename,regex_pattern=pattern, lowercase=False) 
'This-is-a-varyi-Strange-File-Nome.jpeg'

这使用 Python 3.8.4 和 python-slugify 4.0.1

答案 19 :(得分: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+-_ .'))

结果“有点”可读,至少从系统管理员的角度来看。

答案 20 :(得分:0)

Windows 特定路径的另一个答案,使用简单的替换并且没有时髦的模块:

import re

def check_for_illegal_char(input_str):
    # remove illegal characters for Windows file names/paths 
    # (illegal filenames are a superset (41) of the illegal path names (36))
    # this is according to windows blacklist obtained with Powershell
    # from: https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names/44750843#44750843
    #
    # PS> $enc = [system.Text.Encoding]::UTF8
    # PS> $FileNameInvalidChars = [System.IO.Path]::GetInvalidFileNameChars()
    # PS> $FileNameInvalidChars | foreach { $enc.GetBytes($_) } | Out-File -FilePath InvalidFileCharCodes.txt

    illegal = '\u0022\u003c\u003e\u007c\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008' + \
              '\u0009\u000a\u000b\u000c\u000d\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015' + \
              '\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f\u003a\u002a\u003f\u005c\u002f' 

    output_str, _ = re.subn('['+illegal+']','_', input_str)
    output_str = output_str.replace('\\','_')   # backslash cannot be handled by regex
    output_str = output_str.replace('..','_')   # double dots are illegal too, or at least a bad idea 
    output_str = output_str[:-1] if output_str[-1] == '.' else output_str # can't have end of line '.'

    if output_str != input_str:
        print(f"The name '{input_str}' had invalid characters, "
              f"name was modified to '{output_str}'")

    return output_str

使用 check_for_illegal_char('fas\u0003\u0004good\\..asd.') 进行测试时,我得到:

The name 'fas♥♦good\..asd.' had invalid characters, name was modified to 'fas__good__asd'

答案 21 :(得分:0)

这里,这应该涵盖所有基础。它可以为您处理所有类型的问题,包括(但不限于)字符替换。

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

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

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

答案 22 :(得分:0)

为python 3.6修改的答案

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)

答案 23 :(得分: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, '')

答案 24 :(得分: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 ANSWER

bobcat项目包含一个只执行此操作的python模块。

它并不完全健壮,请参阅此post和此reply

因此,如上所述:base64编码可能是一个更好的主意,如果可读性无关紧要。