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


298

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

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

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


17
这不应该内置到os.path模块中吗?
endlith 2009年

2
也许,尽管她的用例将需要一条在所有平台上安全的路径,而不仅仅是当前平台,这是os.path并非旨在处理的问题。
javawizard 2013年

2
为了扩展上面的评论:当前的设计os.path实际上根据os加载了一个不同的库(请参阅文档中的第二个注释)。因此,如果在os.path其中实现了引号功能,则只能在POSIX系统上运行时为POSIX-safety或在Windows上运行时为Windows-safety引用字符串。结果文件名在Windows和POSIX上都不一定有效,这就是问题的要求。
dshepherd'Mar

Answers:


164

您可以查看Django框架,了解它们如何从任意文本创建“子弹”。slug是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] +','-',value))
Joseph Turian 2010年

1
谢谢-我可能会丢失一些东西,但是我得到了:“ normalize()参数2必须是unicode,而不是str”
Alex Cook

“ normalize()参数2”。意思是value。如果该值必须是Unicode,则必须确保它实际上是Unicode。要么。如果您的实际值实际上是ASCII字符串,则可能要省略unicode规范化。
S.Lott 2012年

8
如果没有人注意到这种方法的积极方面是它不仅删除非字母字符,而是尝试首先找到好的替代品(通过NFKD归一化),所以é变成e,上标1变成a正常1,等等。谢谢
Michael Scott Cuthbert

48
slugify功能已移至django / utils / text.py,并且该文件还包含一个get_valid_filename功能。
DenilsonSáMaia

104

如果对文件格式或非法非法字符(例如“ ..”)的组合没有限制,则这种白名单方法(即仅允许有效字符中存在的字符)将起作用,例如,您说的是将允许一个名为“ .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)不会受伤。如果应用于allchar,则速度提高了1.5倍。
jfs

2
警告:这会将两个不同的字符串映射到相同的字符串>>>导入字符串>>> valid_chars =“- 。()%s%s”%(string.ascii_letters,string.digits)>>> valid_chars'-。() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'>>> filename =“ a.com/hello/world” >>>''.join(如果c在valid_chars中为c,则为c表示文件名)'a.comhelloworld'>>> filename =“ a.com/helloworld “ >>>” .join(如果c在有效字符中,则c为文件名中的c)'a.comhelloworld'>>>
罗伯特·金

3
更不用说"CON"在Windows上命名文件会给您带来麻烦……
Nathan Osman

2
稍作重新排列就可以轻松指定替代字符。首先是原始功能:''.join(c表示有效字符中的c,否则为,则为文件名中的c)或为每个无效字符替换一个字符或字符串:''.join(c表示有效字符中的c,否则为'。'。 c in filename)
PeterVermont 2014年

101

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

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

3
请注意,您可以省略方括号。在这种情况下,将生成器表达式传递给join,从而节省了创建否则未使用的列表的步骤。
Oben Sonne

31
+1喜欢这个。我已经做过一些小修改:“” .join([x,如果x.isalnum()否则为“ _”,表示s中的x]])-将产生无效项为_的结果,就像它们是空白的一样。也许是别人。
Eddie Parker

12
这个解决方案很棒!不过,我做了一些修改:filename = "".join(i for i in s if i not in "\/:*?<>|")
Alex Krycek 2013年

1
不幸的是,它甚至不允许有空格和点,但我喜欢这个主意。
tiktak

9
@tiktak :(也)允许留空格,点和下划线–您可以"".join( x for x in s if (x.isalnum() or x in "._- "))
吃力

95

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

更新:根据马修评论更改。


1
显然,如果是这样,这是最好的答案。
user32141

60
警告!默认情况下,base64编码包含“ /”字符作为有效输出,在许多系统上的文件名中无效。而是使用base64.urlsafe_b64encode(your_string)
马修

15
实际上,即使只是出于调试目的,人类可读性几乎总是一个因素。
static_rtti

5
在Python 3中your_string,必须是字节数组或此数组的结果encode('ascii')才能起作用。
Noumenon

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 2015年

40

只是为了使事情更加复杂,不能保证仅通过删除无效字符就可以获得有效的文件名。由于不同文件名上允许的字符不同,因此保守的方法可能最终将有效名称变成无效名称。对于以下情况,您可能需要添加特殊处理:

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

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

  • 在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

您可以通过在文件名前添加一些字符串(它们永远不会导致这些情况之一)并去除无效字符来解决这些问题。


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

就像S.Lott回答的一样,您可以查看Django框架,了解它们如何将字符串转换为有效的文件名。

在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上懒惰的人:django.utils.text import get_valid_filename
播音员

2
如果您不熟悉正则表达式,请re.sub(r'(?u)[^-\w.]', '', s)删除所有不是字母,不是数字(0-9),不是下划线('_'),不是破折号('-')和句点('。'的字符。 )。这里的“字母”包括所有unicode字母,例如汉语。
Cowlinator

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()作为唯一前缀
2009年

6
骆驼案.. ahh
痴呆的刺猬

可以对其进行编辑/更新以与Python 3.6一起使用吗?
Wavesailor '18

13

请记住,除了Unix系统上的文件名外,实际上没有任何限制。

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

其他一切都是公平的游戏。

$ touch”
>甚至多行
>哈哈
> ^ [[31m红色^ [[0m
>邪恶”
$ ls -la 
-rw-r--r-- Nov 11 23:39?even multiline?haha ?? [31m red?[0m?evil
$ ls -lab
-rw-r--r-- 11月17日23:39 \ neven \ multiline \ nhaha \ n \ 033 [31m \ red \ \ 033 [0m \ nevil
$ perl -e'for my $ i(glob(q {./* even *})){print $ i; }'
./
甚至多行
哈哈
 红色 
邪恶

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

为了娱乐,请将BEL字符放在目录名称中,并观看将CD放入其中时的乐趣;)


OP指出“文件名必须在多个操作系统上有效”
cowlinator

1
@cowlinator在发布我的答案10小时后添加了说明:)检查OP的编辑日志。
肯特·弗雷德里克

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)
2010年

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,这是迄今为止最有效的方法。对于特殊的文件名/空文件,简单的前提条件检查就足够了,并且对于无关紧要的时间段也很简单。
Christian Witts 2009年

1
尽管翻译比正则表达式更有效,但是如果您实际上尝试打开文件,那么很可能会浪费时间,这无疑是您打算要做的。因此,我更喜欢多于乱七八糟上方的更易读的regexp溶液
nosatalian

我也担心黑名单。当然,这是一个基于白名单的黑名单,但仍然如此。似乎不太安全。您怎么知道“ allchars”实际上是完整的?
isaaclw

@isaaclw:“。translate()”接受256个字符的字符串作为转换表(字节到字节的转换)。'.maketrans()'创建这样的字符串。所有值均已涵盖;这是一种纯白名单方法
jfs

文件名“。”呢?(一个点)。这在Unixes上不起作用,因为当前目录正在使用该名称。
FinnÅrupNielsen

6

虽然您必须要小心。如果您只看常规语言,则在介绍中并没有明确指出。如果仅使用ascii字符对某些单词进行消毒,则某些单词可能变得毫无意义,或变得另一种含义。

假设您有“Forêtpoésie”(森林诗歌),您的卫生处理可能会给您带来“ fort-posie”(强+毫无意义的东西)

更糟糕的是,如果您不得不处理汉字。

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


6

为什么不只用try / except包裹“ osopen”,然后让底层的OS整理出文件是否有效?

这似乎工作量少得多,并且无论使用哪种操作系统,这都是有效的。


5
它是否有效名称?我的意思是,如果操作系统不满意,那么您仍然需要做一些事情,对吗?
jeromej 2014年

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

即使是这种情况也无法保证,尤其是在意外的OS上,例如RISC OS讨厌空格并使用'。作为目录分隔符。


4

我喜欢这里的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这里)。


1
我喜欢这样,不要重新发明轮子,如果不需要不需要导入整个Django框架,如果以后不想维护它就不要直接粘贴代码,并尝试生成字符串将相似的字母与安全的字母进行匹配,因此新字符串更易于阅读。
vicenteherrera

1
要使用下划线代替破折号:name = slugify(s,eparator ='_')
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)

您能否详细解释您的答案?
宁静,

Sophie Gage接受了相同的答案。但它已被修改以在python 3.6上工作
Jean-Robin Tremblay

2

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

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

不完全是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+-_ .'))

至少从sysadmin的角度来看,结果是“有点”可读的。


为此的包装,文件名中没有空格: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项目包含一个执行此操作的python模块。

它并不完全健壮,请参阅此帖子和此回复

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


所有链接都消失了。老兄,做点什么。
和平编码员2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.