我有一个字符串,想要将其用作文件名。因此,我希望使用Python删除所有在文件名中不允许的字符。
我宁愿保守一些,所以让我们只保留字母、数字和一小组其他字符,如"_-.() "
。最优雅的解决方案是什么?
文件名需要在多个操作系统(Windows、Linux和Mac OS)上有效——这是我音乐库中的MP3文件,歌曲标题是文件名,并在3台机器之间共享和备份。
你可以查看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 无关,而是转义。
unicode()
函数已被取消,可以使用字符串直接操作。 - Joseph Turianslugify
函数已被移至django/utils/text.py,该文件还包含一个get_valid_filename
函数。我会尽力使内容通俗易懂,但不改变原意。 - Denilson Sá Maiaslugify
函数(Python 3版本)可在https://github.com/django/django/blob/master/django/utils/text.py中找到。 - am70>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
filename = "".join(i for i in s if i not in "\/:*?<>|")
。 - Alex Krycek"".join( x for x in s if (x.isalnum() or x in "._- "))
。这将从字符串s
中选出所有字母数字字符和特定的符号(空格、句点和下划线),并将它们连接成一个新字符串。 - hardmooth使用字符串作为文件名的原因是什么?如果人类可读性不是一个因素,我会选择使用 base64 模块生成文件系统安全字符串。它不会可读,但你不必处理碰撞并且它是可逆的。
import base64
file_name_string = base64.urlsafe_b64encode(your_string)
更新: 根据Matthew的评论进行了修改。
>>> 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'
valid_chars = frozenset(valid_chars)
不会对原意造成影响。如果应用到allchars
上,可以提高1.5倍的速度。 - jfs- Rusty Robimport 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'
在 Github 上有一个不错的项目叫做 python-slugify:
安装:
pip install python-slugify
然后使用:
>>> from slugify import slugify
>>> txt = "This\ is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
test.txt
变成了 test-txt
,这有点太多了。 - therealmarv.
和 -
的替换。另请参阅 @therealmarv 的 下面的答案 中的评论。 - mhuckautils/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
django.utils.text
中有一个get_valid_filename
函数,可以用来获取有效的文件名。 - theannouncerre.sub(r'(?u)[^-\w.]', '', s)
将删除所有不是字母、数字(0-9)、下划线('_')、破折号('-')和句点('.')的字符。这里的“字母”包括所有Unicode字母,如汉语。 - cowlinatorre.sub(r'(?u)[^-\w.]', '_', s)
。 - spiralmoon为了让事情更加复杂,仅仅通过删除无效字符并不能保证你会得到一个有效的文件名。由于不同的文件名允许的字符不同,保守的做法可能会将有效的名称变成无效的。你可能需要特殊处理以下情况:
字符串全部由无效字符组成(这样会得到一个空字符串)
你最终得到的字符串具有特殊含义,例如“.”或“..”
在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
你可以通过在文件名前添加一个永远不可能导致以上情况的字符串,并去除无效字符来解决这些问题。
简单概括:
valid_file_name = re.sub('[^\w_.)( -]', '', any_string)
你还可以加入下划线字符 '_',使文本更易读(例如在替换斜杠时)
这是我最终使用的解决方案:
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函数将带重音符号的字符替换为没有重音的对应字符,这比简单地删除它们要好。然后,所有不允许的字符都被移除。
我的解决方案没有在文件名前缀上添加已知字符串以避免可能的非法文件名,因为我知道根据我的特定文件名格式,它们不可能出现。更通用的解决方案需要这样做。
请注意,在Unix系统上,实际上没有文件名限制,除了:
其他任何字符都可以使用。
$ 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进入该目录时会发生的有趣事件;)
os.path
目前的设计实际上根据操作系统加载不同的库(请参见 文档 中第二个注释)。因此,如果在os.path
中实现转义功能,它只能在 POSIX 系统上运行时为字符串进行 POSIX 安全的转义,在 Windows 上运行时为字符串进行 Windows 安全的转义。生成的文件名未必能够在 Windows 和 POSIX 系统之间通用,而这正是问题所要求的。 - dshepherdpath
函数非常容易。例如,在 Unix 上,使用import ntpath; ntpath.abspath("a.txt")
可以获得(虚拟)Windows 文件系统上文件的绝对路径。或者在 POSIX 系统(Linux、Mac OS)上使用posixpath
。 - cowlinator