将字符串转换为有效的文件名?
- 2025-02-08 08:47:00
- admin 原创
- 72
问题描述:
我有一个想要用作文件名的字符串,因此我想使用 Python 删除文件名中不允许的所有字符。
我宁愿严格一些,所以假设我只想保留字母、数字和一小部分其他字符,如"_-.() "
。最优雅的解决方案是什么?
文件名需要在多个操作系统(Windows、Linux 和 Mac OS)上有效 - 它是我的库中的一个 MP3 文件,以歌曲标题作为文件名,并在 3 台机器之间共享和备份。
解决方案 1:
您可以查看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'[^ws-]', '', 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('[^ws-]', '', value).strip().lower())
value = unicode(re.sub('[-s]+', '-', value))
# ...
return value
还有更多,但我把它省略了,因为它没有解决 slugification 问题,而是解决了逃避问题。
解决方案 2:
您可以将列表理解与字符串方法一起使用。
>>> s
'foo-bar#baz?qux@127/\\9]'
>>> "".join(x for x in s if x.isalnum())
'foobarbazqux1279'
解决方案 3:
使用字符串作为文件名的原因是什么?如果人类可读性不是一个因素,我会使用可以生成文件系统安全字符串的 base64 模块。它不可读,但您不必处理冲突,而且它是可逆的。
import base64
file_name_string = base64.urlsafe_b64encode(your_string)
更新:根据马修的评论进行了更改。
解决方案 4:
如果文件格式或非法有效字符组合(如“..”)没有限制,则此白名单方法(即仅允许 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'
解决方案 5:
Github 上有一个很好的项目叫做python-slugify:
安装:
pip install python-slugify
然后使用:
>>> from slugify import slugify
>>> txt = "This is/ a%#$ test ---"
>>> slugify(txt)
'this-is-a-test'
解决方案 6:
就像S.Lott回答的那样,您可以查看Django 框架了解他们如何将字符串转换为有效的文件名。
最新更新的版本位于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)
解决方案 7:
事情变得更加复杂的是,仅通过删除无效字符并不能保证获得有效的文件名。由于不同文件名允许的字符不同,保守的方法最终可能会将有效名称变成无效名称。您可能需要为以下情况添加特殊处理:
该字符串全是无效字符(留下一个空字符串)
您最终会得到一个具有特殊含义的字符串,例如“。”或“..”
在 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
您可以通过在文件名前面添加一些不会导致上述情况的字符串并删除无效字符来解决这些问题。
解决方案 8:
一行代码:
valid_file_name = re.sub(r'[^w_.)( -]', '', any_string)
您还可以添加“_”字符以使其更具可读性(例如,在替换斜杠的情况下)
解决方案 9:
这是我最终使用的解决方案:
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 调用会将重音字符替换为非重音字符,这比简单地删除它们要好。之后,所有不允许的字符都会被删除。
我的解决方案不会在前面添加已知字符串来避免可能不允许的文件名,因为我知道在我的特定文件名格式下它们不会出现。更通用的解决方案需要这样做。
解决方案 10:
请记住,Unix 系统上对文件名实际上没有任何限制,除了
它可能不包含 \0
它可能不包含 /
其余一切都是公平的。
$触摸”
> 甚至多行
>哈哈
> ^[[31m 红色 ^[[0m
> 邪恶”
$ ls -la
-rw-r--r-- 0 11月17日 23:39 ?甚至多行?哈哈??[31m 红色 ?[0m?邪恶
$ ls -实验室
-rw-r--r-- 0 11月 17日 23:39
even multiline
haha
[31m red [0m
evil
$ perl -e '对于我的 $i ( glob(q{./*even*}) ){ print $i; } '
./
甚至多行
哈哈
红色的
邪恶的
是的,我只是将 ANSI 颜色代码存储在文件名中并使其生效。
为了娱乐,在目录名中放置一个 BEL 字符,然后观看当您将 CD 放入其中时发生的乐趣;)
解决方案 11:
我意识到有很多答案,但它们大多依赖于正则表达式或外部模块,所以我想提出自己的答案。纯 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
如果您愿意,您可以在开头将您自己的有效字符添加到validchars
变量中,例如英文字母中不存在的国家字母。这可能是您想要的,也可能不是:某些不使用 UTF-8 运行的文件系统可能仍会遇到非 ASCII 字符的问题。
此函数用于测试单个文件名的有效性,因此它将路径分隔符替换为 _,认为它们是无效字符。如果您想添加它,只需修改if
以包含操作系统路径分隔符即可。
解决方案 12:
如果你不介意安装一个包,这应该很有用:
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"
")
fname = " _a*b:c<d>e%f/(g)h+i_0.txt"
print(f"{fname} -> {sanitize_filename(fname)}
")
### 输出
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
解决方案 13:
您可以使用 re.sub() 方法来替换任何非“文件类”的内容。但实际上,每个字符都可能是有效的;因此没有预建函数(我相信)来完成它。
import re
str = "File!name?.txt"
f = open(os.path.join("/tmp", re.sub('[^-a-zA-Z0-9_.() ]+', '', str))
将导致文件句柄为 /tmp/filename.txt。
解决方案 14:
我喜欢这里的 python-slugify 方法,但它也会删除点,这不是我想要的。因此,我对其进行了优化,以便通过以下方式将干净的文件名上传到 s3:
pip install python-slugify
示例代码:
s = 'Very / Unsafe / file
name hähä
.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
输出:
>>> clean_filename
'very-unsafe-file-name-haha.txt'
这是非常安全的,它可以用于没有扩展名的文件名,甚至仅适用于不安全字符的文件名(结果在none
这里)。
解决方案 15:
其他评论尚未解决的另一个问题是空字符串,这显然不是有效的文件名。您还可能因为删除了太多字符而得到空字符串。
由于 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 讨厌空格并使用“。”作为目录分隔符。
解决方案 16:
不过你必须要小心。如果你只看拉丁语,你的简介中没有明确说明。如果你只用 ascii 字符来清理某些单词,它们可能会变得毫无意义或具有其他含义。
假设你有“forêt poésie”(森林诗),你的净化处理可能会得到“fort-posie”(强+一些无意义的东西)
如果必须处理汉字,情况就更糟了。
“下北沢”你的系统最终可能会执行“---”,这注定会在一段时间后失败并且没有什么帮助。所以如果你只处理文件,我建议要么称它们为你控制的通用链,要么保持字符不变。对于 URI,大致相同。
解决方案 17:
为什么不直接用 try/except 包装“osopen”并让底层操作系统判断该文件是否有效?
这似乎不需要做太多工作,而且无论您使用哪种操作系统都是有效的。
解决方案 18:
>>> import string
>>> safechars = bytearray(('_-.()' + string.digits + string.ascii_letters).encode())
>>> allchars = bytearray(range(0x100))
>>> deletechars = bytearray(set(allchars) - set(safechars))
>>> filename = u'#abxa0c.$%.txt'
>>> safe_filename = filename.encode('ascii', 'ignore').translate(None, deletechars).decode()
>>> safe_filename
'abc..txt'
它不处理空字符串、特殊文件名(‘nul’、‘con’等)。
解决方案 19:
答案已针对 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)
解决方案 20:
对于 Windows 特定路径的另一个答案,使用简单的替换并且不使用时髦的模块:
import re
def check_for_illegal_char(input_str):
# remove illegal characters for Windows file names/paths
# (illegal filenames are a superset (41) of the illegal path names (36))
# this is according to windows blacklist obtained with Powershell
# from: https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names/44750843#44750843
#
# PS> $enc = [system.Text.Encoding]::UTF8
# PS> $FileNameInvalidChars = [System.IO.Path]::GetInvalidFileNameChars()
# PS> $FileNameInvalidChars | foreach { $enc.GetBytes($_) } | Out-File -FilePath InvalidFileCharCodes.txt
illegal = '/u0022/u003c/u003e/u007c/u0000/u0001/u0002/u0003/u0004/u0005/u0006/u0007/u0008' + \n '/u0009/u000a/u000b/u000c/u000d/u000e/u000f/u0010/u0011/u0012/u0013/u0014/u0015' + \n '/u0016/u0017/u0018/u0019/u001a/u001b/u001c/u001d/u001e/u001f/u003a/u002a/u003f/u005c/u002f'
output_str, _ = re.subn('['+illegal+']','_', input_str)
output_str = output_str.replace('\\','_') # backslash cannot be handled by regex
output_str = output_str.replace('..','_') # double dots are illegal too, or at least a bad idea
output_str = output_str[:-1] if output_str[-1] == '.' else output_str # can't have end of line '.'
if output_str != input_str:
print(f"The name '{input_str}' had invalid characters, "
f"name was modified to '{output_str}'")
return output_str
当用 进行测试时check_for_illegal_char('fas/u0003/u0004good\..asd.')
,我得到:
The name 'fas♥♦good..asd.' had invalid characters, name was modified to 'fas__good__asd'
解决方案 21:
不完全是 OP 所要求的,但这是我使用的,因为我需要唯一且可逆的转换:
# p3 code
def safePath (url):
return ''.join(map(lambda ch: chr(ch) if ch in safePath.chars else '%%%02x' % ch, url.encode('utf-8')))
safePath.chars = set(map(lambda x: ord(x), '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-_ .'))
至少从系统管理员的角度来看,结果“有点”可读。
解决方案 22:
当遇到同样的问题时,我使用了 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
slugify.py
python-slugify 包文件内的全局变量,以“。”扩展。
请记住,像 这样的特殊字符.()
必须用 进行转义``。
如果您想保留大写字母,请使用该lowercase=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
解决方案 23:
大多数解决方案都不起作用。
‘/hello/world’ -> ‘helloworld’
‘/helloworld’/->‘helloworld’
这通常不是您想要的,假设您正在保存每个链接的 html,您将覆盖不同网页的 html。
我腌制了一个字典,例如:
{'helloworld':
(
{'/hello/world': 'helloworld', '/helloworld/': 'helloworld1'},
2)
}
2 代表应附加到下一个文件名的数字。
我每次都会从字典中查找文件名。如果不存在,我会创建一个新的,并根据需要附加最大数字。
解决方案 24:
仍然没有找到一个好的库来生成有效的文件名。请注意,在德语、挪威语或法语等语言中,文件名中的特殊字符非常常见,完全没问题。所以我最终有了自己的库:
# util/files.py
CHAR_MAX_LEN = 31
CHAR_REPLACE = '_'
ILLEGAL_CHARS = [
'#', # pound
'%', # percent
'&', # ampersand
'{', # left curly bracket
'}', # right curly bracket
'\\', # back slash
'<', # left angle bracket
'>', # right angle bracket
'*', # asterisk
'?', # question mark
'/', # forward slash
' ', # blank spaces
'$', # dollar sign
'!', # exclamation point
"'", # single quotes
'"', # double quotes
':', # colon
'@', # at sign
'+', # plus sign
'`', # backtick
'|', # pipe
'=', # equal sign
]
def generate_filename(
name, char_replace=CHAR_REPLACE, length=CHAR_MAX_LEN,
illegal=ILLEGAL_CHARS, replace_dot=False):
''' return clean filename '''
# init
_elem = name.split('.')
extension = _elem[-1].strip()
_length = length - len(extension) - 1
label = '.'.join(_elem[:-1]).strip()[:_length]
filename = ''
# replace '.' ?
if replace_dot:
label = label.replace('.', char_replace)
# clean
for char in label + '.' + extension:
if char in illegal:
char = char_replace
filename += char
return filename
generate_filename('nucgae zutaäer..0.1.docx', replace_dot=False)
nucgae_zutaäer..0.1.docx
generate_filename('nucgae zutaäer..0.1.docx', replace_dot=True)
nucgae_zutaäer__0_1.docx
解决方案 25:
我确信这不是一个很好的答案,因为它修改了它循环的字符串,但它似乎工作正常:
import string
for chr in your_string:
if chr == ' ':
your_string = your_string.replace(' ', '_')
elif chr not in string.ascii_letters or chr not in string.digits:
your_string = your_string.replace(chr, '')
解决方案 26:
更新
这个 6 年前的答案中的所有链接均已损坏,无法修复。
此外,我也不会再这样做了,只需base64
编码或删除不安全的字符。 Python 3 示例:
import re
t = re.compile("[a-zA-Z0-9.,_-]")
unsafe = "abc∂éåß®∆˚˙©¬ñ√ƒµ©∆∫ø"
safe = [ch for ch in unsafe if t.match(ch)]
# => 'abc'
您base64
可以进行编码和解码,这样您就可以再次检索原始文件名。
但根据使用情况,您最好生成一个随机文件名并将元数据存储在单独的文件或数据库中。
from random import choice
from string import ascii_lowercase, ascii_uppercase, digits
allowed_chr = ascii_lowercase + ascii_uppercase + digits
safe = ''.join([choice(allowed_chr) for _ in range(16)])
# => 'CYQ4JDKE9JfcRzAZ'
原始链接腐烂答案:
该bobcat
项目包含一个可以完成此任务的 python 模块。
它不是完全强大的,请参阅这个帖子和这个回复。
因此,如上所述:base64
如果可读性并不重要,那么编码可能是一个更好的想法。
文档https://svn.origo.ethz.ch/bobcat/src-doc/safefilename-module.html
来源https://svn.origo.ethz.ch/bobcat/trunk/src/bobcatlib/safefilename.py
解决方案 27:
到这里,这应该涵盖了所有基础。它可以为您处理所有类型的问题,包括(但不限于)字符替换。
适用于 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,' \n 'COM5,COM6,COM7,COM8,COM9,LPT1,LPT2,' \n 'LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,' \n '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
此解决方案不需要外部库。它还替换了不可打印的文件名,因为它们并不总是容易处理。
解决方案 28:
使用 pytest 的最小工作示例
将每个非A-Z
、或 的a-z
字符转换为。0-9
`-`_
演示
slugify("a b c")
Out[9]: 'a_b_c'
slugify("https://www.algorithmus-schmiede.de/kontakt/")
Out[10]: 'https___www_algorithmus-schmiede_de_kontakt_'
slugify("a-b c")
Out[11]: 'a-b_c'
完整代码(带 pytest)
import re
def slugify(str_: str):
slug = re.sub(r'[^A-z0-9-]', '_', str_)
return slug
import pytest
@pytest.mark.parametrize(
"inp, outp_exp",
[
pytest.param("a b c", "a_b_c", id="whitespace -> underscore"),
pytest.param("https://www.algorithmus-schmiede.de/kontakt/",
"https___www_algorithmus-schmiede_de_kontakt_", id="url"),
pytest.param("a-b c", "a-b_c", id="minus conserved"),
]
)
def test_slugify(inp, outp_exp):
assert slugify(inp) == outp_exp
解决方案 29:
如何解决 Windows 的限制字符
“很多人不知道的是,Windows 的文件名支持 Unicode 字符集,并且它只阻止 ASCII 可打印字符版本”
您不能使用原始字符串作为文件路径。它会使转义字符/u
不可变,并将被视为文件路径的一部分,并且不会将其转换为 Unicode 字符。它会引发FileNotFoundError
异常。
将 ASCII 字符替换为字符的 Unicode 版本,然后重命名文件。
from os import environ, rename
rename(f'{environ['USERPROFILE']}\\Desktop\\PotPlayer (SuspendResume).lnk', f'{environ['USERPROFILE']}\\Desktop\\PotPlayer (Suspend /u2215 Resume).lnk')
输出:
PotPlayer (Suspend / Resume).lnk