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

438

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

我宁愿保守一些,所以让我们只保留字母、数字和一小组其他字符,如"_-.() "。最优雅的解决方案是什么?

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


34
这不应该集成到 os.path 模块中吗? - endolith
4
也许,尽管她的使用情况需要一个在所有平台上都安全的单一路径,而不仅仅是当前的路径,这是os.path无法处理的。 - javawizard
6
进一步解释上面的评论:os.path 目前的设计实际上根据操作系统加载不同的库(请参见 文档 中第二个注释)。因此,如果在 os.path 中实现转义功能,它只能在 POSIX 系统上运行时为字符串进行 POSIX 安全的转义,在 Windows 上运行时为字符串进行 Windows 安全的转义。生成的文件名未必能够在 Windows 和 POSIX 系统之间通用,而这正是问题所要求的。 - dshepherd
对于不同的操作系统,使用 path 函数非常容易。例如,在 Unix 上,使用 import ntpath; ntpath.abspath("a.txt") 可以获得(虚拟)Windows 文件系统上文件的绝对路径。或者在 POSIX 系统(Linux、Mac OS)上使用 posixpath - cowlinator
27个回答

285

你可以查看Django框架(但要考虑他们的许可证!)来了解他们如何从任意文本创建“slug”。Slug是URL和文件名友好的。

Django文本工具定义了一个函数slugify(),这可能是这种事情的黄金标准。实际上,他们的代码如下。

import unicodedata
import re

def slugify(value, allow_unicode=False):
    """
    Taken from https://github.com/django/django/blob/master/django/utils/text.py
    Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
    dashes to single dashes. Remove characters that aren't alphanumerics,
    underscores, or hyphens. Convert to lowercase. Also strip leading and
    trailing whitespace, dashes, and underscores.
    """
    value = str(value)
    if allow_unicode:
        value = unicodedata.normalize('NFKC', value)
    else:
        value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
    value = re.sub(r'[^\w\s-]', '', value.lower())
    return re.sub(r'[-\s]+', '-', value).strip('-_')

还有旧版本:

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

还有其他内容,但我省略了,因为它与 slugification 无关,而是转义。


12
最后一行应为:value = unicode(re.sub('[-\s]+', '-', value))。注意:在Python 3中,unicode()函数已被取消,可以使用字符串直接操作。 - Joseph Turian
1
谢谢 - 我可能漏掉了什么,但我得到的是:"normalize() argument 2 must be unicode, not str" - Alex Cook
11
如果有人没有注意到这种方法的积极面,那么需要说明的是,它不仅仅是去除非字母字符,而是首先尝试通过NFKD规范化找到良好的替代品,所以é变成了e,上标1变成了普通1等等。谢谢。 - Michael Scott Asato Cuthbert
67
slugify函数已被移至django/utils/text.py,该文件还包含一个get_valid_filename函数。我会尽力使内容通俗易懂,但不改变原意。 - Denilson Sá Maia
1
slugify函数(Python 3版本)可在https://github.com/django/django/blob/master/django/utils/text.py中找到。 - am70
显示剩余5条评论

173
你可以将列表推导式与字符串方法一起使用。
>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'

5
请注意,您可以省略方括号。在这种情况下,传递给join的是一个生成器表达式,这将节省创建一个否则没有用处的列表的步骤。 - Oben Sonne
56
我很喜欢这条信息。我稍作修改:"".join([x if x.isalnum() else "_" for x in s]),会产生一个结果,其中无效的项目会变成_,就像它们被清空一样。也许这对其他人有帮助。 - Eddie Parker
20
这个解决方案很棒!不过我做了一点修改:filename = "".join(i for i in s if i not in "\/:*?<>|") - Alex Krycek
4
很不幸,它甚至不允许空格和点,但我喜欢这个想法。 - tiktak
22
为了允许空格、句点和下划线,你可以使用以下代码:"".join( x for x in s if (x.isalnum() or x in "._- "))。这将从字符串s中选出所有字母数字字符和特定的符号(空格、句点和下划线),并将它们连接成一个新字符串。 - hardmooth
显示剩余2条评论

115

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)

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


79
警告!默认情况下,base64编码会将斜杠“/”字符作为有效输出包含在内,但在许多系统中,它不适用于文件名。建议使用base64.urlsafe_b64encode(your_string)。 - Matthew
3
这应该被视为具有任何内部用户命名内容的Web服务器的理想答案。即使管理员需要查找某些内容,您也可以轻松编写脚本将所有查询转换为相同的形式。 - codetaku
25
实际上,即使只是为了调试目的,人类可读性几乎总是一个因素。 - static_rtti
6
在Python 3中,为使此代码起作用,“your_string”需要是一个字节数组或者是调用“encode('ascii')”后的结果。 - Noumenon
5
`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
正如@Nouemon所说,你需要将字符串转换为字节数组,例如在这个答案中所示:https://dev59.com/C2sz5IYBdhLWcg3w5MI0 - rcriii

113
这个白名单方法(即,仅允许在valid_chars中存在的字符)将在文件格式或组合的限制没有非法字符(如“..”)的情况下起作用。例如,您所说的将允许命名为“ .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'

9
valid_chars = frozenset(valid_chars)不会对原意造成影响。如果应用到allchars上,可以提高1.5倍的速度。 - jfs
2
警告:这将两个不同的字符串映射到相同的字符串
import string valid_chars = "-.() %s%s" % (string.ascii_letters, string.digits) valid_chars '-.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' filename = "a.com/hello/world" ''.join(c for c in filename if c in valid_chars) 'a.comhelloworld' filename = "a.com/helloworld" ''.join(c for c in filename if c in valid_chars) 'a.comhelloworld'
- Rusty Rob
3
更不必提在Windows上命名文件为“CON”会给您带来麻烦... - Nathan Osman
2
稍作调整即可轻松指定替换字符。首先是原始功能: ''.join(c if c in valid_chars else '' for c in filename) 或者对于每个无效字符都使用替换字符或字符串: ''.join(c if c in valid_chars else '.' for c in filename) - PeterVermont
小细节,".txt" 是 Windows 上的有效文件名,尽管我怀疑有更多的 ".gitignore" 文件存在。 - Pat C

55

在 Github 上有一个不错的项目叫做 python-slugify:

安装:

pip install python-slugify

然后使用:

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

3
我喜欢这个库,但它并没有我想象中那么好。初始测试没问题,但它也会转换点号。所以 test.txt 变成了 test-txt,这有点太多了。 - therealmarv
Slugify 看起来正在积极维护,当前版本(截至2021年11月3日)有许多选项,其中一些可以用于控制 .- 的替换。另请参阅 @therealmarv 的 下面的答案 中的评论。 - mhucka

50
就像S.Lott所回答的那样,您可以查看Django Framework来了解它们如何将字符串转换为有效的文件名。
最新和更新的版本位于utils/text.py中,并定义了get_valid_filename,其代码如下:
def get_valid_filename(name):
    s = str(name).strip().replace(" ", "_")
    s = re.sub(r"(?u)[^-\w.]", "", s)
    if s in {"", ".", ".."}:
        raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
    return s

(请参见https://github.com/django/django/blob/master/django/utils/text.py

8
对于已经在使用Django的懒惰者:django.utils.text中有一个get_valid_filename函数,可以用来获取有效的文件名。 - theannouncer
3
如果您不熟悉正则表达式,re.sub(r'(?u)[^-\w.]', '', s)将删除所有不是字母、数字(0-9)、下划线('_')、破折号('-')和句点('.')的字符。这里的“字母”包括所有Unicode字母,如汉语。 - cowlinator
5
请注意文件名长度限制:文件名长度限制为255个字符(或者根据文件系统不同而有所变化,最多32个字符)。 - Matthias Winkelmann
为了更好的可读性,返回re.sub(r'(?u)[^-\w.]', '_', s) - spiralmoon

47

为了让事情更加复杂,仅仅通过删除无效字符并不能保证你会得到一个有效的文件名。由于不同的文件名允许的字符不同,保守的做法可能会将有效的名称变成无效的。你可能需要特殊处理以下情况:

  • 字符串全部由无效字符组成(这样会得到一个空字符串)

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

  • 在Windows操作系统中,某些设备名称是被保留的。比如,你不能创建一个名为“nul”、“nul.txt”(实际上是任何以“nul.”开头的名称都不行)。这些保留名称包括:

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

你可以通过在文件名前添加一个永远不可能导致以上情况的字符串,并去除无效字符来解决这些问题。


20

简单概括:

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

你还可以加入下划线字符 '_',使文本更易读(例如在替换斜杠时)


20

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

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函数将带重音符号的字符替换为没有重音的对应字符,这比简单地删除它们要好。然后,所有不允许的字符都被移除。

我的解决方案没有在文件名前缀上添加已知字符串以避免可能的非法文件名,因为我知道根据我的特定文件名格式,它们不可能出现。更通用的解决方案需要这样做。


1
你可以使用uuid.uuid4()作为你的唯一前缀。 - slf
11
驼峰命名法(camel case)..啊 - demented hedgehog
这个能否被编辑/更新以适用于Python 3.6? - Wavesailor

16

请注意,在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进入该目录时会发生的有趣事件;)


该OP指出:“文件名需要在多个操作系统上有效”。 - cowlinator
1
@cowlinator,那个澄清是在我回答发布后10小时才添加的 :) 请检查原始帖子的编辑日志。 - Kent Fredric
在Ubuntu上,\0看起来没问题。 - Harley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接