关于python:将字符串转换为有效的文件名?

Turn a string into a valid filename?

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

我宁愿严格,而不是严格,所以假设我只保留字母、数字和一小部分其他字符,如"_-.()"。最优雅的解决方案是什么?

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


您可以查看django框架,了解它们如何从任意文本创建"slug"。slug是URL和文件名友好的。

django文本实用程序定义了一个函数,slugify(),这可能是此类事情的黄金标准。基本上,他们的代码如下。

1
2
3
4
5
6
7
8
9
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))

还有更多的,但我把它忘了,因为它不涉及弹射,而是逃避。


这种白名单方法(即,只允许有效字符中的字符)将工作,如果没有限制的格式文件或有效字符的组合是非法的(如".."),例如,您所说的将允许一个名为""的文件名。txt"我认为它在Windows上无效。由于这是最简单的方法,我将尝试从有效字符中删除空白,并在出现错误的情况下预先添加已知的有效字符串,因此任何其他方法都必须知道在哪里可以处理Windows文件命名限制,因此要复杂得多。

1
2
3
4
5
6
7
>>> 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'


可以将列表理解与字符串方法一起使用。

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


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

1
2
import base64
file_name_string = base64.urlsafe_b64encode(your_string)

更新:根据Matthew评论更改。


更复杂的是,不保证只删除无效字符就能获得有效的文件名。由于不同文件名上允许的字符不同,保守的方法可能最终会将有效的名称转换为无效的名称。您可能需要为以下情况添加特殊处理:

  • 字符串都是无效字符(留下一个空字符串)

  • 最后你会得到一个有特殊含义的字符串,例如"."或".."

  • 在Windows上,某些设备名称是保留的。例如,不能创建名为"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

您可以通过在文件名前面加上一些字符串来解决这些问题,这些字符串永远不会导致这些情况之一,并去掉无效字符。


Github上有一个很好的项目,叫做python slugify:

安装:

1
pip install python-slugify

然后使用:

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


这是我最终使用的解决方案:

1
2
3
4
5
6
7
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)

normalize调用将重音字符替换为非重音等效字符,这比简单地将它们剥离要好。之后,所有不允许的字符都将被删除。

我的解决方案并没有预先附加一个已知的字符串来避免可能的不允许的文件名,因为我知道给定特定的文件名格式,它们不会出现。一个更通用的解决方案需要这样做。


正如S.Lott所回答的,您可以查看django框架,了解如何将字符串转换为有效的文件名。

最新和更新的版本在utils/text.py中找到,并定义了"get-validu filename",如下所示:

1
2
3
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)


请记住,在UNIX系统上除了

  • 它不能包含
  • 它可能不包含/

其他一切都是公平的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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
even\ multiline
haha
\033[31m\ red\ \033[0m
evil
$ perl -e 'for my $i ( glob(q{./*even*}) ){ print $i; } '
./
even multiline
haha
 red
evil

是的,我只是将ANSI颜色代码存储在一个文件名中,并让它们生效。

为了娱乐,把一个bel字符放在一个目录名中,并观看CD进入该目录后的乐趣;)


您可以使用re.sub()方法替换任何非"filelike"的内容。但实际上,每个字符都是有效的;所以没有预先构建的函数(我相信)来完成它。

1
2
3
4
import re

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

将导致文件句柄指向/tmp/filename.txt。


在一行中:

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

您还可以将"u"字符放在更便于阅读的位置(例如,在替换斜杠的情况下)


1
2
3
4
5
6
7
8
>>> 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"等)。


为什么不直接用try/except包装"osopen",让底层操作系统整理文件是否有效?

这看起来工作要少得多,而且无论您使用哪个操作系统都是有效的。


尽管你必须小心。如果你只看拉丁语的话,在你的引言中并没有清楚地说出来。如果只使用ASCII字符对某些单词进行清除,则这些单词可能变得毫无意义,也可能变成另一种意义。

假设你有"森林诗",你的卫生处理可能会给"波西堡"(强+无意义的东西)

更糟的是,如果你必须处理汉字。

"下北_"你的系统可能最终会做"—",这注定会在一段时间后失败,而且没有什么帮助。因此,如果您只处理文件,我建议您要么称它们为您控制的通用链,要么保持字符不变。对于uris,大致相同。


其他注释尚未解决的另一个问题是空字符串,它显然不是有效的文件名。您还可以在剥离太多字符时得到一个空字符串。

对于Windows保留的文件名和点问题,最安全的答案是"如何从任意用户输入中规范有效的文件名?""是"甚至不用费心去尝试":如果你能找到其他方法来避免它(例如,使用数据库中的整数主键作为文件名),那么就这样做。

如果必须,并且确实需要允许空格和"."作为文件扩展名的一部分,请尝试如下操作:

1
2
3
4
5
6
7
8
9
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

即使这样也不能保证正确,特别是在意外的OSS上?-?例如,RISC OS讨厌空格,并使用"."作为目录分隔符。


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

'/hello/world'->'hello world'

'/helloworld'/->'helloworld'

一般来说,这不是你想要的,比如你正在为每个链接保存HTML,你将为不同的网页覆盖HTML。

我会腌制口述,比如:

1
2
3
4
5
{'helloworld':
    (
    {'/hello/world': 'helloworld', '/helloworld/': 'helloworld1'},
    2)
    }

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

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


我喜欢这里的python slagify方法,但它也在剥离点,这是不需要的。所以我优化了它,以将一个干净的文件名上传到S3,方法如下:

1
pip install python-slugify

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
s = 'Very / Unsafe / file
name h?h?

 .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

输出:

1
2
>>> clean_filename
'very-unsafe-file-name-haha.txt'

这是如此的故障保护,它与不带扩展名的文件名一起工作,甚至只对不安全的字符文件名工作(这里的结果是none)。


不完全是OP要求的,但这是我使用的,因为我需要独特和可逆的转换:

1
2
3
4
# 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+-_ .'))

结果是"有点"可读的,至少从系统管理的角度来看是这样的。


我知道有很多答案,但它们大多依赖于正则表达式或外部模块,所以我想给出自己的答案。纯python函数,不需要外部模块,不使用正则表达式。我的方法不是清除无效字符,而是只允许有效字符。

1
2
3
4
5
6
7
8
9
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字符的问题。

此函数用于测试单个文件名的有效性,因此它将用_u替换路径分隔符,因为它们是无效字符。如果您想添加它,修改if以包含OS路径分隔符是很简单的。


更新

在这个6年前的答案中,所有的链接都是无法修复的。

另外,我也不会再这样做了,只需base64编码或删除不安全的字符。python 3示例:

1
2
3
4
5
import re
t = re.compile("[a-zA-Z0-9.,_-]")
unsafe ="abc?é?????˙???√?μ??∫?"
safe = [ch for ch in unsafe if t.match(ch)]
# => 'abc'

使用base64可以进行编码和解码,以便再次检索原始文件名。

但根据用例的不同,您最好生成一个随机文件名并将元数据存储在单独的文件或数据库中。

1
2
3
4
5
6
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项目包含一个这样做的python模块。

它不是完全强大的,看这篇文章和这个回复。

因此,如前所述:如果可读性不重要,那么base64编码可能是一个更好的主意。

  • 文档https://svn.origo.ethz.ch/bobcat/src-doc/safefilename-module.html
  • 来源:https://svn.origo.ethz.ch/bobcat/trunk/src/bobcatlib/safefilename.py


我敢肯定这不是一个很好的答案,因为它修改了字符串,所以它是循环的,但似乎可以正常工作:

1
2
3
4
5
6
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, '')