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

Sop*_*age 269 python filenames sanitize slug

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

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

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

S.L*_*ott 150

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

他们slugify()(在第183行附近)定义了一个函数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))
Run Code Online (Sandbox Code Playgroud)

还有更多,但是我把它排除在外,因为它没有解决挫折问题,而是逃避了.

  • `slugify`函数已移至[django/utils/text.py](http://github.com/django/django/blob/master/django/utils/text.py),该文件还包含`get_valid_filename`函数. (39认同)
  • 最后一行应该是:value = unicode(re.sub('[ - \s] +',' - ',value)) (11认同)
  • 如果有人没有注意到这种方法的积极方面是它不仅仅删除非字母字符,而是首先尝试找到好的替代品(通过NFKD标准化),所以é变为e,上标1变成了正常1等等谢谢 (8认同)

Vin*_*vic 98

如果对文件的格式或非法的有效字符组合(例如"..")没有限制,这种白名单方法(即仅允许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'
Run Code Online (Sandbox Code Playgroud)

  • `valid_chars = frozenset(valid_chars)`不会受伤.如果应用于allchars,它快1.5倍. (7认同)
  • 警告:这会将两个不同的字符串映射到相同的字符串>>>导入字符串>>> valid_chars =“ -_。()%s%s”%(string.ascii_letters,string.digits)>>> valid_chars'-_。 ()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'>>> filename =“ a.com/hello/world” >>>``.join(如果c在有效字符中,则c用于c在文件名中)'a.comhelloworld'>>> filename =“ a.com / helloworld“ >>>''.join(c表示文件名中的c,如果c表示有效字符)'a.comhelloworld'>>> (2认同)
  • 更不用说在Windows上命名文件“ CON”会给您带来麻烦... (2认同)
  • 轻微的重新排列使得指定替代字符变得简单.首先是原始功能:''.join(c,如果c在valid_chars中为''表示文件名中的c)或每个无效字符的替换字符或字符串:''.join(c如果c在valid_chars中,则为'.'为文件名中的c) (2认同)

小智 95

您可以将列表推导与字符串方法一起使用.

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

  • +1喜欢这个.稍微修改我已经完成:"".join([x if x.isalnum()else"_"for x in s]) - 会产生无效项为_的结果,就像它们被清空一样.也许是别人的. (29认同)
  • 这个解决方案很棒!我做了一个小小的修改:`filename ="".join(如果我不在"\ /:*?<> |"中,我在s中为i) (12认同)
  • @tiktak:to(也)允许空格,点和下划线你可以去".".join(x代表x中的x if(x.isalnum()或x in"._-")) (9认同)
  • 请注意,您可以省略方括号.在这种情况下,将[生成器表达式](http://docs.python.org/glossary.html#term-generator-expression)传递给join,这将保存创建其他未使用列表的步骤. (3认同)
  • x.isalnum()做同样的事情 (3认同)
  • 不幸的是,它甚至不允许空格和点,但我喜欢这个想法。 (3认同)

Iga*_*ban 92

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

import base64
file_name_string = base64.urlsafe_b64encode(your_string)
Run Code Online (Sandbox Code Playgroud)

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

  • 警告!默认情况下,base64编码包含"/"字符作为有效输出,在许多系统的文件名中无效.而是使用base64.urlsafe_b64encode(your_string) (58认同)
  • 实际上,人类可读性几乎总是一个因素,即使仅用于调试目的. (14认同)
  • `def url2filename(url):url = url.encode('UTF-8')返回base64.urlsafe_b64encode(url).decode('UTF-8')def filename2url(f):return base64.urlsafe_b64decode(f).decode ( 'UTF-8')` (4认同)
  • 对于具有任何内部用户命名内容的网络服务器来说,这“绝对”应该被视为理想的答案。即使管理员需要去找东西,您也可以轻松编写脚本将所有查询转换为相同的形式。 (3认同)
  • 在Python 3中,`your_string`需要是一个字节数组或者`encode('ascii')`的结果才能使它工作. (3认同)

Bri*_*ian 38

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

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

  • 你最终会得到一个具有特殊含义的字符串,例如"." 要么 ".."

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

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


Sho*_*ham 23

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

安装:

pip install python-slugify
Run Code Online (Sandbox Code Playgroud)

然后使用:

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

  • 我喜欢这个图书馆,但它没有我想象的那么好。初始测试正常,但它也会转换点。所以`test.txt` 得到`test-txt` 太多了。 (3认同)

Sop*_*age 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)
Run Code Online (Sandbox Code Playgroud)

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

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

  • 骆驼案啊..啊 (6认同)

cow*_*tor 18

就像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)
Run Code Online (Sandbox Code Playgroud)

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

  • 对于已经在django上的懒惰:`django.utils.text import get_valid_filename` (4认同)
  • 您可能还需要检查长度:文件名限制为255个字符(或者,您知道,32个;取决于FS) (3认同)
  • 如果你不熟悉正则表达式,`re.sub(r'(?u)[^ - \w.]','',s)`删除所有不是字母的字符,而不是数字(0-9),不是下划线('_'),不是短划线(' - '),而不是句点('.').这里的"字母"包括所有的unicode字母,例如汉语. (2认同)

Ken*_*ric 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刻录后的乐趣;)


mna*_*ach 10

在一行中:

valid_file_name = re.sub('[^\w_.)( -]', '', any_string)
Run Code Online (Sandbox Code Playgroud)

你也可以加上'_'字符使其更具可读性(例如,如果更换斜杠)


小智 7

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

import re

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

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

  • 您需要破折号在组匹配器中首先出现,因此它不会显示为范围.re.sub('[^ - a-zA-Z0-9 _.()] +','',str) (5认同)

jfs*_*jfs 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'
Run Code Online (Sandbox Code Playgroud)

它不处理空字符串,特殊文件名('nul','con'等).


小智 7

如果您不介意安装软件包,这应该很有用: 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")
Run Code Online (Sandbox Code Playgroud)

输出

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
Run Code Online (Sandbox Code Playgroud)


Jam*_*son 6

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

这似乎更少的工作,无论您使用哪种操作系统,它都是有效的.

  • 它虽然名称有效吗?我的意思是,如果操作系统不满意,那么你还需要做点什么,对吧? (5认同)

kar*_*cow 6

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

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

如果你不得不处理汉字,那就更糟了.

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


bob*_*nce 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
Run Code Online (Sandbox Code Playgroud)

即使这样也无法保证,特别是在意外的操作系统上 - 例如RISC OS讨厌空间和使用'.' 作为目录分隔符.


the*_*arv 5

我喜欢这里的 python-slugify 方法,但它也在剥离点,这是不想要的。因此,我对其进行了优化,以通过这种方式将干净的文件名上传到 s3:

pip install python-slugify
Run Code Online (Sandbox Code Playgroud)

示例代码:

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
Run Code Online (Sandbox Code Playgroud)

输出:

>>> clean_filename
'very-unsafe-file-name-haha.txt'
Run Code Online (Sandbox Code Playgroud)

这是非常安全的,它适用于没有扩展名的文件名,甚至只适用于不安全字符的文件名(结果在none这里)。

  • 要使用下划线而不是破折号:name=slugify(s,separator='_') (3认同)

Tun*_*ğlu 5

我意识到有很多答案,但它们主要依赖于正则表达式或外部模块,所以我想给出我自己的答案。纯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    
Run Code Online (Sandbox Code Playgroud)

如果您愿意,您可以在validchars变量的开头添加您自己的有效字符,例如英文字母表中不存在的国家字母。这是您可能想要也可能不想要的:一些不在 UTF-8 上运行的文件系统可能仍然存在非 ASCII 字符的问题。

此函数是为了测试单个文件名的有效性,因此它将路径分隔符替换为 _,认为它们是无效字符。如果你想添加它,修改if包含 os 路径分隔符是微不足道的。


小智 5

针对 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)
Run Code Online (Sandbox Code Playgroud)