我有一个字符串,我想用作文件名,所以我想删除文件名中不允许使用Python的所有字符.
我宁愿比其他方面更严格,所以让我说我只想保留字母,数字和一小部分其他字符"_-.() "
.什么是最优雅的解决方案?
文件名需要在多个操作系统(Windows,Linux和Mac OS)上有效 - 它是我的库中的MP3文件,歌曲标题为文件名,并在3台机器之间共享和备份.
您可以查看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))
还有更多,但是我把它排除在外,因为它没有解决挫折问题,而是逃避了.
如果对文件的格式或非法的有效字符组合(例如"..")没有限制,这种白名单方法(即仅允许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'
您可以将列表推导与字符串方法一起使用.
>>> s 'foo-bar#baz?qux@127/\\9]' >>> "".join(x for x in s if x.isalnum()) 'foobarbazqux1279'
将字符串用作文件名的原因是什么?如果人类可读性不是一个因素,我会使用base64模块,它可以生成文件系统安全字符串.它不可读,但你不必处理碰撞,它是可逆的.
import base64 file_name_string = base64.urlsafe_b64encode(your_string)
更新:根据马修评论更改.
只是为了使事情进一步复杂化,您不能保证仅通过删除无效字符就能获得有效的文件名.由于允许的字符在不同的文件名上有所不同,因此保守的方法最终可能会将有效名称转换为无效的名称.您可能希望为以下情况添加特殊处理:
该字符串是所有无效字符(留下空字符串)
你最终会得到一个具有特殊含义的字符串,例如"." 要么 ".."
在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的好项目:
安装:
pip install python-slugify
然后使用:
>>> from slugify import slugify >>> txt = "This\ is/ a%#$ test ---" >>> slugify(txt) 'this-is-a-test'
这是我最终使用的解决方案:
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调用用非重音等效替换重音字符,这比简单地剥离它们要好.之后,所有不允许的字符都被删除.
我的解决方案没有预先添加已知的字符串以避免可能的不允许的文件名,因为我知道在给定我的特定文件名格式时它们不会发生.更通用的解决方案需要这样做.
就像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)
(见https://github.com/django/django/blob/master/django/utils/text.py)
请记住,除了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刻录后的乐趣;)
在一行中:
valid_file_name = re.sub('[^\w_.)( -]', '', any_string)
你也可以加上'_'字符使其更具可读性(例如,如果更换斜杠)
您可以使用re.sub()方法替换不是"filelike"的任何内容.但实际上,每个角色都是有效的; 因此,没有预先构建的功能(我相信),以完成它.
import re str = "File!name?.txt" f = open(os.path.join("/tmp", re.sub('[^-a-zA-Z0-9_.() ]+', '', str))
会导致文件句柄为/tmp/filename.txt.
>>> 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字符清理它们,某些单词可能会变得毫无意义或其他意义.
想象你有"forêtpoésie"(森林诗歌),你的消毒可能会给"堡垒"(强烈+无意义的东西)
如果你不得不处理汉字,那就更糟了.
"下北沢"你的系统可能最终会做"---",注定会在一段时间后失败并且不是很有帮助.因此,如果您只处理文件,我会鼓励将它们称为您控制的通用链或保持字符不变.对于URI,大致相同.
其他评论尚未解决的另一个问题是空字符串,这显然不是有效的文件名.您也可以通过剥离太多字符来结束空字符串.
对于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
即使这样也无法保证,特别是在意外的操作系统上 - 例如RISC OS讨厌空间和使用'.' 作为目录分隔符.