问题:检查路径在Python中是否有效,而无需在路径的目标位置创建文件
我有一个路径(包括目录和文件名)。
我需要测试文件名是否有效,例如,文件系统是否允许我创建具有该名称的文件。
文件名中包含一些Unicode字符。
可以安全地假设路径的目录段是有效且可访问的(我试图使这个问题更笼统地适用,并且显然我走得太远了)。
除非必须,否则我非常不想逃脱任何东西。
我会发布一些我正在处理的示例字符,但是显然它们会被堆栈交换系统自动删除。无论如何,我想保留标准的unicode实体,例如ö
,仅转义文件名中无效的内容。
这里是要抓住的地方。路径目标上可能已经(可能没有)文件。如果该文件存在,我需要保留该文件,如果不存在,则不要创建该文件。
基本上,我想检查是否可以在不实际打开写入路径的情况下写入路径(以及通常需要进行的自动文件创建/文件破坏)。
因此:
try:
open(filename, 'w')
except OSError:
# handle error here
这是不可接受的,因为它将覆盖我不想触摸的现有文件(如果存在),或者如果不存在则创建该文件。
我知道我可以做:
if not os.access(filePath, os.W_OK):
try:
open(filePath, 'w').close()
os.unlink(filePath)
except OSError:
# handle error here
但这将在处创建文件filePath
,然后我将不得不os.unlink
。
最后,似乎花了6或7行来完成应该简单os.isvalidpath(filePath)
或相似的操作。
顺便说一句,我需要在(至少)Windows和MacOS上运行它,因此我想避免使用特定于平台的东西。
“
回答 0
tl; dr
调用is_path_exists_or_creatable()
下面定义的函数。
严格地使用Python3。这就是我们的发展方向。
两个问题的故事
问题“如何测试路径名的有效性,以及对于有效路径名,这些路径的存在或可写性?” 显然是两个独立的问题。两者都很有趣,而且在这里还是我能找到的任何地方都没有收到真正令人满意的答案。
- 不必要地打开(然后无法可靠地关闭)文件句柄。
- 不必要的写作( …然后无法可靠地关闭或删除)0字节文件。
- 忽略操作系统特定的错误,以区分不可忽略的无效路径名和可忽略的文件系统问题。毫不奇怪,这在Windows下至关重要。(见下文。)
- 忽略由外部进程同时(重新)移动要测试的路径名的父目录导致的竞争条件。(见下文。)
- 忽略此路径名导致的连接超时,该路径名位于陈旧,缓慢或暂时不可访问的文件系统上。这可能会使面向公众的服务遭受潜在的DoS驱动的攻击。(见下文。)
我们将解决所有问题。
问题#0:路径名有效性又是什么?
在将我们脆弱的肉类衣服扔进Python般的痛苦中之前,我们可能应该定义“路径名有效性”的含义。究竟是什么定义了有效性?
“路径名有效性”是指路径名相对于当前系统的根文件系统的语法正确性,无论该路径或其父目录是否物理存在。如果路径名符合根文件系统的所有语法要求,则在此定义下语法上正确。
所谓“根文件系统”,是指:
- 在与POSIX兼容的系统上,文件系统已安装到根目录(
/
)。 - 在Windows中,文件系统安装到
%HOMEDRIVE%
,包含当前的Windows安装(通常但结肠-后缀盘符不必然C:
)。
反过来,“语法正确性”的含义取决于根文件系统的类型。对于ext4
(且不是大多数但与所有POSIX兼容的)文件系统,路径名称在且仅当该路径名称在语法上正确:
- 不包含空字节(即,
\x00
在Python中)。这是所有POSIX兼容文件系统的硬性要求。 - 包含不超过255个字节的路径组件(例如,
'a'*256
在Python中)。路径成分是含有不路径名的最长子串/
字符(例如,bergtatt
,ind
,i
,和fjeldkamrene
在路径名/bergtatt/ind/i/fjeldkamrene
)。
句法正确性。根文件系统。而已。
问题1:我们现在应如何进行路径名有效性?
令人惊讶的是,在Python中验证路径名是不直观的。我在这里与Fake Name达成坚定协议:官方os.path
软件包应为此提供现成的解决方案。出于未知(可能不令人信服)的原因,事实并非如此。幸运的是,展开您自己的临时解决方案并不是那么费劲……
好的,实际上是。毛茸茸的 讨厌 它在发光时发出嘶哑和咯咯笑声时可能会发痒。但是你会怎么做?Nuthin’。
我们将很快进入低级代码的放射性深渊。但首先,让我们谈谈高级商店。当传递无效的路径名时,标准os.stat()
和os.lstat()
函数会引发以下异常:
- 对于驻留在不存在的目录中的路径名,
FileNotFoundError
。 - 对于现有目录中的路径名:
- 在Windows下,
WindowsError
其winerror
属性为123
(即ERROR_INVALID_NAME
)的实例。 - 在所有其他操作系统下:
- 对于包含空字节(即
'\x00'
)的路径名,请使用的实例TypeError
。 - 对于包含长度超过255个字节的路径成分的路径名,
OSError
其errcode
属性的实例为:- 在SunOS和* BSD系列操作系统下,
errno.ERANGE
。(这似乎是操作系统级别的错误,否则称为POSIX标准的“选择性解释”。) - 在所有其他操作系统下,
errno.ENAMETOOLONG
。
- 在SunOS和* BSD系列操作系统下,
- 在Windows下,
至关重要的是,这意味着仅存在于现有目录中的路径名是有效的。当传递的路径名驻留在不存在的目录中时,不管这些路径名是否无效,os.stat()
andos.lstat()
函数都会引发通用FileNotFoundError
异常。目录存在优先于路径名无效。
这是否意味着不存在的目录中的路径名无效?是的-除非我们修改这些路径名以驻留在现有目录中。但是,这甚至安全可行吗?修改路径名是否应该阻止我们验证原始路径名?
要回答这个问题,请从上面回忆一下,ext4
文件系统上语法正确的路径名不包含路径组件(A)包含空字节,或(B)长度超过255个字节。因此,ext4
仅当该路径名中的所有路径组件均有效时,该路径名才有效。大多数 现实世界中感兴趣的文件系统都是如此。
那根学究的见解真的对我们有帮助吗?是。它将一次验证完整路径名的较大问题减少到仅验证该路径名中的所有路径组成部分的较小问题。通过遵循以下算法,可以以跨平台方式对任意路径名进行有效验证(无论该路径名是否位于现有目录中):
- 将该路径名拆分为路径组成部分(例如,将路径名
/troldskog/faren/vild
拆分为list['', 'troldskog', 'faren', 'vild']
)。 - 对于每个这样的组件:
- 将保证与该组件一起存在的目录的路径名加入新的临时路径名(例如
/troldskog
)。 - 将该路径名传递给
os.stat()
或os.lstat()
。如果该路径名及其组件无效,则可以确保此调用引发一个暴露无效类型的异常,而不是通用FileNotFoundError
异常。为什么?因为该路径名位于现有目录中。(循环逻辑是循环的。)
- 将保证与该组件一起存在的目录的路径名加入新的临时路径名(例如
是否有目录保证存在?是的,但通常只有一个:根文件系统的最顶层目录(如上定义)。
将驻留在任何其他目录(因此不保证存在)中的路径名传递给竞争条件os.stat()
或os.lstat()
引发竞争条件,即使该目录先前已被测试存在。为什么?因为在执行该测试之后但在将该路径名传递给os.stat()
or之前,无法阻止外部进程同时删除该目录os.lstat()
。释放令人发疯的狗!
上述方法也有一个很大的附带好处:安全性。(是不是说好的?)具体为:
前端应用程序通过简单地将这样的路径名传递给拒绝服务(DoS)攻击
os.stat()
或os.lstat()
容易受到拒绝的攻击,从而验证来自不受信任来源的任意路径名。恶意用户可能试图反复验证驻留在已知陈旧或缓慢的文件系统上的路径名(例如,NFS Samba共享);在这种情况下,盲目声明传入的路径名可能最终会因连接超时而失败,或者消耗的时间和资源要比您承受失业的能力弱。
上面的方法通过仅针对根文件系统的根目录验证路径名的路径组成部分来避免这种情况。(即使这是陈旧,缓慢或无法访问的,也比路径名验证要麻烦得多。)
丢失?大。让我们开始。(假定使用Python3。请参阅“ leycec对300的脆弱希望是什么?”)
import errno, os
# Sadly, Python fails to provide the following magic number for us.
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
Official listing of all such codes.
'''
def is_pathname_valid(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
'''
# If this pathname is either not a string or is but is empty, this pathname
# is invalid.
try:
if not isinstance(pathname, str) or not pathname:
return False
# Strip this pathname's Windows-specific drive specifier (e.g., `C:\`)
# if any. Since Windows prohibits path components from containing `:`
# characters, failing to strip this `:`-suffixed prefix would
# erroneously invalidate all valid absolute Windows pathnames.
_, pathname = os.path.splitdrive(pathname)
# Directory guaranteed to exist. If the current OS is Windows, this is
# the drive to which Windows was installed (e.g., the "%HOMEDRIVE%"
# environment variable); else, the typical root directory.
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law
# Append a path separator to this directory if needed.
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
# Test whether each path component split from this pathname is valid or
# not, ignoring non-existent and non-readable path components.
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
# If an OS-specific exception is raised, its error code
# indicates whether this pathname is valid or not. Unless this
# is the case, this exception implies an ignorable kernel or
# filesystem complaint (e.g., path not found or inaccessible).
#
# Only the following exceptions indicate invalid pathnames:
#
# * Instances of the Windows-specific "WindowsError" class
# defining the "winerror" attribute whose value is
# "ERROR_INVALID_NAME". Under Windows, "winerror" is more
# fine-grained and hence useful than the generic "errno"
# attribute. When a too-long pathname is passed, for example,
# "errno" is "ENOENT" (i.e., no such file or directory) rather
# than "ENAMETOOLONG" (i.e., file name too long).
# * Instances of the cross-platform "OSError" class defining the
# generic "errno" attribute whose value is either:
# * Under most POSIX-compatible OSes, "ENAMETOOLONG".
# * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE".
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
# If a "TypeError" exception was raised, it almost certainly has the
# error message "embedded NUL character" indicating an invalid pathname.
except TypeError as exc:
return False
# If no exception was raised, all path components and hence this
# pathname itself are valid. (Praise be to the curmudgeonly python.)
else:
return True
# If any other exception was raised, this is an unrelated fatal issue
# (e.g., a bug). Permit this exception to unwind the call stack.
#
# Did we mention this should be shipped with Python already?
做完了 不要斜视那个代码。(它咬。)
问题2:路径名的存在或可创建性可能无效,是吗?
在上述解决方案的基础上,测试可能无效的路径名的存在或可创建性通常很简单。这里的关键是在测试传递的路径之前调用先前定义的函数:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
return os.access(dirname, os.W_OK)
def is_path_exists_or_creatable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS _and_
either currently exists or is hypothetically creatable; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
完成和完成的。除了不太一样。
问题3:Windows上可能存在无效的路径名或可写性
有一个警告。当然有。
官方os.access()
文件承认:
注意:即使
os.access()
表明I / O操作将成功,它也可能会失败,尤其是对于网络文件系统上的操作,其权限语义可能超出通常的POSIX权限位模型。
毫不奇怪,Windows通常是这里的嫌疑人。由于在NTFS文件系统上广泛使用了访问控制列表(ACL),因此简单的POSIX权限位模型无法很好地映射到底层Windows现实。尽管这(不是问题)不是Python的错,但对于与Windows兼容的应用程序,它可能仍然值得关注。
如果是您,那么需要一个更强大的替代方案。如果传递的路径也不会存在,我们不是试图建立保证该路径的父目录被立即删除临时文件- creatability的更便携的(如昂贵的)测试:
import os, tempfile
def is_path_sibling_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create **siblings**
(i.e., arbitrary files in the parent directory) of the passed pathname;
`False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
try:
# For safety, explicitly close and hence delete this temporary file
# immediately after creating it in the passed path's parent directory.
with tempfile.TemporaryFile(dir=dirname): pass
return True
# While the exact type of exception raised by the above function depends on
# the current version of the Python interpreter, all such types subclass the
# following exception superclass.
except EnvironmentError:
return False
def is_path_exists_or_creatable_portable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname on the current OS _and_
either currently exists or is hypothetically creatable in a cross-platform
manner optimized for POSIX-unfriendly filesystems; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
但是请注意,即使这可能还不够。
多亏了用户访问控制(UAC),永远无法模仿的Windows Vista及其所有后续迭代都明显地涉及与系统目录有关的权限。当非管理员用户尝试在规范目录C:\Windows
或C:\Windows\system32
目录中创建文件时,UAC会从表面上允许用户这样做,同时实际上将所有创建的文件隔离到该用户配置文件中的“虚拟存储”中。(谁能想到欺骗用户会产生有害的长期后果?)
这太疯狂了。这是Windows。
证明给我看
敢吗 现在该进行上述测试了。
由于NULL是面向UNIX的文件系统上路径名中唯一禁止使用的字符,因此让我们利用它来展示冷酷的事实–忽略不可忽略的Windows恶作剧,坦白地说,这同样使我感到厌烦并激怒了我:
>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False
超越理智。超越痛苦。您会发现Python可移植性问题。
回答 1
if os.path.exists(filePath):
#the file is there
elif os.access(os.path.dirname(filePath), os.W_OK):
#the file does not exists but write privileges are given
else:
#can not write there
请注意,path.exists
失败的原因the file is not there
可能不仅仅是,所以您可能必须进行更精细的测试,例如测试包含目录是否存在等等。
在与OP讨论之后,事实证明,主要的问题似乎是文件名可能包含文件系统不允许的字符。当然,需要将它们删除,但是OP希望在文件系统允许的范围内保持尽可能多的人可读性。
可悲的是,我不知道有什么好的解决方案。但是,塞西尔·库里(Cecil Curry)的答案更仔细地研究了发现问题。
回答 2
使用Python 3,如何:
try:
with open(filename, 'x') as tempfile: # OSError if file exists or is invalid
pass
except OSError:
# handle error here
使用“ x”选项,我们也不必担心比赛条件。请参阅此处的文档。
现在,如果该文件尚不存在,它将创建一个寿命很短的临时文件-除非名称无效。如果您可以忍受,那么可以简化很多事情。
回答 3
open(filename,'r') #2nd argument is r and not w
将打开文件或给出错误(如果不存在)。如果有错误,那么您可以尝试写入路径,如果不能,则出现第二个错误
try:
open(filename,'r')
return True
except IOError:
try:
open(filename, 'w')
return True
except IOError:
return False
也可以在这里查看有关Windows权限的信息