我有一个字符串,我想用它作为文件名,所以我想用Python删除文件名中不允许的所有字符。
我宁愿严格一点,所以假设我想只保留字母、数字和一小组其他字符,如“_-.()”。”。最优雅的解决方案是什么?
文件名需要在多个操作系统(Windows, Linux和Mac OS)上有效——它是我库中的一个MP3文件,以歌曲标题为文件名,并在3台机器之间共享和备份。
我有一个字符串,我想用它作为文件名,所以我想用Python删除文件名中不允许的所有字符。
我宁愿严格一点,所以假设我想只保留字母、数字和一小组其他字符,如“_-.()”。”。最优雅的解决方案是什么?
文件名需要在多个操作系统(Windows, Linux和Mac OS)上有效——它是我库中的一个MP3文件,以歌曲标题为文件名,并在3台机器之间共享和备份。
当前回答
就像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)
其他回答
使用字符串作为文件名的原因是什么?如果人类的可读性不是一个因素,我会使用base64模块,它可以产生文件系统安全字符串。它是不可读的,但你不需要处理碰撞,而且它是可逆的。
import base64
file_name_string = base64.urlsafe_b64encode(your_string)
更新:根据Matthew的评论修改。
当遇到同样的问题时,我使用python-slugify。
Shoham也建议使用这种方法,但正如therealmarv指出的那样,默认情况下python-slugify也会转换圆点。
可以通过在regex_pattern参数中包含点来否决这种行为。
> filename = "This is a väryì' Strange File-Nömé.jpeg"
> pattern = re.compile(r'[^-a-zA-Z0-9.]+')
> slugify(filename,regex_pattern=pattern)
'this-is-a-varyi-strange-file-nome.jpeg'
方法复制的正则表达式模式
ALLOWED_CHARS_PATTERN_WITH_UPPERCASE
python-slugify包的slugify.py文件中的全局变量,并扩展为“。”
请记住,像.()这样的特殊字符必须用\转义。
如果您想保留大写字母,请使用小写=False参数。
> filename = "This is a väryì' Strange File-Nömé.jpeg"
> pattern = re.compile(r'[^-a-zA-Z0-9.]+')
> slugify(filename,regex_pattern=pattern, lowercase=False)
'This-is-a-varyi-Strange-File-Nome.jpeg'
这是使用Python 3.8.4和Python -slugify 4.0.1实现的
其他注释还没有解决的另一个问题是空字符串,这显然不是一个有效的文件名。你也可能因为剥离太多字符而得到一个空字符串。
对于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操作系统讨厌空格并使用'。'作为目录分隔符。
给,这应该涵盖了所有的基础。它为您处理所有类型的问题,包括(但不限于)字符替换。
适用于Windows、*nix和几乎所有其他文件系统。只允许打印字符。
def txt2filename(txt, chr_set='normal'):
"""Converts txt to a valid Windows/*nix filename with printable characters only.
args:
txt: The str to convert.
chr_set: 'normal', 'universal', or 'inclusive'.
'universal': ' -.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
'normal': Every printable character exept those disallowed on Windows/*nix.
'extended': All 'normal' characters plus the extended character ASCII codes 128-255
"""
FILLER = '-'
# Step 1: Remove excluded characters.
if chr_set == 'universal':
# Lookups in a set are O(n) vs O(n * x) for a str.
printables = set(' -.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
else:
if chr_set == 'normal':
max_chr = 127
elif chr_set == 'extended':
max_chr = 256
else:
raise ValueError(f'The chr_set argument may be normal, extended or universal; not {chr_set=}')
EXCLUDED_CHRS = set(r'<>:"/\|?*') # Illegal characters in Windows filenames.
EXCLUDED_CHRS.update(chr(127)) # DEL (non-printable).
printables = set(chr(x)
for x in range(32, max_chr)
if chr(x) not in EXCLUDED_CHRS)
result = ''.join(x if x in printables else FILLER # Allow printable characters only.
for x in txt)
# Step 2: Device names, '.', and '..' are invalid filenames in Windows.
DEVICE_NAMES = 'CON,PRN,AUX,NUL,COM1,COM2,COM3,COM4,' \
'COM5,COM6,COM7,COM8,COM9,LPT1,LPT2,' \
'LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,' \
'CONIN$,CONOUT$,..,.'.split() # This list is an O(n) operation.
if result in DEVICE_NAMES:
result = f'-{result}-'
# Step 3: Maximum length of filename is 255 bytes in Windows and Linux (other *nix flavors may allow longer names).
result = result[:255]
# Step 4: Windows does not allow filenames to end with '.' or ' ' or begin with ' '.
result = re.sub(r'^[. ]', FILLER, result)
result = re.sub(r' $', FILLER, result)
return result
这个解决方案不需要外部库。它也替代了不可打印的文件名,因为它们并不总是容易处理。
为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)